Skip to content

Commit 6861614

Browse files
committed
handle sync views
before this you could use sync views, but had to override the middleware to handle that, now it works out of the box
1 parent 3879749 commit 6861614

File tree

3 files changed

+157
-21
lines changed

3 files changed

+157
-21
lines changed

django_async_extensions/utils/decorators.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from asgiref.sync import async_to_sync, iscoroutinefunction, sync_to_async
44

55

6-
def decorator_from_middleware_with_args(middleware_class):
6+
def decorator_from_middleware_with_args(middleware_class, async_only=True):
77
"""
88
Like decorator_from_middleware, but return a function
99
that accepts the arguments to be passed to the middleware_class.
@@ -16,22 +16,31 @@ def decorator_from_middleware_with_args(middleware_class):
1616
def my_view(request):
1717
# ...
1818
"""
19-
return make_middleware_decorator(middleware_class)
19+
return make_middleware_decorator(middleware_class, async_only=async_only)
2020

2121

22-
def decorator_from_middleware(middleware_class):
22+
def decorator_from_middleware(middleware_class, async_only=True):
2323
"""
2424
Given a middleware class (not an instance), return a view decorator. This
2525
lets you use middleware functionality on a per-view basis. The middleware
2626
is created with no params passed.
2727
"""
28-
return make_middleware_decorator(middleware_class)()
28+
return make_middleware_decorator(middleware_class, async_only=async_only)()
2929

3030

31-
def make_middleware_decorator(middleware_class):
31+
def make_middleware_decorator(middleware_class, async_only=True):
3232
def _make_decorator(*m_args, **m_kwargs):
3333
def _decorator(view_func):
34-
middleware = middleware_class(view_func, *m_args, **m_kwargs)
34+
_view_func = view_func
35+
if all(
36+
[
37+
not iscoroutinefunction(view_func),
38+
not iscoroutinefunction(getattr(view_func, "__call__", None)),
39+
async_only,
40+
]
41+
):
42+
_view_func = sync_to_async(view_func)
43+
middleware = middleware_class(_view_func, *m_args, **m_kwargs)
3544

3645
async def _pre_process_request(request, *args, **kwargs):
3746
if hasattr(middleware, "process_request"):
@@ -87,7 +96,9 @@ async def callback(response):
8796
return await middleware.process_response(request, response)
8897
return response
8998

90-
if iscoroutinefunction(view_func):
99+
if iscoroutinefunction(view_func) or iscoroutinefunction(
100+
getattr(view_func, "__call__", view_func)
101+
):
91102

92103
async def _view_wrapper(request, *args, **kwargs):
93104
result = await _pre_process_request(request, *args, **kwargs)

docs/middleware/decorate_views.md

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ they work almost exactly like django's [decorator_from_middleware](https://docs.
66
and [decorator_from_middleware_with_args](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.decorators.decorator_from_middleware_with_args)
77
but it expects an async middleware as described in [AsyncMiddlewareMixin](base.md)
88

9-
**Important:** if you are using a middleware that inherits from [AsyncMiddlewareMixin](base.md) you can only decorate async views
10-
if you need to decorate a sync view change middleware's `__init__()` method to accept async `get_response` argument.
11-
129
with an async view
1310
```python
1411
from django.http.response import HttpResponse
@@ -30,12 +27,18 @@ async def my_view(request):
3027
```
3128

3229

33-
if you need to use a sync view design your middleware like this
30+
if your view is sync, it'll be wrapped in `sync_to_async` before getting passed down to middleware.
31+
32+
if you need, you can disable this by passing `async_only=False`.
33+
note that the middlewares presented in this package will error if you do that, so you have to override the `__init__()` and `__call__()` methods to handle that.
34+
3435
```python
35-
from django_async_extensions.middleware.base import AsyncMiddlewareMixin
36+
from django.http.response import HttpResponse
3637

3738
from asgiref.sync import iscoroutinefunction, markcoroutinefunction
3839

40+
from django_async_extensions.middleware.base import AsyncMiddlewareMixin
41+
from django_async_extensions.utils.decorators import decorator_from_middleware
3942

4043
class MyMiddleware(AsyncMiddlewareMixin):
4144
sync_capable = True
@@ -51,6 +54,24 @@ class MyMiddleware(AsyncMiddlewareMixin):
5154
if self.async_mode:
5255
# Mark the class as async-capable.
5356
markcoroutinefunction(self)
57+
58+
async def __call__(self, request):
59+
response = None
60+
if hasattr(self, "process_request"):
61+
response = await self.process_request(request)
62+
response = response or self.get_response(request) # here call the method in a sync manner, or handle it in another way
63+
if hasattr(self, "process_response"):
64+
response = await self.process_response(request, response)
65+
return response
5466

55-
super().__init__()
67+
async def process_request(self, request):
68+
return HttpResponse()
69+
70+
71+
deco = decorator_from_middleware(MyMiddleware, async_only=False)
72+
73+
74+
@deco
75+
def my_view(request):
76+
return HttpResponse()
5677
```

tests/test_async_utils/test_decorators.py

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@
1212

1313

1414
class ProcessViewMiddleware(AsyncMiddlewareMixin):
15-
def __init__(self, get_response):
16-
self.get_response = get_response
17-
1815
async def process_view(self, request, view_func, view_args, view_kwargs):
1916
pass
2017

@@ -49,9 +46,6 @@ async def __call__(self, request):
4946

5047

5148
class FullMiddleware(AsyncMiddlewareMixin):
52-
def __init__(self, get_response):
53-
self.get_response = get_response
54-
5549
async def process_request(self, request):
5650
request.process_request_reached = True
5751

