Replace decorator parameter validation with variant of code from inspect.getcallargs() from Python 3.

This commit is contained in:
Graham Dumpleton
2013-08-24 22:39:02 +10:00
parent 2e9670810e
commit 136a69b472
4 changed files with 142 additions and 60 deletions

View File

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

View File

@@ -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.

View File

@@ -1,2 +0,0 @@
class MissingParameter(TypeError): pass
class UnexpectedParameters(TypeError): pass

View File

@@ -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)