Improvements to ability to bind an alternate function prototype to the wrapper for inspect.getargspec().

This commit is contained in:
Graham Dumpleton
2013-08-28 20:50:03 +10:00
parent 5f942f9318
commit 1ab62f7e75
5 changed files with 402 additions and 630 deletions

View File

@@ -2,4 +2,4 @@ __version_info__ = ('0', '9', '0')
__version__ = '.'.join(__version_info__)
from .wrappers import ObjectProxy, FunctionWrapper
from .decorators import decorator, adapter
from .decorators import decorator

File diff suppressed because it is too large Load Diff

View File

@@ -133,21 +133,6 @@ def _validate_parameters(func, spec, *positional, **named):
_missing_arguments(f_name, kwonlyargs, False, arg2value)
return arg2value
# Copy name attributes from a wrapped function onto an wrapper. This is
# only used in mapping the name from the final wrapped function to which
# an adapter is applied onto the adapter itself. All other details come
# from the adapter function via the function wrapper so we don't update
# __dict__ or __wrapped__.
def _update_adapter(wrapper, target):
for attr in ('__module__', '__name__', '__qualname__'):
try:
value = getattr(target, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
# Decorators for creating other decorators. These decorators and the
# wrappers which they use are designed to properly preserve any name
# attributes, function signatures etc, in addition to the wrappers
@@ -157,16 +142,19 @@ def _update_adapter(wrapper, target):
WRAPPER_ARGLIST = ('wrapped', 'instance', 'args', 'kwargs')
def decorator(wrapper=None, target=None, validate=True):
def decorator(wrapper=None, adapter=None, validate=True):
# The decorator takes some optional keyword parameters to change its
# behaviour. The decorator works out whether parameters have been
# passed based on whether the first positional argument, which is
# the wrapper which implements the user decorator, has been
# supplied. The 'target' argument is used to optionally denote a
# function which is wrapped by an adapter decorator. In that case
# the name attributes are copied from the target function rather
# than those of the adapter function. The 'validate' argument, which
# defaults to True, indicates whether validation of decorator
# supplied. The 'adapter' argument is used to optionally denote a
# separate function which is notionally used by an adapter
# decorator. In that case the function '__code__' and '__defaults__'
# attributes are used from the adapter function rather than those of
# the wrapped function. This allows for the argument specification
# from inspect.getargspec() to be overridden with a prototype for a
# different function than what was wrapped. The 'validate' argument,
# which defaults to True, indicates whether validation of decorator
# parameters should be validated at the time the decorator is
# applied, or whether a failure is allow to occur only when the
# wrapped function is later called and the user wrapper function
@@ -210,10 +198,9 @@ def decorator(wrapper=None, target=None, validate=True):
def _wrapper(func):
result = FunctionWrapper(wrapped=func,
wrapper=wrapper, args=decorator_args,
kwargs=decorator_kwargs)
if target:
_update_adapter(result, target)
wrapper=wrapper, wrapper_args=decorator_args,
wrapper_kwargs=decorator_kwargs,
adapter=adapter)
return result
return _wrapper
@@ -228,9 +215,8 @@ def decorator(wrapper=None, target=None, validate=True):
@wraps(wrapper)
def _wrapper(func):
result = FunctionWrapper(wrapped=func, wrapper=wrapper)
if target:
_update_adapter(result, target)
result = FunctionWrapper(wrapped=func, wrapper=wrapper,
adapter=adapter)
return result
return _wrapper
@@ -241,10 +227,4 @@ def decorator(wrapper=None, target=None, validate=True):
# a partial using the collected default parameters and the
# adapter function if one is being used.
return partial(decorator, target=target, validate=validate)
def adapter(target):
@decorator(target=target)
def wrapper(wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
return wrapper
return partial(decorator, adapter=adapter, validate=validate)

View File

@@ -51,7 +51,7 @@ class _ObjectProxyMetaType(type):
class ObjectProxy(six.with_metaclass(_ObjectProxyMetaType)):
def __init__(self, wrapped):
self._self_wrapped = wrapped
object.__setattr__(self, '_self_wrapped', wrapped)
# Python 3.2+ has the __qualname__ attribute, but it does not
# allow it to be overridden using a property and it must instead
@@ -360,14 +360,67 @@ class ObjectProxy(six.with_metaclass(_ObjectProxyMetaType)):
def __call__(self, *args, **kwargs):
return self._self_wrapped(*args, **kwargs)
class _BoundFunctionWrapper(ObjectProxy):
class _FunctionWrapperBase(ObjectProxy):
def __init__(self, wrapped, instance, wrapper, args=(), kwargs={}):
super(_BoundFunctionWrapper, self).__init__(wrapped)
self._self_instance = instance
self._self_wrapper = wrapper
self._self_wrapper_args = args
self._self_wrapper_kwargs = kwargs
def __init__(self, wrapped, instance, wrapper, wrapper_args=(),
wrapper_kwargs={}, adapter=None, bound_type=None):
super(_FunctionWrapperBase, self).__init__(wrapped)
object.__setattr__(self, '_self_instance', instance)
object.__setattr__(self, '_self_wrapper', wrapper)
object.__setattr__(self, '_self_wrapper_args', wrapper_args)
object.__setattr__(self, '_self_wrapper_kwargs', wrapper_kwargs)
object.__setattr__(self, '_self_adapter', adapter)
object.__setattr__(self, '_self_bound_type', bound_type)
def __get__(self, instance, owner):
# If we have already been bound to an instance of something, we
# do not do it again and return ourselves again. This appears to
# mirror what Python itself does.
if self._self_bound_type is None:
return self
descriptor = self._self_wrapped.__get__(instance, owner)
return self._self_bound_type(descriptor, instance, self._self_wrapper,
self._self_wrapper_args, self._self_wrapper_kwargs,
self._self_adapter)
def __call__(self, *args, **kwargs):
# This is generally invoked when the wrapped function is being
# called as a normal function and is not bound to a class as an
# instance method. This is also invoked in the case where the
# wrapped function was a method, but this wrapper was in turn
# wrapped using the staticmethod decorator.
return self._self_wrapper(self._self_wrapped, self._self_instance,
args, kwargs, *self._self_wrapper_args,
**self._self_wrapper_kwargs)
# If an adapter function was provided we want to return certain
# attributes of the function from the adapter rather than the
# wrapped function so things like inspect.getargspec() will reflect
# the prototype of the adapter and not the wrapped function.
@property
def __code__(self):
if self._self_adapter:
return self._self_adapter.__code__
return self._self_wrapped.__code__
@property
def __defaults__(self):
if self._self_adapter:
return self._self_adapter.__defaults__
return self._self_wrapped.__defaults__
if six.PY2:
func_code = __code__
func_defaults = __defaults__
class _BoundFunctionWrapper(_FunctionWrapperBase):
def __call__(self, *args, **kwargs):
# As in this case we would be dealing with a classmethod or
@@ -389,14 +442,7 @@ class _BoundFunctionWrapper(ObjectProxy):
args, kwargs, *self._self_wrapper_args,
**self._self_wrapper_kwargs)
class _BoundMethodWrapper(ObjectProxy):
def __init__(self, wrapped, instance, wrapper, args=(), kwargs={}):
super(_BoundMethodWrapper, self).__init__(wrapped)
self._self_instance = instance
self._self_wrapper = wrapper
self._self_wrapper_args = args
self._self_wrapper_kwargs = kwargs
class _BoundMethodWrapper(_FunctionWrapperBase):
def __call__(self, *args, **kwargs):
if self._self_instance is None:
@@ -416,13 +462,10 @@ class _BoundMethodWrapper(ObjectProxy):
args, kwargs, *self._self_wrapper_args,
**self._self_wrapper_kwargs)
class FunctionWrapper(ObjectProxy):
class FunctionWrapper(_FunctionWrapperBase):
def __init__(self, wrapped, wrapper, args=(), kwargs={}):
super(FunctionWrapper, self).__init__(wrapped)
self._self_wrapper = wrapper
self._self_wrapper_args = args
self._self_wrapper_kwargs = kwargs
def __init__(self, wrapped, wrapper, wrapper_args=(), wrapper_kwargs={},
adapter=None):
# We need to do special fixups on the args in the case of an
# instancemethod where called via the class and the instance is
@@ -434,10 +477,7 @@ class FunctionWrapper(ObjectProxy):
# Note that there isn't strictly a fool proof method of knowing
# which is occuring because if a decorator using this code wraps
# other decorators and they are poorly implemented they can
# throw away important information needed to determine it. Some
# ways that it could be determined in Python 2 are also not
# possible in Python 3 due to the concept of unbound methods
# being done away with.
# throw away important information needed to determine it.
#
# Anyway, the best we can do is look at the original type of the
# object which was wrapped prior to any binding being done and
@@ -453,26 +493,13 @@ class FunctionWrapper(ObjectProxy):
# that those other decorators be fixed. It is also only an issue
# if a decorator wants to actually do things with the arguments.
if isinstance(self._self_wrapped, (classmethod, staticmethod)):
self._self_bound_type = _BoundFunctionWrapper
if isinstance(wrapped, (classmethod, staticmethod)):
bound_type = _BoundFunctionWrapper
else:
self._self_bound_type = _BoundMethodWrapper
bound_type = _BoundMethodWrapper
def __get__(self, instance, owner):
descriptor = self._self_wrapped.__get__(instance, owner)
return self._self_bound_type(descriptor, instance, self._self_wrapper,
self._self_wrapper_args, self._self_wrapper_kwargs)
def __call__(self, *args, **kwargs):
# This is invoked when the wrapped function is being called as a
# normal function and is not bound to a class as an instance
# method. This is also invoked in the case where the wrapped
# function was a method, but this wrapper was in turn wrapped
# using the staticmethod decorator.
return self._self_wrapper(self._self_wrapped, None, args,
kwargs, *self._self_wrapper_args, **self._self_wrapper_kwargs)
super(FunctionWrapper, self).__init__(wrapped, None, wrapper,
wrapper_args, wrapper_kwargs, adapter, bound_type)
try:
from ._wrappers import ObjectProxy as C_ObjectProxy