@@ -72,6 +66,30 @@ async def process_response(self, request, response):
7266
full_dec = decorator_from_middleware(FullMiddleware)
7367

7468

69+
class MiddlewareSyncGetResponse(FullMiddleware):
70+
sync_capable = True
71+
72+
def __init__(self, get_response):
73+
self.get_response = get_response
74+
75+
async def __call__(self, request):
76+
response = None
77+
if hasattr(self, "process_request"):
78+
response = await self.process_request(request)
79+
response = response or self.get_response(request)
80+
if hasattr(self, "process_response"):
81+
response = await self.process_response(request, response)
82+
return response
83+
84+
85+
full_sync_dec = decorator_from_middleware(MiddlewareSyncGetResponse, async_only=False)
86+
87+
88+
@full_sync_dec
89+
def process_view_sync(request):
90+
return HttpResponse()
91+
92+
7593
class TestDecoratorFromMiddleware:
7694
"""
7795
Tests for view decorators created using
@@ -89,7 +107,7 @@ def test_process_view_middleware(self):
89107
async def test_process_view_middleware_async(self, async_rf):
90108
await async_process_view(async_rf.get("/"))
91109

92-
async def test_sync_process_view_raises_in_async_context(self):
110+
async def test_sync_process_view_in_async_context_errors(self):
93111
msg = (
94112
"You cannot use AsyncToSync in the same thread as an async event loop"
95113
" - just await the async function directly."
@@ -104,7 +122,7 @@ def test_callable_process_view_middleware(self):
104122
class_process_view(self.rf.get("/"))
105123

106124
async def test_callable_process_view_middleware_async(self, async_rf):
107-
await async_process_view(async_rf.get("/"))
125+
await async_class_process_view(async_rf.get("/"))
108126

109127
def test_full_dec_normal(self):
110128
"""
@@ -142,6 +160,38 @@ async def normal_view(request):
142160
assert getattr(request, "process_template_response_reached", False) is False
143161
assert getattr(request, "process_response_reached", False)
144162

163+
def test_full_sync_dec_normal(self):
164+
@full_sync_dec
165+
def normal_view(request):
166+
template = engines["django"].from_string("Hello world")
167+
return HttpResponse(template.render())
168+
169+
request = self.rf.get("/")
170+
normal_view(request)
171+
assert getattr(request, "process_request_reached", False)
172+
assert getattr(request, "process_view_reached", False)
173+
# process_template_response must not be called for HttpResponse
174+
assert getattr(request, "process_template_response_reached", False) is False
175+
assert getattr(request, "process_response_reached", False)
176+
177+
async def test_full_sync_dec_normal_async(self, async_rf):
178+
"""
179+
All methods of middleware are called for normal HttpResponses
180+
"""
181+
182+
@full_sync_dec
183+
async def normal_view(request):
184+
template = engines["django"].from_string("Hello world")
185+
return HttpResponse(template.render())
186+
187+
request = async_rf.get("/")
188+
await normal_view(request)
189+
assert getattr(request, "process_request_reached", False)
190+
assert getattr(request, "process_view_reached", False)
191+
# process_template_response must not be called for HttpResponse
192+
assert getattr(request, "process_template_response_reached", False) is False
193+
assert getattr(request, "process_response_reached", False)
194+
145195
def test_full_dec_templateresponse(self):
146196
"""
147197
All methods of middleware are called for TemplateResponses in
@@ -195,3 +245,57 @@ async def template_response_view(request):
195245
assert getattr(request, "process_response_reached", False)
196246
# process_response saw the rendered content
197247
assert request.process_response_content == b"Hello world"
248+
249+
def test_full_sync_dec_templateresponse(self):
250+
"""
251+
All methods of middleware are called for TemplateResponses in
252+
the right sequence.
253+
"""
254+
255+
@full_sync_dec
256+
def template_response_view(request):
257+
template = engines["django"].from_string("Hello world")
258+
return TemplateResponse(request, template)
259+
260+
request = self.rf.get("/")
261+
response = template_response_view(request)
262+
assert getattr(request, "process_request_reached", False)
263+
assert getattr(request, "process_view_reached", False)
264+
assert getattr(request, "process_template_response_reached", False)
265+
# response must not be rendered yet.
266+
assert response._is_rendered is False
267+
# process_response must not be called until after response is rendered,
268+
# otherwise some decorators like csrf_protect and gzip_page will not
269+
# work correctly. See #16004
270+
assert getattr(request, "process_response_reached", False) is False
271+
response.render()
272+
assert getattr(request, "process_response_reached", False)
273+
# process_response saw the rendered content
274+
assert request.process_response_content == b"Hello world"
275+
276+
async def test_full_sync_dec_templateresponse_async(self, async_rf):
277+
"""
278+
All methods of middleware are called for TemplateResponses in
279+
the right sequence.
280+
"""
281+
282+
@full_sync_dec
283+
async def template_response_view(request):
284+
template = engines["django"].from_string("Hello world")
285+
return TemplateResponse(request, template)
286+
287+
request = async_rf.get("/")
288+
response = await template_response_view(request)
289+
assert getattr(request, "process_request_reached", False)
290+
assert getattr(request, "process_view_reached", False)
291+
assert getattr(request, "process_template_response_reached", False)
292+
# response must not be rendered yet.
293+
assert response._is_rendered is False
294+
# process_response must not be called until after response is rendered,
295+
# otherwise some decorators like csrf_protect and gzip_page will not
296+
# work correctly. See #16004
297+
assert getattr(request, "process_response_reached", False) is False
298+
await sync_to_async(response.render)()
299+
assert getattr(request, "process_response_reached", False)
300+
# process_response saw the rendered content
301+
assert request.process_response_content == b"Hello world"

0 commit comments

Comments
 (0)