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

1from __future__ import annotations 

2 

3import copy 

4import operator 

5from collections.abc import Callable, Iterator 

6from typing import Any, Generic, TypeVar, cast 

7 

8empty: object = object() 

9 

10 

11_T = TypeVar("_T") 

12Wrapped = TypeVar("Wrapped") 

13 

14 

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) 

21 

22 inner._mask_wrapped = False # type: ignore[attr-defined] 

23 return inner 

24 

25 

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 

32 

33 

34class LazyObject(Generic[Wrapped]): 

35 """ 

36 A wrapper for another class that can be used to delay instantiation of the 

37 wrapped class. 

38 

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 """ 

42 

43 # Avoid infinite recursion when tracing __init__ (#19456). 

44 _wrapped: Wrapped | None | object = None 

45 

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 

50 

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 

61 

62 __getattr__: Callable = new_method_proxy(getattr) 

63 

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) 

72 

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) 

79 

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 ) 

87 

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),)) 

106 

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] 

115 

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] 

124 

125 __bytes__: Callable[..., bytes] = new_method_proxy(bytes) 

126 __str__: Callable[..., str] = new_method_proxy(str) 

127 __bool__: Callable[..., bool] = new_method_proxy(bool) 

128 

129 # Introspection support 

130 __dir__: Callable = new_method_proxy(dir) 

131 

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) 

140 

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)