Fixed #29253 -- Made method_decorator(list) copy attributes.

This commit is contained in:
Chris Jerdonek 2018-04-16 10:38:37 -07:00 committed by Tim Graham
parent a480ef89ad
commit fdc936c913
2 changed files with 82 additions and 67 deletions

View File

@ -12,6 +12,44 @@ class classonlymethod(classmethod):
return super().__get__(instance, cls) return super().__get__(instance, cls)
def _update_method_wrapper(_wrapper, decorator):
# _multi_decorate()'s bound_method isn't available in this scope. Cheat by
# using it on a dummy function.
@decorator
def dummy(*args, **kwargs):
pass
update_wrapper(_wrapper, dummy)
def _multi_decorate(decorators, method):
"""
Decorate `method` with one or more function decorators. `decorators` can be
a single decorator or an iterable of decorators.
"""
if hasattr(decorators, '__iter__'):
# Apply a list/tuple of decorators if 'decorators' is one. Decorator
# functions are applied so that the call order is the same as the
# order in which they appear in the iterable.
decorators = decorators[::-1]
else:
decorators = [decorators]
def _wrapper(self, *args, **kwargs):
# bound_method has the signature that 'decorator' expects i.e. no
# 'self' argument.
bound_method = method.__get__(self, type(self))
for dec in decorators:
bound_method = dec(bound_method)
return bound_method(*args, **kwargs)
# Copy any attributes that a decorator adds to the function it decorates.
for dec in decorators:
_update_method_wrapper(_wrapper, dec)
# Preserve any existing attributes of 'method', including the name.
update_wrapper(_wrapper, method)
return _wrapper
def method_decorator(decorator, name=''): def method_decorator(decorator, name=''):
""" """
Convert a function decorator into a method decorator Convert a function decorator into a method decorator
@ -21,70 +59,30 @@ def method_decorator(decorator, name=''):
# defined on. If 'obj' is a class, the 'name' is required to be the name # defined on. If 'obj' is a class, the 'name' is required to be the name
# of the method that will be decorated. # of the method that will be decorated.
def _dec(obj): def _dec(obj):
is_class = isinstance(obj, type) if not isinstance(obj, type):
if is_class: return _multi_decorate(decorator, obj)
if name and hasattr(obj, name): if not (name and hasattr(obj, name)):
func = getattr(obj, name) raise ValueError(
if not callable(func): "The keyword argument `name` must be the name of a method "
raise TypeError( "of the decorated class: %s. Got '%s' instead." % (obj, name)
"Cannot decorate '{0}' as it isn't a callable " )
"attribute of {1} ({2})".format(name, obj, func) method = getattr(obj, name)
) if not callable(method):
else: raise TypeError(
raise ValueError( "Cannot decorate '%s' as it isn't a callable attribute of "
"The keyword argument `name` must be the name of a method " "%s (%s)." % (name, obj, method)
"of the decorated class: {0}. Got '{1}' instead".format( )
obj, name, _wrapper = _multi_decorate(decorator, method)
) setattr(obj, name, _wrapper)
) return obj
else:
func = obj
def decorate(function):
"""
Apply a list/tuple of decorators if decorator is one. Decorator
functions are applied so that the call order is the same as the
order in which they appear in the iterable.
"""
if hasattr(decorator, '__iter__'):
for dec in decorator[::-1]:
function = dec(function)
return function
return decorator(function)
def _wrapper(self, *args, **kwargs):
@decorate
def bound_func(*args2, **kwargs2):
return func.__get__(self, type(self))(*args2, **kwargs2)
# bound_func has the signature that 'decorator' expects i.e. no
# 'self' argument, but it is a closure over self so it can call
# 'func' correctly.
return bound_func(*args, **kwargs)
# In case 'decorator' adds attributes to the function it decorates, we
# want to copy those. We don't have access to bound_func in this scope,
# but we can cheat by using it on a dummy function.
@decorate
def dummy(*args, **kwargs):
pass
update_wrapper(_wrapper, dummy)
# Need to preserve any existing attributes of 'func', including the name.
update_wrapper(_wrapper, func)
if is_class:
setattr(obj, name, _wrapper)
return obj
return _wrapper
# Don't worry about making _dec look similar to a list/tuple as it's rather # Don't worry about making _dec look similar to a list/tuple as it's rather
# meaningless. # meaningless.
if not hasattr(decorator, '__iter__'): if not hasattr(decorator, '__iter__'):
update_wrapper(_dec, decorator) update_wrapper(_dec, decorator)
# Change the name to aid debugging. # Change the name to aid debugging.
if hasattr(decorator, '__name__'): obj = decorator if hasattr(decorator, '__name__') else decorator.__class__
_dec.__name__ = 'method_decorator(%s)' % decorator.__name__ _dec.__name__ = 'method_decorator(%s)' % obj.__name__
else:
_dec.__name__ = 'method_decorator(%s)' % decorator.__class__.__name__
return _dec return _dec

View File

@ -168,7 +168,7 @@ def myattr_dec(func):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
return func(*args, **kwargs) return func(*args, **kwargs)
wrapper.myattr = True wrapper.myattr = True
return wraps(func)(wrapper) return wrapper
myattr_dec_m = method_decorator(myattr_dec) myattr_dec_m = method_decorator(myattr_dec)
@ -178,7 +178,7 @@ def myattr2_dec(func):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
return func(*args, **kwargs) return func(*args, **kwargs)
wrapper.myattr2 = True wrapper.myattr2 = True
return wraps(func)(wrapper) return wrapper
myattr2_dec_m = method_decorator(myattr2_dec) myattr2_dec_m = method_decorator(myattr2_dec)
@ -209,13 +209,23 @@ class MethodDecoratorTests(SimpleTestCase):
def test_preserve_attributes(self): def test_preserve_attributes(self):
# Sanity check myattr_dec and myattr2_dec # Sanity check myattr_dec and myattr2_dec
@myattr_dec
def func():
pass
self.assertIs(getattr(func, 'myattr', False), True)
@myattr2_dec
def func():
pass
self.assertIs(getattr(func, 'myattr2', False), True)
@myattr_dec @myattr_dec
@myattr2_dec @myattr2_dec
def func(): def func():
pass pass
self.assertIs(getattr(func, 'myattr', False), True) self.assertIs(getattr(func, 'myattr', False), True)
self.assertIs(getattr(func, 'myattr2', False), True) self.assertIs(getattr(func, 'myattr2', False), False)
# Decorate using method_decorator() on the method. # Decorate using method_decorator() on the method.
class TestPlain: class TestPlain:
@ -235,16 +245,23 @@ class MethodDecoratorTests(SimpleTestCase):
"A method" "A method"
pass pass
# Decorate using an iterable of decorators. # Decorate using an iterable of function decorators.
decorators = (myattr_dec_m, myattr2_dec_m) @method_decorator((myattr_dec, myattr2_dec), 'method')
class TestFunctionIterable:
@method_decorator(decorators, "method")
class TestIterable:
def method(self): def method(self):
"A method" "A method"
pass pass
tests = (TestPlain, TestMethodAndClass, TestIterable) # Decorate using an iterable of method decorators.
decorators = (myattr_dec_m, myattr2_dec_m)
@method_decorator(decorators, "method")
class TestMethodIterable:
def method(self):
"A method"
pass
tests = (TestPlain, TestMethodAndClass, TestFunctionIterable, TestMethodIterable)
for Test in tests: for Test in tests:
with self.subTest(Test=Test): with self.subTest(Test=Test):
self.assertIs(getattr(Test().method, 'myattr', False), True) self.assertIs(getattr(Test().method, 'myattr', False), True)