160 lines
7.7 KiB
Python
160 lines
7.7 KiB
Python
"""This module implements a set of decorators for implementing other
|
|
decorators. These can be generic decorators where there is never a specific
|
|
requirement to reliably process the inbound arguments being passed to the
|
|
wrapped function, or more specific decorators designed just for functions,
|
|
instance methods etc.
|
|
|
|
"""
|
|
|
|
from functools import wraps, partial
|
|
from inspect import getargspec
|
|
|
|
from .wrappers import GenericWrapper, FunctionWrapper, InstanceMethodWrapper
|
|
from .exceptions import (UnexpectedDefaultParameters, MissingDefaultParameter,
|
|
UnexpectedParameters)
|
|
|
|
def _decorator_factory(wrapper_type):
|
|
# This decorator factory serves as a way of parameterising our
|
|
# decorators based on the wrapper type. The wrapper type determines
|
|
# whether or not descriptor behaviour is supported and the number of
|
|
# arguments which are then in turn passed to the wrapper function
|
|
# supplied by the user, which implements their decorator. In other
|
|
# words, it saves us from duplicating all of this code for the
|
|
# different decorator types we provide for constructing the users
|
|
# decorators.
|
|
|
|
def _decorator_binder(wrapper=None, adapter=None, **default_params):
|
|
# The binder works out whether the user decorator will have its
|
|
# own parameters. Parameters for the user decorator must always
|
|
# be specified using keyword arguments and must always have
|
|
# defaults. The user cannot use 'wrapper' or 'adapter' for their
|
|
# own parameters as we use them ourselves and so they are
|
|
# effectively reserved. The 'wrapper' argument being how the
|
|
# user's wrapper function is passed in. The 'adapter' argument
|
|
# is used to optionally denote a function which is an adapter,
|
|
# which changes the effective prototype of the wrapped function.
|
|
# The latter is used to ensure that any function argument
|
|
# specification returned by the final result of any decorator is
|
|
# correct and reflects that of the adapter and not the wrapped
|
|
# function.
|
|
|
|
if 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.
|
|
|
|
expected_arglist = wrapper_type.WRAPPER_ARGLIST
|
|
complete_arglist = getargspec(wrapper).args
|
|
|
|
received_names = set(default_params.keys())
|
|
expected_names = complete_arglist[len(expected_arglist):]
|
|
|
|
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, wrapper.__name__))
|
|
if received_names:
|
|
raise UnexpectedDefaultParameters('Unexpected default '
|
|
'parameters %r supplied for decorator %r.' % (
|
|
list(received_names), 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
|
|
# parameters, return a partial wrapper to collect the
|
|
# parameters.
|
|
|
|
@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
|
|
# wrapped function is called.
|
|
|
|
if len(decorator_args) > len(expected_names):
|
|
raise UnexpectedParameters('Expected at most %r '
|
|
'positional parameters for decorator %r, '
|
|
'but received %r.' % (len(expected_names),
|
|
wrapper.__name__, len(decorator_args)))
|
|
|
|
unexpected_params = []
|
|
for name in decorator_kwargs:
|
|
if name not in default_params:
|
|
unexpected_params.append(name)
|
|
|
|
if unexpected_params:
|
|
raise UnexpectedParameters('Unexpected parameters '
|
|
'%r supplied for decorator %r.' % (
|
|
unexpected_params, wrapper.__name__))
|
|
|
|
complete_params = dict(default_params)
|
|
|
|
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],
|
|
wrapper.__name__))
|
|
decorator_kwargs[expected_names[i]] = arg
|
|
|
|
complete_params.update(decorator_kwargs)
|
|
|
|
# Now create and return the final wrapper which
|
|
# combines the parameters with the wrapped function.
|
|
|
|
def _wrapper(func):
|
|
return wrapper_type(wrapped=func, wrapper=wrapper,
|
|
adapter=adapter, params=complete_params)
|
|
return _wrapper
|
|
|
|
# Here is where the partial wrapper is returned. This is
|
|
# effectively the users decorator.
|
|
|
|
return _partial
|
|
|
|
else:
|
|
# No parameters so create and return the final wrapper.
|
|
# This is effectively the users decorator.
|
|
|
|
@wraps(wrapper)
|
|
def _wrapper(func):
|
|
return wrapper_type(wrapped=func, wrapper=wrapper,
|
|
adapter=adapter)
|
|
return _wrapper
|
|
|
|
else:
|
|
# The wrapper still has not been provided, so we are just
|
|
# collecting the optional default keyword parameters for the
|
|
# users decorator at this point. Return the binder again as
|
|
# a partial using the collected default parameters and the
|
|
# adapter function if one is being used.
|
|
|
|
return partial(_decorator_binder, adapter=adapter,
|
|
**default_params)
|
|
|
|
return _decorator_binder
|
|
|
|
generic_decorator = _decorator_factory(GenericWrapper)
|
|
function_decorator = _decorator_factory(FunctionWrapper)
|
|
instancemethod_decorator = _decorator_factory(InstanceMethodWrapper)
|