Coverage for /Users/davegaeddert/Development/dropseed/plain/plain/plain/utils/functional.py: 64%
224 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-16 22:04 -0500
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-16 22:04 -0500
1import copy
2import itertools
3import operator
4from functools import total_ordering, wraps
7class cached_property:
8 """
9 Decorator that converts a method with a single self argument into a
10 property cached on the instance.
12 A cached property can be made out of an existing method:
13 (e.g. ``url = cached_property(get_absolute_url)``).
14 """
16 name = None
18 @staticmethod
19 def func(instance):
20 raise TypeError(
21 "Cannot use cached_property instance without calling "
22 "__set_name__() on it."
23 )
25 def __init__(self, func):
26 self.real_func = func
27 self.__doc__ = getattr(func, "__doc__")
29 def __set_name__(self, owner, name):
30 if self.name is None:
31 self.name = name
32 self.func = self.real_func
33 elif name != self.name:
34 raise TypeError(
35 "Cannot assign the same cached_property to two different names "
36 f"({self.name!r} and {name!r})."
37 )
39 def __get__(self, instance, cls=None):
40 """
41 Call the function and put the return value in instance.__dict__ so that
42 subsequent attribute access on the instance returns the cached value
43 instead of calling cached_property.__get__().
44 """
45 if instance is None:
46 return self
47 res = instance.__dict__[self.name] = self.func(instance)
48 return res
51class classproperty:
52 """
53 Decorator that converts a method with a single cls argument into a property
54 that can be accessed directly from the class.
55 """
57 def __init__(self, method=None):
58 self.fget = method
60 def __get__(self, instance, cls=None):
61 return self.fget(cls)
63 def getter(self, method):
64 self.fget = method
65 return self
68class Promise:
69 """
70 Base class for the proxy class created in the closure of the lazy function.
71 It's used to recognize promises in code.
72 """
74 pass
77def lazy(func, *resultclasses):
78 """
79 Turn any callable into a lazy evaluated callable. result classes or types
80 is required -- at least one is needed so that the automatic forcing of
81 the lazy evaluation code is triggered. Results are not memoized; the
82 function is evaluated on every access.
83 """
85 @total_ordering
86 class __proxy__(Promise):
87 """
88 Encapsulate a function call and act as a proxy for methods that are
89 called on the result of that function. The function is not evaluated
90 until one of the methods on the result is called.
91 """
93 __prepared = False
95 def __init__(self, args, kw):
96 self.__args = args
97 self.__kw = kw
98 if not self.__prepared:
99 self.__prepare_class__()
100 self.__class__.__prepared = True
102 def __reduce__(self):
103 return (
104 _lazy_proxy_unpickle,
105 (func, self.__args, self.__kw) + resultclasses,
106 )
108 def __repr__(self):
109 return repr(self.__cast())
111 @classmethod
112 def __prepare_class__(cls):
113 for resultclass in resultclasses:
114 for type_ in resultclass.mro():
115 for method_name in type_.__dict__:
116 # All __promise__ return the same wrapper method, they
117 # look up the correct implementation when called.
118 if hasattr(cls, method_name):
119 continue
120 meth = cls.__promise__(method_name)
121 setattr(cls, method_name, meth)
122 cls._delegate_bytes = bytes in resultclasses
123 cls._delegate_text = str in resultclasses
124 if cls._delegate_bytes and cls._delegate_text:
125 raise ValueError(
126 "Cannot call lazy() with both bytes and text return types."
127 )
128 if cls._delegate_text:
129 cls.__str__ = cls.__text_cast
130 elif cls._delegate_bytes:
131 cls.__bytes__ = cls.__bytes_cast
133 @classmethod
134 def __promise__(cls, method_name):
135 # Builds a wrapper around some magic method
136 def __wrapper__(self, *args, **kw):
137 # Automatically triggers the evaluation of a lazy value and
138 # applies the given magic method of the result type.
139 res = func(*self.__args, **self.__kw)
140 return getattr(res, method_name)(*args, **kw)
142 return __wrapper__
144 def __text_cast(self):
145 return func(*self.__args, **self.__kw)
147 def __bytes_cast(self):
148 return bytes(func(*self.__args, **self.__kw))
150 def __bytes_cast_encoded(self):
151 return func(*self.__args, **self.__kw).encode()
153 def __cast(self):
154 if self._delegate_bytes:
155 return self.__bytes_cast()
156 elif self._delegate_text:
157 return self.__text_cast()
158 else:
159 return func(*self.__args, **self.__kw)
161 def __str__(self):
162 # object defines __str__(), so __prepare_class__() won't overload
163 # a __str__() method from the proxied class.
164 return str(self.__cast())
166 def __eq__(self, other):
167 if isinstance(other, Promise):
168 other = other.__cast()
169 return self.__cast() == other
171 def __lt__(self, other):
172 if isinstance(other, Promise):
173 other = other.__cast()
174 return self.__cast() < other
176 def __hash__(self):
177 return hash(self.__cast())
179 def __mod__(self, rhs):
180 if self._delegate_text:
181 return str(self) % rhs
182 return self.__cast() % rhs
184 def __add__(self, other):
185 return self.__cast() + other
187 def __radd__(self, other):
188 return other + self.__cast()
190 def __deepcopy__(self, memo):
191 # Instances of this class are effectively immutable. It's just a
192 # collection of functions. So we don't need to do anything
193 # complicated for copying.
194 memo[id(self)] = self
195 return self
197 @wraps(func)
198 def __wrapper__(*args, **kw):
199 # Creates the proxy object, instead of the actual value.
200 return __proxy__(args, kw)
202 return __wrapper__
205def _lazy_proxy_unpickle(func, args, kwargs, *resultclasses):
206 return lazy(func, *resultclasses)(*args, **kwargs)
209def lazystr(text):
210 """
211 Shortcut for the common case of a lazy callable that returns str.
212 """
213 return lazy(str, str)(text)
216def keep_lazy(*resultclasses):
217 """
218 A decorator that allows a function to be called with one or more lazy
219 arguments. If none of the args are lazy, the function is evaluated
220 immediately, otherwise a __proxy__ is returned that will evaluate the
221 function when needed.
222 """
223 if not resultclasses:
224 raise TypeError("You must pass at least one argument to keep_lazy().")
226 def decorator(func):
227 lazy_func = lazy(func, *resultclasses)
229 @wraps(func)
230 def wrapper(*args, **kwargs):
231 if any(
232 isinstance(arg, Promise)
233 for arg in itertools.chain(args, kwargs.values())
234 ):
235 return lazy_func(*args, **kwargs)
236 return func(*args, **kwargs)
238 return wrapper
240 return decorator
243def keep_lazy_text(func):
244 """
245 A decorator for functions that accept lazy arguments and return text.
246 """
247 return keep_lazy(str)(func)
250empty = object()
253def new_method_proxy(func):
254 def inner(self, *args):
255 if (_wrapped := self._wrapped) is empty:
256 self._setup()
257 _wrapped = self._wrapped
258 return func(_wrapped, *args)
260 inner._mask_wrapped = False
261 return inner
264class LazyObject:
265 """
266 A wrapper for another class that can be used to delay instantiation of the
267 wrapped class.
269 By subclassing, you have the opportunity to intercept and alter the
270 instantiation. If you don't need to do that, use SimpleLazyObject.
271 """
273 # Avoid infinite recursion when tracing __init__ (#19456).
274 _wrapped = None
276 def __init__(self):
277 # Note: if a subclass overrides __init__(), it will likely need to
278 # override __copy__() and __deepcopy__() as well.
279 self._wrapped = empty
281 def __getattribute__(self, name):
282 if name == "_wrapped":
283 # Avoid recursion when getting wrapped object.
284 return super().__getattribute__(name)
285 value = super().__getattribute__(name)
286 # If attribute is a proxy method, raise an AttributeError to call
287 # __getattr__() and use the wrapped object method.
288 if not getattr(value, "_mask_wrapped", True):
289 raise AttributeError
290 return value
292 __getattr__ = new_method_proxy(getattr)
294 def __setattr__(self, name, value):
295 if name == "_wrapped":
296 # Assign to __dict__ to avoid infinite __setattr__ loops.
297 self.__dict__["_wrapped"] = value
298 else:
299 if self._wrapped is empty:
300 self._setup()
301 setattr(self._wrapped, name, value)
303 def __delattr__(self, name):
304 if name == "_wrapped":
305 raise TypeError("can't delete _wrapped.")
306 if self._wrapped is empty:
307 self._setup()
308 delattr(self._wrapped, name)
310 def _setup(self):
311 """
312 Must be implemented by subclasses to initialize the wrapped object.
313 """
314 raise NotImplementedError(
315 "subclasses of LazyObject must provide a _setup() method"
316 )
318 # Because we have messed with __class__ below, we confuse pickle as to what
319 # class we are pickling. We're going to have to initialize the wrapped
320 # object to successfully pickle it, so we might as well just pickle the
321 # wrapped object since they're supposed to act the same way.
322 #
323 # Unfortunately, if we try to simply act like the wrapped object, the ruse
324 # will break down when pickle gets our id(). Thus we end up with pickle
325 # thinking, in effect, that we are a distinct object from the wrapped
326 # object, but with the same __dict__. This can cause problems (see #25389).
327 #
328 # So instead, we define our own __reduce__ method and custom unpickler. We
329 # pickle the wrapped object as the unpickler's argument, so that pickle
330 # will pickle it normally, and then the unpickler simply returns its
331 # argument.
332 def __reduce__(self):
333 if self._wrapped is empty:
334 self._setup()
335 return (unpickle_lazyobject, (self._wrapped,))
337 def __copy__(self):
338 if self._wrapped is empty:
339 # If uninitialized, copy the wrapper. Use type(self), not
340 # self.__class__, because the latter is proxied.
341 return type(self)()
342 else:
343 # If initialized, return a copy of the wrapped object.
344 return copy.copy(self._wrapped)
346 def __deepcopy__(self, memo):
347 if self._wrapped is empty:
348 # We have to use type(self), not self.__class__, because the
349 # latter is proxied.
350 result = type(self)()
351 memo[id(self)] = result
352 return result
353 return copy.deepcopy(self._wrapped, memo)
355 __bytes__ = new_method_proxy(bytes)
356 __str__ = new_method_proxy(str)
357 __bool__ = new_method_proxy(bool)
359 # Introspection support
360 __dir__ = new_method_proxy(dir)
362 # Need to pretend to be the wrapped class, for the sake of objects that
363 # care about this (especially in equality tests)
364 __class__ = property(new_method_proxy(operator.attrgetter("__class__")))
365 __eq__ = new_method_proxy(operator.eq)
366 __lt__ = new_method_proxy(operator.lt)
367 __gt__ = new_method_proxy(operator.gt)
368 __ne__ = new_method_proxy(operator.ne)
369 __hash__ = new_method_proxy(hash)
371 # List/Tuple/Dictionary methods support
372 __getitem__ = new_method_proxy(operator.getitem)
373 __setitem__ = new_method_proxy(operator.setitem)
374 __delitem__ = new_method_proxy(operator.delitem)
375 __iter__ = new_method_proxy(iter)
376 __len__ = new_method_proxy(len)
377 __contains__ = new_method_proxy(operator.contains)
380def unpickle_lazyobject(wrapped):
381 """
382 Used to unpickle lazy objects. Just return its argument, which will be the
383 wrapped object.
384 """
385 return wrapped
388class SimpleLazyObject(LazyObject):
389 """
390 A lazy object initialized from any function.
392 Designed for compound objects of unknown type. For builtins or objects of
393 known type, use plain.utils.functional.lazy.
394 """
396 def __init__(self, func):
397 """
398 Pass in a callable that returns the object to be wrapped.
400 If copies are made of the resulting SimpleLazyObject, which can happen
401 in various circumstances within Plain, then you must ensure that the
402 callable can be safely run more than once and will return the same
403 value.
404 """
405 self.__dict__["_setupfunc"] = func
406 super().__init__()
408 def _setup(self):
409 self._wrapped = self._setupfunc()
411 # Return a meaningful representation of the lazy object for debugging
412 # without evaluating the wrapped object.
413 def __repr__(self):
414 if self._wrapped is empty:
415 repr_attr = self._setupfunc
416 else:
417 repr_attr = self._wrapped
418 return f"<{type(self).__name__}: {repr_attr!r}>"
420 def __copy__(self):
421 if self._wrapped is empty:
422 # If uninitialized, copy the wrapper. Use SimpleLazyObject, not
423 # self.__class__, because the latter is proxied.
424 return SimpleLazyObject(self._setupfunc)
425 else:
426 # If initialized, return a copy of the wrapped object.
427 return copy.copy(self._wrapped)
429 def __deepcopy__(self, memo):
430 if self._wrapped is empty:
431 # We have to use SimpleLazyObject, not self.__class__, because the
432 # latter is proxied.
433 result = SimpleLazyObject(self._setupfunc)
434 memo[id(self)] = result
435 return result
436 return copy.deepcopy(self._wrapped, memo)
438 __add__ = new_method_proxy(operator.add)
440 @new_method_proxy
441 def __radd__(self, other):
442 return other + self
445def partition(predicate, values):
446 """
447 Split the values into two sets, based on the return value of the function
448 (True/False). e.g.:
450 >>> partition(lambda x: x > 3, range(5))
451 [0, 1, 2, 3], [4]
452 """
453 results = ([], [])
454 for item in values:
455 results[predicate(item)].append(item)
456 return results