Coverage for /Users/davegaeddert/Developer/dropseed/plain/plain/plain/utils/decorators.py: 15%

41 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-23 11:16 -0600

1"Functions that help with dynamically creating decorators for views." 

2 

3from functools import partial, update_wrapper, wraps 

4 

5 

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) 

13 

14 

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 

21 

22 update_wrapper(_wrapper, dummy) 

23 

24 

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] 

37 

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) 

47 

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 

54 

55 

56def method_decorator(decorator, name=""): 

57 """ 

58 Convert a function decorator into a method decorator 

59 """ 

60 

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 

82 

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__ = f"method_decorator({obj.__name__})" 

90 return _dec