Derive the user decorator parameters from the wrapper itself and don't require defaults.

This commit is contained in:
Graham Dumpleton
2013-08-23 15:41:34 +10:00
parent 11c2cbb304
commit a250fe748d
4 changed files with 110 additions and 141 deletions

View File

@@ -3,5 +3,4 @@ __version__ = '.'.join(__version_info__)
from .wrappers import ObjectProxy, FunctionWrapper
from .decorators import decorator, adapter
from .exceptions import (UnexpectedDefaultParameters, MissingDefaultParameter,
UnexpectedParameters)
from .exceptions import (MissingParameter, UnexpectedParameters)

View File

@@ -6,8 +6,7 @@ from functools import wraps, partial
from inspect import getargspec
from .wrappers import FunctionWrapper
from .exceptions import (UnexpectedDefaultParameters,
MissingDefaultParameter, UnexpectedParameters)
from .exceptions import (MissingParameter, UnexpectedParameters)
# 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
@@ -28,111 +27,98 @@ def _update_adapter(wrapper, target):
# wrappers which they use are designed to properly preserve any name
# attributes, function signatures etc, in addition to the wrappers
# themselves acting like a transparent proxy for the original wrapped
# function so they the wrapper is effectively indistinguishable from
# the original wrapped function.
# function so the wrapper is effectively indistinguishable from the
# original wrapped function.
WRAPPER_ARGLIST = ('wrapped', 'instance', 'args', 'kwargs')
def decorator(_wrapt_wrapper=None, _wrapt_target=None,
**_wrapt_default_params):
# The decorator works out whether the user decorator will have its
# own parameters. Parameters for the user decorator must avoid any
# keyword arguments starting with '_wrapt_' as that is reserved for
# our own use. Our 'wrapper' argument being how the user's wrapper
# function is passed in. The 'target' argument is used to optionally
# denote a function which is wrapped by an adapter decorator. In
# that case the name attrributes are copied from the target function
# rather than those of the adapter function.
def decorator(wrapper=None, target=None):
# 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.
if _wrapt_wrapper is not None:
# The wrapper has been provided, so we must also have any
# optional default keyword parameters for the user decorator
# at this point if they were supplied. Before constructing
# the decorator we validate if the list of supplied default
# parameters are actually the same as what the users wrapper
# function expects.
if wrapper is not None:
# We now need to work out whether the users decorator is
# to take any arguments. If there are parameters, the
# final decorator we create needs to be constructed a bit
# differently, as when that decorator is used it needs to
# accept parameters. Those parameters do not need to be
# supplied if they have defaults, but at least an empty
# argument list needs to be used on the decorator at that
# point. When the users decorator parameters are
# supplied, they can be as either positional or keyword
# arguments.
expected_arglist = WRAPPER_ARGLIST
complete_arglist = getargspec(_wrapt_wrapper).args
wrapper_argspec = getargspec(wrapper)
received_names = set(_wrapt_default_params.keys())
expected_names = complete_arglist[len(expected_arglist):]
wrapper_arglist = wrapper_argspec.args
wrapper_defaults = (wrapper_argspec.defaults and
wrapper_arglist[-len(wrapper_argspec.defaults):] or [])
for name in expected_names:
try:
received_names.remove(name)
except KeyError:
raise MissingDefaultParameter('Expected value for '
'default parameter %r was not supplied for '
'decorator %r.' % (name, _wrapt_wrapper.__name__))
if received_names:
raise UnexpectedDefaultParameters('Unexpected default '
'parameters %r supplied for decorator %r.' % (
list(received_names), _wrapt_wrapper.__name__))
# If we do have default parameters, the final decorator we
# create needs to be constructed a bit differently as when
# that decorator is used, it needs to accept parameters.
# Those parameters need not be supplied, but at least an
# empty argument list needs to be used on the decorator at
# that point. When parameters are supplied, they can be as
# either positional or keyword arguments.
if len(complete_arglist) > len(expected_arglist):
# For the case where the decorator is able to accept
if len(wrapper_arglist) > len(WRAPPER_ARGLIST):
# For the case where the user decorator is able to accept
# parameters, return a partial wrapper to collect the
# parameters.
@wraps(_wrapt_wrapper)
@wraps(wrapper)
def _partial(*decorator_args, **decorator_kwargs):
# Since the supply of parameters is optional due to
# having defaults, we need to construct a final set
# of parameters by overlaying those finally supplied
# to the decorator at the point of use over the
# defaults. As we accept positional parameters, we
# need to translate those back to keyword parameters
# in the process. This allows us to pass just one
# dictionary of parameters and we can validate the
# set of parameters at the point the decorator is
# used and not only let it fail at the time the
# We need to construct a final set of parameters
# by merging positional parameters with the
# keyword parameters. This allows us to pass just
# one dictionary of parameters. We can also
# validate the set of parameters at this point so
# that an error occurs when the decorator is used
# and not only let it fail at the time the
# wrapped function is called.
expected_names = wrapper_arglist[len(WRAPPER_ARGLIST):]
if len(decorator_args) > len(expected_names):
raise UnexpectedParameters('Expected at most %r '
'positional parameters for decorator %r, '
'but received %r.' % (len(expected_names),
_wrapt_wrapper.__name__, len(decorator_args)))
wrapper.__name__, len(decorator_args)))
unexpected_params = []
for name in decorator_kwargs:
if name not in _wrapt_default_params:
if name not in expected_names:
unexpected_params.append(name)
if unexpected_params:
raise UnexpectedParameters('Unexpected parameters '
'%r supplied for decorator %r.' % (
unexpected_params, _wrapt_wrapper.__name__))
complete_params = dict(_wrapt_default_params)
unexpected_params, wrapper.__name__))
for i, arg in enumerate(decorator_args):
if expected_names[i] in decorator_kwargs:
raise UnexpectedParameters('Positional parameter '
'%r also supplied as keyword parameter '
'to decorator %r.' % (expected_names[i],
_wrapt_wrapper.__name__))
wrapper.__name__))
decorator_kwargs[expected_names[i]] = arg
complete_params.update(decorator_kwargs)
received_names = set(wrapper_defaults)
received_names.update(decorator_kwargs.keys())
for name in expected_names:
if name not in received_names:
raise MissingParameter('Expected value for '
'parameter %r was not supplied for '
'decorator %r.' % (name, wrapper.__name__))
# Now create and return the final wrapper which
# combines the parameters with the wrapped function.
def _wrapper(func):
result = FunctionWrapper(wrapped=func,
wrapper=_wrapt_wrapper, params=complete_params)
if _wrapt_target:
_update_adapter(result, _wrapt_target)
wrapper=wrapper, params=decorator_kwargs)
if target:
_update_adapter(result, target)
return result
return _wrapper
@@ -145,11 +131,11 @@ def decorator(_wrapt_wrapper=None, _wrapt_target=None,
# No parameters so create and return the final wrapper.
# This is effectively the users decorator.
@wraps(_wrapt_wrapper)
@wraps(wrapper)
def _wrapper(func):
result = FunctionWrapper(wrapped=func, wrapper=_wrapt_wrapper)
if _wrapt_target:
_update_adapter(result, _wrapt_target)
result = FunctionWrapper(wrapped=func, wrapper=wrapper)
if target:
_update_adapter(result, target)
return result
return _wrapper
@@ -160,11 +146,10 @@ def decorator(_wrapt_wrapper=None, _wrapt_target=None,
# a partial using the collected default parameters and the
# adapter function if one is being used.
return partial(decorator, _wrapt_target=_wrapt_target,
**_wrapt_default_params)
return partial(decorator, target=target)
def adapter(target):
@decorator(_wrapt_target=target)
@decorator(target=target)
def wrapper(wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
return wrapper

View File

@@ -1,3 +1,2 @@
class UnexpectedDefaultParameters(Exception): pass
class MissingDefaultParameter(Exception): pass
class UnexpectedParameters(Exception): pass
class MissingParameter(TypeError): pass
class UnexpectedParameters(TypeError): pass

View File

@@ -27,10 +27,10 @@ class TestDecorator(unittest.TestCase):
_args = (1, 2)
_kwargs = { 'one': 1, 'two': 2 }
@wrapt.decorator(param1=1, param2=2)
def _decorator(wrapped, instance, args, kwargs, param1, param2):
self.assertEqual(param1, 1)
self.assertEqual(param2, 2)
@wrapt.decorator
def _decorator(wrapped, instance, args, kwargs, p1=1, p2=2):
self.assertEqual(p1, 1)
self.assertEqual(p2, 2)
return wrapped(*args, **kwargs)
@_decorator()
@@ -45,13 +45,13 @@ class TestDecorator(unittest.TestCase):
_args = (1, 2)
_kwargs = { 'one': 1, 'two': 2 }
@wrapt.decorator(param1=1, param2=2)
def _decorator(wrapped, instance, args, kwargs, param1, param2):
self.assertEqual(param1, 3)
self.assertEqual(param2, 4)
@wrapt.decorator
def _decorator(wrapped, instance, args, kwargs, p1=1, p2=2):
self.assertEqual(p1, 3)
self.assertEqual(p2, 4)
return wrapped(*args, **kwargs)
@_decorator(param1=3, param2=4)
@_decorator(p1=3, p2=4)
def _function(*args, **kwargs):
return args, kwargs
@@ -63,13 +63,13 @@ class TestDecorator(unittest.TestCase):
_args = (1, 2)
_kwargs = { 'one': 1, 'two': 2 }
@wrapt.decorator(param1=1, param2=2)
def _decorator(wrapped, instance, args, kwargs, param1, param2):
self.assertEqual(param1, 1)
self.assertEqual(param2, 4)
@wrapt.decorator
def _decorator(wrapped, instance, args, kwargs, p1=1, p2=2):
self.assertEqual(p1, 1)
self.assertEqual(p2, 4)
return wrapped(*args, **kwargs)
@_decorator(param2=4)
@_decorator(p2=4)
def _function(*args, **kwargs):
return args, kwargs
@@ -77,52 +77,38 @@ class TestDecorator(unittest.TestCase):
self.assertEqual(result, (_args, _kwargs))
def test_missing_default_parameter(self):
def test_required_parameter_missing(self):
def run(*args):
@wrapt.decorator(param1=1)
def _decorator(wrapped, instance, args, kwargs, param1, param2):
@wrapt.decorator
def _decorator(wrapped, instance, args, kwargs, p1, p2=2):
return wrapped(*args, **kwargs)
self.assertRaises(wrapt.exceptions.MissingDefaultParameter,
@_decorator()
def _function(*args, **kwargs):
return args, kwargs
self.assertRaises(wrapt.exceptions.MissingParameter,
run, ())
def test_unexpected_default_parameters_one(self):
def test_unexpected_parameters_one(self):
def run(*args):
@wrapt.decorator(param1=1, param2=2, param3=4)
def _decorator(wrapped, instance, args, kwargs, param1, param2):
@wrapt.decorator
def _decorator(wrapped, instance, args, kwargs, p1=1, p2=2):
return wrapped(*args, **kwargs)
self.assertRaises(wrapt.exceptions.UnexpectedDefaultParameters,
run, ())
def test_unexpected_default_parameters_many(self):
def run(*args):
@wrapt.decorator(param1=1, param2=2, param3=4, param4=4)
def _decorator(wrapped, instance, args, kwargs, param1, param2):
return wrapped(*args, **kwargs)
self.assertRaises(wrapt.exceptions.UnexpectedDefaultParameters,
run, ())
def test_unexpected_override_parameters_one(self):
def run(*args):
@wrapt.decorator(param1=1, param2=2)
def _decorator(wrapped, instance, args, kwargs, param1, param2):
return wrapped(*args, **kwargs)
@_decorator(param3=3)
@_decorator(p3=3)
def _function(*args, **kwargs):
return args, kwargs
self.assertRaises(wrapt.exceptions.UnexpectedParameters, run, ())
def test_unexpected_override_parameters_many(self):
def test_unexpected_parameters_many(self):
def run(*args):
@wrapt.decorator(param1=1, param2=2)
def _decorator(wrapped, instance, args, kwargs, param1, param2):
@wrapt.decorator
def _decorator(wrapped, instance, args, kwargs, p1=1, p2=2):
return wrapped(*args, **kwargs)
@_decorator(param3=3, param4=4)
@_decorator(p3=3, p4=4)
def _function(*args, **kwargs):
return args, kwargs
@@ -132,10 +118,10 @@ class TestDecorator(unittest.TestCase):
_args = (1, 2)
_kwargs = { 'one': 1, 'two': 2 }
@wrapt.decorator(param1=1, param2=2)
def _decorator(wrapped, instance, args, kwargs, param1, param2):
self.assertEqual(param1, 3)
self.assertEqual(param2, 4)
@wrapt.decorator
def _decorator(wrapped, instance, args, kwargs, p1=1, p2=2):
self.assertEqual(p1, 3)
self.assertEqual(p2, 4)
return wrapped(*args, **kwargs)
@_decorator(3, 4)
@@ -150,10 +136,10 @@ class TestDecorator(unittest.TestCase):
_args = (1, 2)
_kwargs = { 'one': 1, 'two': 2 }
@wrapt.decorator(param1=1, param2=2)
def _decorator(wrapped, instance, args, kwargs, param1, param2):
self.assertEqual(param1, 3)
self.assertEqual(param2, 2)
@wrapt.decorator
def _decorator(wrapped, instance, args, kwargs, p1=1, p2=2):
self.assertEqual(p1, 3)
self.assertEqual(p2, 2)
return wrapped(*args, **kwargs)
@_decorator(3)
@@ -168,13 +154,13 @@ class TestDecorator(unittest.TestCase):
_args = (1, 2)
_kwargs = { 'one': 1, 'two': 2 }
@wrapt.decorator(param1=1, param2=2)
def _decorator(wrapped, instance, args, kwargs, param1, param2):
self.assertEqual(param1, 3)
self.assertEqual(param2, 4)
@wrapt.decorator
def _decorator(wrapped, instance, args, kwargs, p1=1, p2=2):
self.assertEqual(p1, 3)
self.assertEqual(p2, 4)
return wrapped(*args, **kwargs)
@_decorator(3, param2=4)
@_decorator(3, p2=4)
def _function(*args, **kwargs):
return args, kwargs
@@ -184,8 +170,8 @@ class TestDecorator(unittest.TestCase):
def test_override_parameters_positional_excess_one(self):
def run(*args):
@wrapt.decorator(param1=1, param2=2)
def _decorator(wrapped, instance, args, kwargs, param1, param2):
@wrapt.decorator
def _decorator(wrapped, instance, args, kwargs, p1=1, p2=2):
return wrapped(*args, **kwargs)
@_decorator(3, 4, 5)
@@ -196,8 +182,8 @@ class TestDecorator(unittest.TestCase):
def test_override_parameters_positional_excess_many(self):
def run(*args):
@wrapt.decorator(param1=1, param2=2)
def _decorator(wrapped, instance, args, kwargs, param1, param2):
@wrapt.decorator
def _decorator(wrapped, instance, args, kwargs, p1=1, p2=2):
return wrapped(*args, **kwargs)
@_decorator(3, 4, 5, 6)