Coverage for /Users/davegaeddert/Development/dropseed/plain/plain/plain/utils/decorators.py: 15%
41 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-16 22:03 -0500
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-16 22:03 -0500
1"Functions that help with dynamically creating decorators for views."
3from functools import partial, update_wrapper, wraps
6class classonlymethod(classmethod):
7 def __get__(self, instance, cls=None):
8 if instance is not None:
9 raise AttributeError(
10 "This method is available only on the class, not on instances."
11 )
12 return super().__get__(instance, cls)
15def _update_method_wrapper(_wrapper, decorator):
16 # _multi_decorate()'s bound_method isn't available in this scope. Cheat by
17 # using it on a dummy function.
18 @decorator
19 def dummy(*args, **kwargs):
20 pass
22 update_wrapper(_wrapper, dummy)
25def _multi_decorate(decorators, method):
26 """
27 Decorate `method` with one or more function decorators. `decorators` can be
28 a single decorator or an iterable of decorators.
29 """
30 if hasattr(decorators, "__iter__"):
31 # Apply a list/tuple of decorators if 'decorators' is one. Decorator
32 # functions are applied so that the call order is the same as the
33 # order in which they appear in the iterable.
34 decorators = decorators[::-1]
35 else:
36 decorators = [decorators]
38 def _wrapper(self, *args, **kwargs):
39 # bound_method has the signature that 'decorator' expects i.e. no
40 # 'self' argument, but it's a closure over self so it can call
41 # 'func'. Also, wrap method.__get__() in a function because new
42 # attributes can't be set on bound method objects, only on functions.
43 bound_method = wraps(method)(partial(method.__get__(self, type(self))))
44 for dec in decorators:
45 bound_method = dec(bound_method)
46 return bound_method(*args, **kwargs)
48 # Copy any attributes that a decorator adds to the function it decorates.
49 for dec in decorators:
50 _update_method_wrapper(_wrapper, dec)
51 # Preserve any existing attributes of 'method', including the name.
52 update_wrapper(_wrapper, method)
53 return _wrapper
56def method_decorator(decorator, name=""):
57 """
58 Convert a function decorator into a method decorator
59 """
61 # 'obj' can be a class or a function. If 'obj' is a function at the time it
62 # is passed to _dec, it will eventually be a method of the class it is
63 # defined on. If 'obj' is a class, the 'name' is required to be the name
64 # of the method that will be decorated.
65 def _dec(obj):
66 if not isinstance(obj, type):
67 return _multi_decorate(decorator, obj)
68 if not (name and hasattr(obj, name)):
69 raise ValueError(
70 "The keyword argument `name` must be the name of a method "
71 f"of the decorated class: {obj}. Got '{name}' instead."
72 )
73 method = getattr(obj, name)
74 if not callable(method):
75 raise TypeError(
76 f"Cannot decorate '{name}' as it isn't a callable attribute of "
77 f"{obj} ({method})."
78 )
79 _wrapper = _multi_decorate(decorator, method)
80 setattr(obj, name, _wrapper)
81 return obj
83 # Don't worry about making _dec look similar to a list/tuple as it's rather
84 # meaningless.
85 if not hasattr(decorator, "__iter__"):
86 update_wrapper(_dec, decorator)
87 # Change the name to aid debugging.
88 obj = decorator if hasattr(decorator, "__name__") else decorator.__class__
89 _dec.__name__ = "method_decorator(%s)" % obj.__name__
90 return _dec