diff --git a/django/views/decorators/common.py b/django/views/decorators/common.py index 71ee232ae4d..d09b0bfee47 100644 --- a/django/views/decorators/common.py +++ b/django/views/decorators/common.py @@ -1,5 +1,7 @@ from functools import wraps +from asgiref.sync import iscoroutinefunction + def no_append_slash(view_func): """ @@ -9,9 +11,17 @@ def no_append_slash(view_func): # view_func.should_append_slash = False would also work, but decorators are # nicer if they don't have side effects, so return a new function. - @wraps(view_func) - def wrapper_view(*args, **kwargs): - return view_func(*args, **kwargs) - wrapper_view.should_append_slash = False - return wrapper_view + if iscoroutinefunction(view_func): + + async def _view_wrapper(request, *args, **kwargs): + return await view_func(request, *args, **kwargs) + + else: + + def _view_wrapper(request, *args, **kwargs): + return view_func(request, *args, **kwargs) + + _view_wrapper.should_append_slash = False + + return wraps(view_func)(_view_wrapper) diff --git a/docs/releases/5.0.txt b/docs/releases/5.0.txt index e6526dd798f..6c16945f1fa 100644 --- a/docs/releases/5.0.txt +++ b/docs/releases/5.0.txt @@ -237,6 +237,7 @@ Decorators * :func:`~django.views.decorators.cache.cache_control` * :func:`~django.views.decorators.cache.never_cache` + * :func:`~django.views.decorators.common.no_append_slash` * ``xframe_options_deny()`` * ``xframe_options_sameorigin()`` * ``xframe_options_exempt()`` diff --git a/docs/topics/async.txt b/docs/topics/async.txt index 444c88a1c69..73f4bf2a356 100644 --- a/docs/topics/async.txt +++ b/docs/topics/async.txt @@ -83,6 +83,7 @@ view functions: * :func:`~django.views.decorators.cache.cache_control` * :func:`~django.views.decorators.cache.never_cache` +* :func:`~django.views.decorators.common.no_append_slash` * ``xframe_options_deny()`` * ``xframe_options_sameorigin()`` * ``xframe_options_exempt()`` diff --git a/docs/topics/http/decorators.txt b/docs/topics/http/decorators.txt index 4bcae64e27d..49219f3d5a6 100644 --- a/docs/topics/http/decorators.txt +++ b/docs/topics/http/decorators.txt @@ -147,3 +147,7 @@ customization of :class:`~django.middleware.common.CommonMiddleware` behavior. This decorator allows individual views to be excluded from :setting:`APPEND_SLASH` URL normalization. + + .. versionchanged:: 5.0 + + Support for wrapping asynchronous view functions was added. diff --git a/tests/decorators/test_common.py b/tests/decorators/test_common.py new file mode 100644 index 00000000000..a2661ef30a1 --- /dev/null +++ b/tests/decorators/test_common.py @@ -0,0 +1,37 @@ +from asgiref.sync import iscoroutinefunction + +from django.http import HttpRequest, HttpResponse +from django.test import SimpleTestCase +from django.views.decorators.common import no_append_slash + + +class NoAppendSlashTests(SimpleTestCase): + def test_wrapped_sync_function_is_not_coroutine_function(self): + def sync_view(request): + return HttpResponse() + + wrapped_view = no_append_slash(sync_view) + self.assertIs(iscoroutinefunction(wrapped_view), False) + + def test_wrapped_async_function_is_coroutine_function(self): + async def async_view(request): + return HttpResponse() + + wrapped_view = no_append_slash(async_view) + self.assertIs(iscoroutinefunction(wrapped_view), True) + + def test_no_append_slash_decorator(self): + @no_append_slash + def sync_view(request): + return HttpResponse() + + self.assertIs(sync_view.should_append_slash, False) + self.assertIsInstance(sync_view(HttpRequest()), HttpResponse) + + async def test_no_append_slash_decorator_async_view(self): + @no_append_slash + async def async_view(request): + return HttpResponse() + + self.assertIs(async_view.should_append_slash, False) + self.assertIsInstance(await async_view(HttpRequest()), HttpResponse)