Replace decorator parameter validation with variant of code from inspect.getcallargs() from Python 3.
This commit is contained in:
@@ -3,4 +3,3 @@ __version__ = '.'.join(__version_info__)
|
||||
|
||||
from .wrappers import ObjectProxy, FunctionWrapper
|
||||
from .decorators import decorator, adapter
|
||||
from .exceptions import (MissingParameter, UnexpectedParameters)
|
||||
|
||||
@@ -3,10 +3,135 @@
|
||||
"""
|
||||
|
||||
from functools import wraps, partial
|
||||
from inspect import getargspec
|
||||
from inspect import getargspec, ismethod
|
||||
from collections import namedtuple
|
||||
|
||||
from .wrappers import FunctionWrapper
|
||||
from .exceptions import (MissingParameter, UnexpectedParameters)
|
||||
|
||||
from . import six
|
||||
|
||||
# We must use inspect.getfullargspec() in Python 3 because of the
|
||||
# possibility that decorators can be applied to functions with keyword
|
||||
# only arguments. Create a version of getfulargspec() for Python 2 so we
|
||||
# work against the same specification structure.
|
||||
|
||||
if six.PY2:
|
||||
FullArgSpec = namedtuple('FullArgSpec', 'args, varargs, varkw, ' \
|
||||
'defaults, kwonlyargs, kwonlydefaults, annotations')
|
||||
|
||||
def getfullargspec(func):
|
||||
argspec = getargspec(func)
|
||||
return FullArgSpec(args=argspec.args, varargs=argspec.varargs,
|
||||
varkw=argspec.keywords, defaults=argspec.defaults,
|
||||
kwonlyargs=[], kwonlydefaults=None, annotations={})
|
||||
|
||||
else:
|
||||
from inspect import getfullargspec
|
||||
|
||||
# Python 3 provides getcallargs() that could be used to validate the
|
||||
# decorator parameters, but it would not be available in Python 2. Plus,
|
||||
# we need to fiddle the result from getfullargspec() a bit as we are
|
||||
# going to be using the argspec from the wrapper and not the decorator,
|
||||
# so it has our standard wrapper arguments before what would be the
|
||||
# decorator arguments. So we duplicate the code from Python 3 and tweak
|
||||
# it a bit.
|
||||
|
||||
def _missing_arguments(f_name, argnames, pos, values):
|
||||
names = [repr(name) for name in argnames if name not in values]
|
||||
missing = len(names)
|
||||
if missing == 1:
|
||||
s = names[0]
|
||||
elif missing == 2:
|
||||
s = "{} and {}".format(*names)
|
||||
else:
|
||||
tail = ", {} and {}".format(names[-2:])
|
||||
del names[-2:]
|
||||
s = ", ".join(names) + tail
|
||||
raise TypeError("%s() missing %i required %s argument%s: %s" %
|
||||
(f_name, missing,
|
||||
"positional" if pos else "keyword-only",
|
||||
"" if missing == 1 else "s", s))
|
||||
|
||||
def _too_many(f_name, args, kwonly, varargs, defcount, given, values):
|
||||
atleast = len(args) - defcount
|
||||
kwonly_given = len([arg for arg in kwonly if arg in values])
|
||||
if varargs:
|
||||
plural = atleast != 1
|
||||
sig = "at least %d" % (atleast,)
|
||||
elif defcount:
|
||||
plural = True
|
||||
sig = "from %d to %d" % (atleast, len(args))
|
||||
else:
|
||||
plural = len(args) != 1
|
||||
sig = str(len(args))
|
||||
kwonly_sig = ""
|
||||
if kwonly_given:
|
||||
msg = " positional argument%s (and %d keyword-only argument%s)"
|
||||
kwonly_sig = (msg % ("s" if given != 1 else "", kwonly_given,
|
||||
"s" if kwonly_given != 1 else ""))
|
||||
raise TypeError("%s() takes %s positional argument%s but %d%s %s given" %
|
||||
(f_name, sig, "s" if plural else "", given, kwonly_sig,
|
||||
"was" if given == 1 and not kwonly_given else "were"))
|
||||
|
||||
def _validate_parameters(func, spec, *positional, **named):
|
||||
args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, ann = spec
|
||||
|
||||
# We throw away the initial leading arguments as they are our
|
||||
# special wrapper arguments and not part of the arguments which
|
||||
# become the decorator arguments.
|
||||
|
||||
args = args[len(WRAPPER_ARGLIST):]
|
||||
|
||||
f_name = func.__name__
|
||||
arg2value = {}
|
||||
|
||||
if ismethod(func) and func.__self__ is not None:
|
||||
# implicit 'self' (or 'cls' for classmethods) argument
|
||||
positional = (func.__self__,) + positional
|
||||
num_pos = len(positional)
|
||||
num_args = len(args)
|
||||
num_defaults = len(defaults) if defaults else 0
|
||||
|
||||
n = min(num_pos, num_args)
|
||||
for i in range(n):
|
||||
arg2value[args[i]] = positional[i]
|
||||
if varargs:
|
||||
arg2value[varargs] = tuple(positional[n:])
|
||||
possible_kwargs = set(args + kwonlyargs)
|
||||
if varkw:
|
||||
arg2value[varkw] = {}
|
||||
for kw, value in named.items():
|
||||
if kw not in possible_kwargs:
|
||||
if not varkw:
|
||||
raise TypeError("%s() got an unexpected keyword argument %r" %
|
||||
(f_name, kw))
|
||||
arg2value[varkw][kw] = value
|
||||
continue
|
||||
if kw in arg2value:
|
||||
raise TypeError("%s() got multiple values for argument %r" %
|
||||
(f_name, kw))
|
||||
arg2value[kw] = value
|
||||
if num_pos > num_args and not varargs:
|
||||
_too_many(f_name, args, kwonlyargs, varargs, num_defaults,
|
||||
num_pos, arg2value)
|
||||
if num_pos < num_args:
|
||||
req = args[:num_args - num_defaults]
|
||||
for arg in req:
|
||||
if arg not in arg2value:
|
||||
_missing_arguments(f_name, req, True, arg2value)
|
||||
for i, arg in enumerate(args[num_args - num_defaults:]):
|
||||
if arg not in arg2value:
|
||||
arg2value[arg] = defaults[i]
|
||||
missing = 0
|
||||
for kwarg in kwonlyargs:
|
||||
if kwarg not in arg2value:
|
||||
if kwarg in kwonlydefaults:
|
||||
arg2value[kwarg] = kwonlydefaults[kwarg]
|
||||
else:
|
||||
missing += 1
|
||||
if missing:
|
||||
_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
|
||||
@@ -54,64 +179,26 @@ def decorator(wrapper=None, target=None):
|
||||
# supplied, they can be as either positional or keyword
|
||||
# arguments.
|
||||
|
||||
wrapper_argspec = getargspec(wrapper)
|
||||
|
||||
wrapper_argspec = getfullargspec(wrapper)
|
||||
wrapper_arglist = wrapper_argspec.args
|
||||
wrapper_defaults = (wrapper_argspec.defaults and
|
||||
wrapper_arglist[-len(wrapper_argspec.defaults):] or [])
|
||||
|
||||
if (len(wrapper_arglist) > len(WRAPPER_ARGLIST) or
|
||||
wrapper_argspec.varargs or wrapper_argspec.keywords):
|
||||
wrapper_argspec.varargs or wrapper_argspec.varkw or
|
||||
wrapper_argspec.kwonlyargs):
|
||||
|
||||
# For the case where the user decorator is able to accept
|
||||
# parameters, return a partial wrapper to collect the
|
||||
# parameters.
|
||||
|
||||
@wraps(wrapper)
|
||||
def _partial(*decorator_args, **decorator_kwargs):
|
||||
# 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.
|
||||
# Validate the set of decorator parameters at
|
||||
# this point so that an error occurs when the
|
||||
# decorator is used and not only when the
|
||||
# function is later called.
|
||||
|
||||
expected_names = wrapper_arglist[len(WRAPPER_ARGLIST):]
|
||||
|
||||
if len(decorator_args) > len(expected_names):
|
||||
if not wrapper_argspec.varargs:
|
||||
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 expected_names:
|
||||
unexpected_params.append(name)
|
||||
|
||||
if unexpected_params:
|
||||
if not wrapper_argspec.keywords:
|
||||
raise UnexpectedParameters('Unexpected parameters '
|
||||
'%r supplied for decorator %r.' % (
|
||||
unexpected_params, wrapper.__name__))
|
||||
|
||||
received_names = set(wrapper_defaults)
|
||||
for i in range(min(len(decorator_args), len(expected_names))):
|
||||
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__))
|
||||
received_names.add(expected_names[i])
|
||||
|
||||
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__))
|
||||
_validate_parameters(wrapper, wrapper_argspec,
|
||||
*decorator_args, **decorator_kwargs)
|
||||
|
||||
# Now create and return the final wrapper which
|
||||
# combines the parameters with the wrapped function.
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
class MissingParameter(TypeError): pass
|
||||
class UnexpectedParameters(TypeError): pass
|
||||
@@ -3,7 +3,6 @@ from __future__ import print_function
|
||||
import unittest
|
||||
|
||||
import wrapt
|
||||
import wrapt.exceptions
|
||||
|
||||
class TestDecorator(unittest.TestCase):
|
||||
|
||||
@@ -87,8 +86,7 @@ class TestDecorator(unittest.TestCase):
|
||||
def _function(*args, **kwargs):
|
||||
return args, kwargs
|
||||
|
||||
self.assertRaises(wrapt.exceptions.MissingParameter,
|
||||
run, ())
|
||||
self.assertRaises(TypeError, run, ())
|
||||
|
||||
def test_unexpected_parameters_one(self):
|
||||
def run(*args):
|
||||
@@ -100,7 +98,7 @@ class TestDecorator(unittest.TestCase):
|
||||
def _function(*args, **kwargs):
|
||||
return args, kwargs
|
||||
|
||||
self.assertRaises(wrapt.exceptions.UnexpectedParameters, run, ())
|
||||
self.assertRaises(TypeError, run, ())
|
||||
|
||||
def test_unexpected_parameters_many(self):
|
||||
def run(*args):
|
||||
@@ -112,7 +110,7 @@ class TestDecorator(unittest.TestCase):
|
||||
def _function(*args, **kwargs):
|
||||
return args, kwargs
|
||||
|
||||
self.assertRaises(wrapt.exceptions.UnexpectedParameters, run, ())
|
||||
self.assertRaises(TypeError, run, ())
|
||||
|
||||
def test_override_parameters_positional_all(self):
|
||||
_args = (1, 2)
|
||||
@@ -178,7 +176,7 @@ class TestDecorator(unittest.TestCase):
|
||||
def _function(*args, **kwargs):
|
||||
return args, kwargs
|
||||
|
||||
self.assertRaises(wrapt.exceptions.UnexpectedParameters, run, ())
|
||||
self.assertRaises(TypeError, run, ())
|
||||
|
||||
def test_override_parameters_positional_excess_many(self):
|
||||
def run(*args):
|
||||
@@ -190,7 +188,7 @@ class TestDecorator(unittest.TestCase):
|
||||
def _function(*args, **kwargs):
|
||||
return args, kwargs
|
||||
|
||||
self.assertRaises(wrapt.exceptions.UnexpectedParameters, run, ())
|
||||
self.assertRaises(TypeError, run, ())
|
||||
|
||||
def test_varargs_parameters(self):
|
||||
_args = (1, 2)
|
||||
|
||||
Reference in New Issue
Block a user