From a250fe748d2597de1727ae2a45ef7eb254edcde5 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Fri, 23 Aug 2013 15:41:34 +1000 Subject: [PATCH] Derive the user decorator parameters from the wrapper itself and don't require defaults. --- src/__init__.py | 3 +- src/decorators.py | 135 +++++++++++++++++---------------------- src/exceptions.py | 5 +- tests/test_decorators.py | 108 ++++++++++++++----------------- 4 files changed, 110 insertions(+), 141 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 1be97b5..785df2d 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -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) diff --git a/src/decorators.py b/src/decorators.py index 9b79d03..ab186f5 100644 --- a/src/decorators.py +++ b/src/decorators.py @@ -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 diff --git a/src/exceptions.py b/src/exceptions.py index 7e863ed..0ebc4b8 100644 --- a/src/exceptions.py +++ b/src/exceptions.py @@ -1,3 +1,2 @@ -class UnexpectedDefaultParameters(Exception): pass -class MissingDefaultParameter(Exception): pass -class UnexpectedParameters(Exception): pass +class MissingParameter(TypeError): pass +class UnexpectedParameters(TypeError): pass diff --git a/tests/test_decorators.py b/tests/test_decorators.py index a3a1d81..b10a773 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -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)