View File

@@ -11,12 +11,11 @@ from wrapt import six
DECORATORS_CODE = """
import wrapt
def adapter1(func):
@wrapt.adapter(target=func)
def _adapter(arg):
'''adapter documentation'''
return func(arg, arg)
return _adapter
def prototype(arg): pass
@wrapt.decorator(adapter=prototype)
def adapter1(wrapped, instance, args, kwargs):
'''adapter documentation'''
return wrapped(*args, **kwargs)
"""
decorators = imp.new_module('decorators')
@@ -58,14 +57,16 @@ class TestNamingAdapter(unittest.TestCase):
self.assertEqual(function1d.__module__, __name__)
def test_doc_string(self):
# Test preservation of function __doc__ attribute from the
# adapter rather than from the original target wrapped function.
# Test preservation of function __doc__ attribute. It is
# still the documentation from the wrapped function, not
# of the adapter.
self.assertEqual(function1d.__doc__, 'adapter documentation')
self.assertEqual(function1d.__doc__, 'documentation')
def test_argspec(self):
# Test preservation of function argument specification. It
# actually needs to match that of the adapter function.
# actually needs to match that of the adapter function the
# prototype of which was supplied via the dummy function.
def _adapter(arg): pass
@@ -78,61 +79,5 @@ class TestNamingAdapter(unittest.TestCase):
self.assertTrue(isinstance(function1d, type(function1o)))
class TestAdapter(unittest.TestCase):
def test_no_arguments(self):
events = []
def _adapter(events):
def _trace(func):
@wrapt.adapter(target=func)
def _wrapper():
if events is not None:
events.append('in')
try:
return func()
finally:
if events is not None:
events.append('out')
return _wrapper
return _trace
@_adapter(events=events)
def _function():
'''documentation'''
events.append('call')
_function()
self.assertEqual(events, ['in', 'call', 'out'])
def test_add_argument(self):
events = []
def _adapter(events):
def _trace(func):
@wrapt.adapter(target=func)
def _wrapper():
if events is not None:
events.append('in')
try:
return func(1)
finally:
if events is not None:
events.append('out')
return _wrapper
return _trace
@_adapter(events=events)
def _function(*args):
'''documentation'''
events.append('call')
return args
result = _function()
self.assertEqual(result, (1,))
self.assertEqual(events, ['in', 'call', 'out'])
if __name__ == '__main__':
unittest.main()