1"Functions that help with dynamically creating decorators for views."
2
3from functools import partial, update_wrapper, wraps
4
5from asgiref.sync import iscoroutinefunction, markcoroutinefunction
6
7
8class classonlymethod(classmethod):
9 def __get__(self, instance, cls=None):
10 if instance is not None:
11 raise AttributeError(
12 "This method is available only on the class, not on instances."
13 )
14 return super().__get__(instance, cls)
15
16
17def _update_method_wrapper(_wrapper, decorator):
18 # _multi_decorate()'s bound_method isn't available in this scope. Cheat by
19 # using it on a dummy function.
20 @decorator
21 def dummy(*args, **kwargs):
22 pass
23
24 update_wrapper(_wrapper, dummy)
25
26
27def _multi_decorate(decorators, method):
28 """
29 Decorate `method` with one or more function decorators. `decorators` can be
30 a single decorator or an iterable of decorators.
31 """
32 if hasattr(decorators, "__iter__"):
33 # Apply a list/tuple of decorators if 'decorators' is one. Decorator
34 # functions are applied so that the call order is the same as the
35 # order in which they appear in the iterable.
36 decorators = decorators[::-1]
37 else:
38 decorators = [decorators]
39
40 def _wrapper(self, *args, **kwargs):
41 # bound_method has the signature that 'decorator' expects i.e. no
42 # 'self' argument, but it's a closure over self so it can call
43 # 'func'. Also, wrap method.__get__() in a function because new
44 # attributes can't be set on bound method objects, only on functions.
45 bound_method = wraps(method)(partial(method.__get__(self, type(self))))
46 for dec in decorators:
47 bound_method = dec(bound_method)
48 return bound_method(*args, **kwargs)
49
50 # Copy any attributes that a decorator adds to the function it decorates.
51 for dec in decorators:
52 _update_method_wrapper(_wrapper, dec)
53 # Preserve any existing attributes of 'method', including the name.
54 update_wrapper(_wrapper, method)
55
56 if iscoroutinefunction(method):
57 markcoroutinefunction(_wrapper)
58
59 return _wrapper
60
61
62def method_decorator(decorator, name=""):
63 """
64 Convert a function decorator into a method decorator
65 """
66
67 # 'obj' can be a class or a function. If 'obj' is a function at the time it
68 # is passed to _dec, it will eventually be a method of the class it is
69 # defined on. If 'obj' is a class, the 'name' is required to be the name
70 # of the method that will be decorated.
71 def _dec(obj):
72 if not isinstance(obj, type):
73 return _multi_decorate(decorator, obj)
74 if not (name and hasattr(obj, name)):
75 raise ValueError(
76 "The keyword argument `name` must be the name of a method "
77 "of the decorated class: %s. Got '%s' instead." % (obj, name)
78 )
79 method = getattr(obj, name)
80 if not callable(method):
81 raise TypeError(
82 "Cannot decorate '%s' as it isn't a callable attribute of "
83 "%s (%s)." % (name, obj, method)
84 )
85 _wrapper = _multi_decorate(decorator, method)
86 setattr(obj, name, _wrapper)
87 return obj
88
89 # Don't worry about making _dec look similar to a list/tuple as it's rather
90 # meaningless.
91 if not hasattr(decorator, "__iter__"):
92 update_wrapper(_dec, decorator)
93 # Change the name to aid debugging.
94 obj = decorator if hasattr(decorator, "__name__") else decorator.__class__
95 _dec.__name__ = "method_decorator(%s)" % obj.__name__
96 return _dec
97
98
99def decorator_from_middleware_with_args(middleware_class):
100 """
101 Like decorator_from_middleware, but return a function
102 that accepts the arguments to be passed to the middleware_class.
103 Use like::
104
105 cache_page = decorator_from_middleware_with_args(CacheMiddleware)
106 # ...
107
108 @cache_page(3600)
109 def my_view(request):
110 # ...
111 """
112 return make_middleware_decorator(middleware_class)
113
114
115def decorator_from_middleware(middleware_class):
116 """
117 Given a middleware class (not an instance), return a view decorator. This
118 lets you use middleware functionality on a per-view basis. The middleware
119 is created with no params passed.
120 """
121 return make_middleware_decorator(middleware_class)()
122
123
124def make_middleware_decorator(middleware_class):
125 def _make_decorator(*m_args, **m_kwargs):
126 def _decorator(view_func):
127 middleware = middleware_class(view_func, *m_args, **m_kwargs)
128
129 def _pre_process_request(request, *args, **kwargs):
130 if hasattr(middleware, "process_request"):
131 result = middleware.process_request(request)
132 if result is not None:
133 return result
134 if hasattr(middleware, "process_view"):
135 result = middleware.process_view(request, view_func, args, kwargs)
136 if result is not None:
137 return result
138 return None
139
140 def _process_exception(request, exception):
141 if hasattr(middleware, "process_exception"):
142 result = middleware.process_exception(request, exception)
143 if result is not None:
144 return result
145 raise
146
147 def _post_process_request(request, response):
148 if hasattr(response, "render") and callable(response.render):
149 if hasattr(middleware, "process_template_response"):
150 response = middleware.process_template_response(
151 request, response
152 )
153 # Defer running of process_response until after the template
154 # has been rendered:
155 if hasattr(middleware, "process_response"):
156
157 def callback(response):
158 return middleware.process_response(request, response)
159
160 response.add_post_render_callback(callback)
161 else:
162 if hasattr(middleware, "process_response"):
163 return middleware.process_response(request, response)
164 return response
165
166 if iscoroutinefunction(view_func):
167
168 async def _view_wrapper(request, *args, **kwargs):
169 result = _pre_process_request(request, *args, **kwargs)
170 if result is not None:
171 return result
172
173 try:
174 response = await view_func(request, *args, **kwargs)
175 except Exception as e:
176 result = _process_exception(request, e)
177 if result is not None:
178 return result
179
180 return _post_process_request(request, response)
181
182 else:
183
184 def _view_wrapper(request, *args, **kwargs):
185 result = _pre_process_request(request, *args, **kwargs)
186 if result is not None:
187 return result
188
189 try:
190 response = view_func(request, *args, **kwargs)
191 except Exception as e:
192 result = _process_exception(request, e)
193 if result is not None:
194 return result
195
196 return _post_process_request(request, response)
197
198 return wraps(view_func)(_view_wrapper)
199
200 return _decorator
201
202 return _make_decorator
203
204
205def sync_and_async_middleware(func):
206 """
207 Mark a middleware factory as returning a hybrid middleware supporting both
208 types of request.
209 """
210 func.sync_capable = True
211 func.async_capable = True
212 return func
213
214
215def sync_only_middleware(func):
216 """
217 Mark a middleware factory as returning a sync middleware.
218 This is the default.
219 """
220 func.sync_capable = True
221 func.async_capable = False
222 return func
223
224
225def async_only_middleware(func):
226 """Mark a middleware factory as returning an async middleware."""
227 func.sync_capable = False
228 func.async_capable = True
229 return func