Coverage for lazy_settings/utils/functional.py: 68%
74 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-11 03:57 +0330
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-11 03:57 +0330
1from __future__ import annotations
3import copy
4import operator
5from collections.abc import Callable, Iterator
6from typing import Any, Generic, TypeVar, cast
8empty: object = object()
11_T = TypeVar("_T")
12Wrapped = TypeVar("Wrapped")
15def new_method_proxy(func: Callable[..., _T]) -> Callable[..., _T]:
16 def inner(self, *args):
17 if (_wrapped := self._wrapped) is empty:
18 self._setup()
19 _wrapped = self._wrapped
20 return func(_wrapped, *args)
22 inner._mask_wrapped = False # type: ignore[attr-defined]
23 return inner
26def unpickle_lazyobject(wrapped: Wrapped) -> Wrapped:
27 """
28 Used to unpickle lazy objects. Just return its argument, which will be the
29 wrapped object.
30 """
31 return wrapped
34class LazyObject(Generic[Wrapped]):
35 """
36 A wrapper for another class that can be used to delay instantiation of the
37 wrapped class.
39 By subclassing, you have the opportunity to intercept and alter the
40 instantiation. If you don't need to do that, use SimpleLazyObject.
41 """
43 # Avoid infinite recursion when tracing __init__ (#19456).
44 _wrapped: Wrapped | None | object = None
46 def __init__(self) -> None:
47 # Note: if a subclass overrides __init__(), it will likely need to
48 # override __copy__() and __deepcopy__() as well.
49 self._wrapped = empty
51 def __getattribute__(self, name: str) -> Any:
52 if name == "_wrapped":
53 # Avoid recursion when getting wrapped object.
54 return super().__getattribute__(name)
55 value = super().__getattribute__(name)
56 # If attribute is a proxy method, raise an AttributeError to call
57 # __getattr__() and use the wrapped object method.
58 if not getattr(value, "_mask_wrapped", True):
59 raise AttributeError
60 return value
62 __getattr__: Callable = new_method_proxy(getattr)
64 def __setattr__(self, name: str, value: Any) -> None:
65 if name == "_wrapped":
66 # Assign to __dict__ to avoid infinite __setattr__ loops.
67 self.__dict__["_wrapped"] = value
68 else:
69 if self._wrapped is empty:
70 self._setup()
71 setattr(self._wrapped, name, value)
73 def __delattr__(self, name: str) -> None:
74 if name == "_wrapped":
75 raise TypeError("can't delete _wrapped.")
76 if self._wrapped is empty:
77 self._setup()
78 delattr(self._wrapped, name)
80 def _setup(self) -> None:
81 """
82 Must be implemented by subclasses to initialize the wrapped object.
83 """
84 raise NotImplementedError(
85 "subclasses of LazyObject must provide a _setup() method"
86 )
88 # Because we have messed with __class__ below, we confuse pickle as to what
89 # class we are pickling. We're going to have to initialize the wrapped
90 # object to successfully pickle it, so we might as well just pickle the
91 # wrapped object since they're supposed to act the same way.
92 #
93 # Unfortunately, if we try to simply act like the wrapped object, the ruse
94 # will break down when pickle gets our id(). Thus we end up with pickle
95 # thinking, in effect, that we are a distinct object from the wrapped
96 # object, but with the same __dict__. This can cause problems (see #25389).
97 #
98 # So instead, we define our own __reduce__ method and custom unpickler. We
99 # pickle the wrapped object as the unpickler's argument, so that pickle
100 # will pickle it normally, and then the unpickler simply returns its
101 # argument.
102 def __reduce__(self) -> tuple[Callable[[Wrapped], Wrapped], tuple[Wrapped]]:
103 if self._wrapped is empty:
104 self._setup()
105 return (unpickle_lazyobject, (cast(Wrapped, self._wrapped),))
107 def __copy__(self) -> "LazyObject | Wrapped":
108 if self._wrapped is empty:
109 # If uninitialized, copy the wrapper. Use type(self), not
110 # self.__class__, because the latter is proxied.
111 return type(self)()
112 else:
113 # If initialized, return a copy of the wrapped object.
114 return copy.copy(self._wrapped) # type: ignore[return-value]
116 def __deepcopy__(self, memo: dict[int, Any]) -> "LazyObject | Wrapped":
117 if self._wrapped is empty:
118 # We have to use type(self), not self.__class__, because the
119 # latter is proxied.
120 result = type(self)()
121 memo[id(self)] = result
122 return result
123 return copy.deepcopy(self._wrapped, memo) # type: ignore[return-value]
125 __bytes__: Callable[..., bytes] = new_method_proxy(bytes)
126 __str__: Callable[..., str] = new_method_proxy(str)
127 __bool__: Callable[..., bool] = new_method_proxy(bool)
129 # Introspection support
130 __dir__: Callable = new_method_proxy(dir)
132 # Need to pretend to be the wrapped class, for the sake of objects that
133 # care about this (especially in equality tests)
134 __class__ = property(new_method_proxy(operator.attrgetter("__class__"))) # type: ignore[assignment]
135 __eq__: Callable[..., bool] = new_method_proxy(operator.eq)
136 __lt__: Callable[..., bool] = new_method_proxy(operator.lt)
137 __gt__: Callable[..., bool] = new_method_proxy(operator.gt)
138 __ne__: Callable[..., bool] = new_method_proxy(operator.ne)
139 __hash__: Callable[..., int] = new_method_proxy(hash)
141 # List/Tuple/Dictionary methods support
142 __getitem__: Callable = new_method_proxy(operator.getitem)
143 __setitem__: Callable[..., None] = new_method_proxy(operator.setitem)
144 __delitem__: Callable[..., None] = new_method_proxy(operator.delitem)
145 __iter__: Callable[..., Iterator] = new_method_proxy(iter)
146 __len__: Callable[..., int] = new_method_proxy(len)
147 __contains__: Callable[..., bool] = new_method_proxy(operator.contains)