Add better support for making a decorator out of a class.

This commit is contained in:
Graham Dumpleton
2014-05-03 20:00:29 +10:00
parent 0fee375735
commit 356a131d83
3 changed files with 231 additions and 17 deletions

View File

@@ -1,6 +1,42 @@
Release Notes
=============
Version 1.8.0
-------------
**Features Changed**
* Previously using @wrapt.decorator on a class type didn't really yield
anything which was practically useful. This is now changed and when
applied to a class an instance of the class will be automatically
created to be used as the decorator wrapper function. The requirement
for this is that the __call__() method be specified in the style as
would be done for the decorator wrapper function.
::
@wrapt.decorator
class mydecoratorclass(object):
def __init__(self, arg=None):
self.arg = arg
def __call__(self, wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
@mydecoratorclass
def function():
pass
If the resulting decorator class is to be used with no arguments, the
__init__() method of the class must have all default arguments. These
arguments can be optionally supplied though, by using keyword arguments
to the resulting decorator when applied to the function to be decorated.
::
@mydecoratorclass(arg=1)
def function():
pass
Version 1.7.0
-------------
@@ -172,7 +208,7 @@ Version 1.2.0
* Added in helper functions specifically designed to assist in performing
monkey patching of existing code.
**Features Changes**
**Features Changed**
* Collapsed functionality of _BoundMethodWrapper into _BoundFunctionWrapper
and renamed the latter to BoundFunctionWrapper. If deriving from the

View File

@@ -154,6 +154,17 @@ def decorator(wrapper=None, enabled=None, adapter=None):
# function will be called directly instead.
if wrapper is not None:
# Helper function for creating wrapper of the appropriate
# time when we need it down below.
def _build(wrapped, wrapper, enabled=None, adapter=None):
if adapter:
return AdapterWrapper(wrapped=wrapped, wrapper=wrapper,
enabled=enabled, adapter=adapter)
return FunctionWrapper(wrapped=wrapped, wrapper=wrapper,
enabled=enabled)
# The wrapper has been provided so return the final decorator.
# The decorator is itself one of our function wrappers so we
# can determine when it is applied to functions, instance methods
@@ -162,30 +173,179 @@ def decorator(wrapper=None, enabled=None, adapter=None):
# when it is finally called.
def _wrapper(wrapped, instance, args, kwargs):
# We first check for the case where the decorator was applied
# to a class type.
#
# @decorator
# class mydecoratorclass(object):
# def __init__(self, arg=None):
# self.arg = arg
# def __call__(self, wrapped, instance, args, kwargs):
# return wrapped(*args, **kwargs)
#
# @mydecoratorclass(arg=1)
# def function():
# pass
#
# In this case an instance of the class is to be used as the
# decorator wrapper function. If args was empty at this point,
# then it means that there were optional keyword arguments
# supplied to be used when creating an instance of the class
# to be used as the wrapper function.
if instance is None and isclass(wrapped) and not args:
# We still need to be passed the target function to be
# wrapped as yet, so we need to return a further function
# to be able to capture it.
def _capture(target_wrapped):
# Now have the target function to be wrapped and need
# to create an instance of the class which is to act
# as the decorator wrapper function. Before we do that,
# we need to first check that use of the decorator
# hadn't been disabled by a simple boolean. If it was,
# the target function to be wrapped is returned instead.
_enabled = enabled
if type(_enabled) is bool:
if not _enabled:
return target_wrapped
_enabled = None
# Now create an instance of the class which is to act
# as the decorator wrapper function. Any arguments had
# to be supplied as keyword only arguments so that is
# all we pass when creating it.
target_wrapper = wrapped(**kwargs)
# Finally build the wrapper itself and return it.
return _build(target_wrapped, target_wrapper,
_enabled, adapter)
return _capture
# We should always have the target function to be wrapped at
# this point as the first (and only) value in args.
target_wrapped = args[0]
# Need to now check that use of the decorator hadn't been
# disabled by a simple boolean. If it was, then target
# function to be wrapped is returned instead.
_enabled = enabled
if type(_enabled) is bool:
if not _enabled:
return target_wrapped
_enabled = None
# We now need to build the wrapper, but there are a couple of
# different cases we need to consider.
if instance is None:
target_wrapper = wrapper
elif isclass(instance):
target_wrapper = wrapper.__get__(None, instance)
if isclass(wrapped):
# In this case the decorator was applied to a class
# type but optional keyword arguments were not supplied
# for initialising an instance of the class to be used
# as the decorator wrapper function.
#
# @decorator
# class mydecoratorclass(object):
# def __init__(self, arg=None):
# self.arg = arg
# def __call__(self, wrapped, instance,
# args, kwargs):
# return wrapped(*args, **kwargs)
#
# @mydecoratorclass
# def function():
# pass
#
# We still need to create an instance of the class to
# be used as the decorator wrapper function, but no
# arguments are pass.
target_wrapper = wrapped()
else:
# In this case the decorator was applied to a normal
# function, or possibly a static method of a class.
#
# @decorator
# def mydecoratorfuntion(wrapped, instance,
# args, kwargs):
# return wrapped(*args, **kwargs)
#
# @mydecoratorfunction
# def function():
# pass
#
# That normal function becomes the decorator wrapper
# function.
target_wrapper = wrapper
else:
target_wrapper = wrapper.__get__(instance, type(instance))
if isclass(instance):
# In this case the decorator was applied to a class
# method.
#
# class myclass(object):
# @decorator
# @classmethod
# def decoratorclassmethod(cls, wrapped,
# instance, args, kwargs):
# return wrapped(*args, **kwargs)
#
# instance = myclass()
#
# @instance.decoratorclassmethod
# def function():
# pass
#
# This one is a bit strange because binding was actually
# performed on the wrapper created by our decorator
# factory. We need to apply that binding to the decorator
# wrapper function which which the decorator factory
# was applied to.
if adapter:
return AdapterWrapper(wrapped=target_wrapped,
wrapper=target_wrapper, enabled=_enabled,
adapter=adapter)
target_wrapper = wrapper.__get__(None, instance)
return FunctionWrapper(wrapped=target_wrapped,
wrapper=target_wrapper, enabled=_enabled)
else:
# In this case the decorator was applied to an instance
# method.
#
# class myclass(object):
# @decorator
# def decoratorclassmethod(self, wrapped,
# instance, args, kwargs):
# return wrapped(*args, **kwargs)
#
# instance = myclass()
#
# @instance.decoratorclassmethod
# def function():
# pass
#
# This one is a bit strange because binding was actually
# performed on the wrapper created by our decorator
# factory. We need to apply that binding to the decorator
# wrapper function which which the decorator factory
# was applied to.
return FunctionWrapper(wrapper, _wrapper)
target_wrapper = wrapper.__get__(instance, type(instance))
# Finally build the wrapper itself and return it.
return _build(target_wrapped, target_wrapper, _enabled, adapter)
# We first return our magic function wrapper here so we can
# determine in what context the decorator factory was used. In
# other words, it is itself a universal decorator.
return _build(wrapper, _wrapper)
else:
# The wrapper still has not been provided, so we are just

View File

@@ -81,12 +81,30 @@ class TestDecorator(unittest.TestCase):
_kwargs = { 'one': 1, 'two': 2 }
@wrapt.decorator
class CustomProxy(wrapt.ObjectProxy):
def __init__(self, wrapped, instance, args, kwargs):
result = wrapped(*args, **kwargs)
super(CustomProxy.__wrapped__, self).__init__(result)
class ClassDecorator(object):
def __call__(self, wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
@CustomProxy
@ClassDecorator
def _function(*args, **kwargs):
return args, kwargs
result = _function(*_args, **_kwargs)
self.assertEqual(result, (_args, _kwargs))
def test_class_type_as_decorator_args(self):
_args = (1, 2)
_kwargs = { 'one': 1, 'two': 2 }
@wrapt.decorator
class ClassDecorator(object):
def __init__(self, arg):
assert arg == 1
def __call__(self, wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
@ClassDecorator(arg=1)
def _function(*args, **kwargs):
return args, kwargs