mirror of
https://github.com/django/django.git
synced 2025-06-06 03:59:12 +00:00
Fixed #35083 -- Updated method_decorator to handle async methods.
Co-authored-by: Natalia <124304+nessita@users.noreply.github.com> Co-authored-by: Carlton Gibson <carlton.gibson@noumenal.es>
This commit is contained in:
parent
2c1f27d0d0
commit
884ce37479
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from functools import partial, update_wrapper, wraps
|
from functools import partial, update_wrapper, wraps
|
||||||
|
|
||||||
from asgiref.sync import iscoroutinefunction
|
from asgiref.sync import iscoroutinefunction, markcoroutinefunction
|
||||||
|
|
||||||
|
|
||||||
class classonlymethod(classmethod):
|
class classonlymethod(classmethod):
|
||||||
@ -52,6 +52,10 @@ def _multi_decorate(decorators, method):
|
|||||||
_update_method_wrapper(_wrapper, dec)
|
_update_method_wrapper(_wrapper, dec)
|
||||||
# Preserve any existing attributes of 'method', including the name.
|
# Preserve any existing attributes of 'method', including the name.
|
||||||
update_wrapper(_wrapper, method)
|
update_wrapper(_wrapper, method)
|
||||||
|
|
||||||
|
if iscoroutinefunction(method):
|
||||||
|
markcoroutinefunction(_wrapper)
|
||||||
|
|
||||||
return _wrapper
|
return _wrapper
|
||||||
|
|
||||||
|
|
||||||
|
@ -128,7 +128,8 @@ Database backends
|
|||||||
Decorators
|
Decorators
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
* ...
|
* :func:`~django.utils.decorators.method_decorator` now supports wrapping
|
||||||
|
asynchronous view methods.
|
||||||
|
|
||||||
Email
|
Email
|
||||||
~~~~~
|
~~~~~
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
import asyncio
|
||||||
from functools import update_wrapper, wraps
|
from functools import update_wrapper, wraps
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from asgiref.sync import iscoroutinefunction
|
||||||
|
|
||||||
from django.contrib.admin.views.decorators import staff_member_required
|
from django.contrib.admin.views.decorators import staff_member_required
|
||||||
from django.contrib.auth.decorators import (
|
from django.contrib.auth.decorators import (
|
||||||
login_required,
|
login_required,
|
||||||
@ -434,3 +437,262 @@ class MethodDecoratorTests(SimpleTestCase):
|
|||||||
Test().method()
|
Test().method()
|
||||||
self.assertEqual(func_name, "method")
|
self.assertEqual(func_name, "method")
|
||||||
self.assertIsNotNone(func_module)
|
self.assertIsNotNone(func_module)
|
||||||
|
|
||||||
|
|
||||||
|
def async_simple_dec(func):
|
||||||
|
@wraps(func)
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
result = await func(*args, **kwargs)
|
||||||
|
return f"returned: {result}"
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
async_simple_dec_m = method_decorator(async_simple_dec)
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncMethodDecoratorTests(SimpleTestCase):
|
||||||
|
"""
|
||||||
|
Tests for async method_decorator
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def test_preserve_signature(self):
|
||||||
|
class Test:
|
||||||
|
@async_simple_dec_m
|
||||||
|
async def say(self, msg):
|
||||||
|
return f"Saying {msg}"
|
||||||
|
|
||||||
|
self.assertEqual(await Test().say("hello"), "returned: Saying hello")
|
||||||
|
|
||||||
|
def test_preserve_attributes(self):
|
||||||
|
async def func(*args, **kwargs):
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
return args, kwargs
|
||||||
|
|
||||||
|
def myattr_dec(func):
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
|
||||||
|
wrapper.myattr = True
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
def myattr2_dec(func):
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
|
||||||
|
wrapper.myattr2 = True
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
# Sanity check myattr_dec and myattr2_dec
|
||||||
|
func = myattr_dec(func)
|
||||||
|
|
||||||
|
self.assertIs(getattr(func, "myattr", False), True)
|
||||||
|
|
||||||
|
func = myattr2_dec(func)
|
||||||
|
self.assertIs(getattr(func, "myattr2", False), True)
|
||||||
|
|
||||||
|
func = myattr_dec(myattr2_dec(func))
|
||||||
|
self.assertIs(getattr(func, "myattr", False), True)
|
||||||
|
self.assertIs(getattr(func, "myattr2", False), False)
|
||||||
|
|
||||||
|
myattr_dec_m = method_decorator(myattr_dec)
|
||||||
|
myattr2_dec_m = method_decorator(myattr2_dec)
|
||||||
|
|
||||||
|
# Decorate using method_decorator() on the async method.
|
||||||
|
class TestPlain:
|
||||||
|
@myattr_dec_m
|
||||||
|
@myattr2_dec_m
|
||||||
|
async def method(self):
|
||||||
|
"A method"
|
||||||
|
|
||||||
|
# Decorate using method_decorator() on both the class and the method.
|
||||||
|
# The decorators applied to the methods are applied before the ones
|
||||||
|
# applied to the class.
|
||||||
|
@method_decorator(myattr_dec_m, "method")
|
||||||
|
class TestMethodAndClass:
|
||||||
|
@method_decorator(myattr2_dec_m)
|
||||||
|
async def method(self):
|
||||||
|
"A method"
|
||||||
|
|
||||||
|
# Decorate using an iterable of function decorators.
|
||||||
|
@method_decorator((myattr_dec, myattr2_dec), "method")
|
||||||
|
class TestFunctionIterable:
|
||||||
|
async def method(self):
|
||||||
|
"A method"
|
||||||
|
|
||||||
|
# Decorate using an iterable of method decorators.
|
||||||
|
@method_decorator((myattr_dec_m, myattr2_dec_m), "method")
|
||||||
|
class TestMethodIterable:
|
||||||
|
async def method(self):
|
||||||
|
"A method"
|
||||||
|
|
||||||
|
tests = (
|
||||||
|
TestPlain,
|
||||||
|
TestMethodAndClass,
|
||||||
|
TestFunctionIterable,
|
||||||
|
TestMethodIterable,
|
||||||
|
)
|
||||||
|
for Test in tests:
|
||||||
|
with self.subTest(Test=Test):
|
||||||
|
self.assertIs(getattr(Test().method, "myattr", False), True)
|
||||||
|
self.assertIs(getattr(Test().method, "myattr2", False), True)
|
||||||
|
self.assertIs(getattr(Test.method, "myattr", False), True)
|
||||||
|
self.assertIs(getattr(Test.method, "myattr2", False), True)
|
||||||
|
self.assertEqual(Test.method.__doc__, "A method")
|
||||||
|
self.assertEqual(Test.method.__name__, "method")
|
||||||
|
|
||||||
|
async def test_new_attribute(self):
|
||||||
|
"""A decorator that sets a new attribute on the method."""
|
||||||
|
|
||||||
|
def decorate(func):
|
||||||
|
func.x = 1
|
||||||
|
return func
|
||||||
|
|
||||||
|
class MyClass:
|
||||||
|
@method_decorator(decorate)
|
||||||
|
async def method(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
obj = MyClass()
|
||||||
|
self.assertEqual(obj.method.x, 1)
|
||||||
|
self.assertIs(await obj.method(), True)
|
||||||
|
|
||||||
|
def test_bad_iterable(self):
|
||||||
|
decorators = {async_simple_dec}
|
||||||
|
msg = "'set' object is not subscriptable"
|
||||||
|
with self.assertRaisesMessage(TypeError, msg):
|
||||||
|
|
||||||
|
@method_decorator(decorators, "method")
|
||||||
|
class TestIterable:
|
||||||
|
async def method(self):
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
|
async def test_argumented(self):
|
||||||
|
|
||||||
|
class ClsDecAsync:
|
||||||
|
def __init__(self, myattr):
|
||||||
|
self.myattr = myattr
|
||||||
|
|
||||||
|
def __call__(self, f):
|
||||||
|
async def wrapper():
|
||||||
|
result = await f()
|
||||||
|
return f"{result} appending {self.myattr}"
|
||||||
|
|
||||||
|
return update_wrapper(wrapper, f)
|
||||||
|
|
||||||
|
class Test:
|
||||||
|
@method_decorator(ClsDecAsync(False))
|
||||||
|
async def method(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.assertEqual(await Test().method(), "True appending False")
|
||||||
|
|
||||||
|
async def test_descriptors(self):
|
||||||
|
class bound_wrapper:
|
||||||
|
def __init__(self, wrapped):
|
||||||
|
self.wrapped = wrapped
|
||||||
|
self.__name__ = wrapped.__name__
|
||||||
|
|
||||||
|
async def __call__(self, *args, **kwargs):
|
||||||
|
return await self.wrapped(*args, **kwargs)
|
||||||
|
|
||||||
|
def __get__(self, instance, cls=None):
|
||||||
|
return self
|
||||||
|
|
||||||
|
class descriptor_wrapper:
|
||||||
|
def __init__(self, wrapped):
|
||||||
|
self.wrapped = wrapped
|
||||||
|
self.__name__ = wrapped.__name__
|
||||||
|
|
||||||
|
def __get__(self, instance, cls=None):
|
||||||
|
return bound_wrapper(self.wrapped.__get__(instance, cls))
|
||||||
|
|
||||||
|
class Test:
|
||||||
|
@async_simple_dec_m
|
||||||
|
@descriptor_wrapper
|
||||||
|
async def method(self, arg):
|
||||||
|
return arg
|
||||||
|
|
||||||
|
self.assertEqual(await Test().method(1), "returned: 1")
|
||||||
|
|
||||||
|
async def test_class_decoration(self):
|
||||||
|
"""
|
||||||
|
@method_decorator can be used to decorate a class and its methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@method_decorator(async_simple_dec, name="method")
|
||||||
|
class Test:
|
||||||
|
async def method(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def not_method(self):
|
||||||
|
return "a string"
|
||||||
|
|
||||||
|
self.assertEqual(await Test().method(), "returned: False")
|
||||||
|
self.assertEqual(await Test().not_method(), "a string")
|
||||||
|
|
||||||
|
async def test_tuple_of_decorators(self):
|
||||||
|
"""
|
||||||
|
@method_decorator can accept a tuple of decorators.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def add_question_mark(func):
|
||||||
|
async def _wrapper(*args, **kwargs):
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
return await func(*args, **kwargs) + "?"
|
||||||
|
|
||||||
|
return _wrapper
|
||||||
|
|
||||||
|
def add_exclamation_mark(func):
|
||||||
|
async def _wrapper(*args, **kwargs):
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
return await func(*args, **kwargs) + "!"
|
||||||
|
|
||||||
|
return _wrapper
|
||||||
|
|
||||||
|
decorators = (add_exclamation_mark, add_question_mark)
|
||||||
|
|
||||||
|
@method_decorator(decorators, name="method")
|
||||||
|
class TestFirst:
|
||||||
|
async def method(self):
|
||||||
|
return "hello world"
|
||||||
|
|
||||||
|
class TestSecond:
|
||||||
|
@method_decorator(decorators)
|
||||||
|
async def method(self):
|
||||||
|
return "world hello"
|
||||||
|
|
||||||
|
self.assertEqual(await TestFirst().method(), "hello world?!")
|
||||||
|
self.assertEqual(await TestSecond().method(), "world hello?!")
|
||||||
|
|
||||||
|
async def test_wrapper_assignments(self):
|
||||||
|
"""@method_decorator preserves wrapper assignments."""
|
||||||
|
func_data = {}
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
async def inner(*args, **kwargs):
|
||||||
|
func_data["func_name"] = getattr(func, "__name__", None)
|
||||||
|
func_data["func_module"] = getattr(func, "__module__", None)
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
class Test:
|
||||||
|
@method_decorator(decorator)
|
||||||
|
async def method(self):
|
||||||
|
return "tests"
|
||||||
|
|
||||||
|
await Test().method()
|
||||||
|
expected = {"func_name": "method", "func_module": "decorators.tests"}
|
||||||
|
self.assertEqual(func_data, expected)
|
||||||
|
|
||||||
|
async def test_markcoroutinefunction_applied(self):
|
||||||
|
class Test:
|
||||||
|
@async_simple_dec_m
|
||||||
|
async def method(self):
|
||||||
|
return "tests"
|
||||||
|
|
||||||
|
method = Test().method
|
||||||
|
self.assertIs(iscoroutinefunction(method), True)
|
||||||
|
self.assertEqual(await method(), "returned: tests")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user