#!/usr/bin/python
from __future__ import print_function
import functools
import inspect
def decorator_factory(*kw_only_parameters):
"""Transforms a function with keyword arguments into one with
keyword-only arguments.
Call this decorator as @decorator_factory() for the default mode,
which makes all keyword arguments keyword-only, or with the names
of arguments to make keyword-only. They must correspond with the
names of arguments in the decorated function. It works by
collecting all the arguments into *args and **kws, then moving the
arguments marked as keyword-only from **kws into *args.
Args:
*kw_only_parameters: Keyword-only arguments as strings.
Returns:
A decorator that modifies a function so it has keyword-only
arguments.
"""
def decorator(wrapped):
"""The decorator itself, assigns arguments as keyword-only and
calculates sets for error checking.
Args:
wrapped: The function to decorate.
Returns:
A function wrapped so that it has keyword-only arguments.
"""
# Each Python 3 argument has two independent properties: it is
# positional-and-keyword *or* keyword-only, and it has a
# default value or it doesn't.
names, varargs, _, defaults = inspect.getargspec(wrapped)
# If there are no default values getargpsec() returns None
# rather than an empty iterable for some reason.
if defaults is None:
defaults = ()
names_with_defaults = frozenset(names[len(names) - len(defaults):])
names_to_defaults = dict(zip(reversed(names), reversed(defaults)))
if kw_only_parameters:
kw_only_names = frozenset(kw_only_parameters)
else:
kw_only_names = names_with_defaults.copy()
@functools.wraps(wrapped)
def wrapper(*args, **kws):
"""Wrapper function, checks arguments with set operations, moves args
from **kws into *args, and then calls wrapped().
Args:
*args, **kws: The arguments passed to the original function.
Returns:
The original function's result when it's called with the
modified arguments.
Raises:
TypeError: When there is a mismatch between the supplied
and expected arguments.
"""
new_args = []
args_index = 0
for name in names:
if name in kws:
# Check first if there's a bound keyword for this name
new_args.append(kws.pop(name))
elif name in kw_only_names:
# If this name is keyword-only, check for a
# default or raise.
if name in names_to_defaults:
new_args.append(names_to_defaults[name])
else:
_wrong_args(wrapped, names,
kw_only_names -
(names_with_defaults | frozenset(kws)),
'keyword-only')
elif args_index < len(args):
# Check for a positional arg.
new_args.append(args[args_index])
args_index += 1
elif name in names_to_defaults:
# Check for a default value.
new_args.append(names_to_defaults[name])
else:
# No positional arg or default for this name so raise.
_wrong_args(wrapped, names,
frozenset(names) -
(names_with_defaults | frozenset(kws)),
'positional', len(args))
if args_index != len(args) and not varargs:
# Too many positional arguments and no varargs, so
# raise after subtracting off the number of kw-only
# arguments from those expected.
raise TypeError(
'%s() takes %d positional arguments but %d were given' %
(wrapped.__name__,
len(names) - len(names_with_defaults | frozenset(kws)),
len(args)))
else:
# Pass the rest of the positional args, if any.
new_args.extend(args[args_index:])
return wrapped(*new_args, **kws)
return wrapper
return decorator
def _wrong_args(wrapped, names, missing_args, arg_type, number_of_args=0):
""" Raise Python 3-style TypeErrors for missing arguments."""
ordered_args = [a for a in names if a in missing_args]
ordered_args = ordered_args[number_of_args:]
error_message = ['%s() missing %d required %s argument' %
(wrapped.__name__, len(ordered_args), arg_type)]
if len(ordered_args) == 1:
error_message.append(": '%s'" % ordered_args[0])
else:
error_message.extend(['s: ',
' '.join("'%s'" % a for a in ordered_args[:-1]),
" and '%s'" % ordered_args[-1]])
raise TypeError(''.join(error_message))
if __name__ == '__main__':
def test(f, *args, **kws):
print(args, kws, '-> ', end='')
try:
f(*args, **kws)
except TypeError as e:
print(e.args[0])
@decorator_factory()
def f(a, b, c, d, e='e'):
print(a, b, c, d, e)
test(f)
test(f, 0, 1, 2, 3)
test(f, 0, 1, 2, 3, 4)
@decorator_factory('c')
def f(a, b, c='c', d='d', *args, **kws):
print(a, b, c, d, args, kws)
test(f)
test(f, 0, 1)
test(f, -1, b='b')
test(f, b='b')
test(f, 0)
test(f, 0, 1, 2, 3, 4, 5, c='foo', d='bar', e='baz')
@decorator_factory('b', 'c')
def f(a, b, c='c', d='d', *args, **kws):
print(a, b, c, d, args, kws)
test(f, 0)
test(f, 0, b='b')
test(f, 0, 1, 2)
test(f, 0, 1, 2, b='b')
test(f, 0, 1, 2, 3, 4, 5, c='foo', d='bar', e='baz')
test(f, 0, 1, 2, 3, 4, 5, b='foo', c='bar', e='baz')
class C(object):
@decorator_factory('b', 'c')
def __init__(self, a, b, c='c', d='d', *args, **kws):
print(a, b, c, d, args, kws)
test(C, 0, 1, 2, b='b')
test(C, 0, 1, 2, 3, 4, 5, b='foo', c='bar', e='baz')
Diff to Previous Revision
--- revision 9 2015-09-21 21:52:55
+++ revision 10 2015-09-22 18:16:40
@@ -4,18 +4,6 @@
import functools
import inspect
-
-try:
- from itertools import zip_longest
-except ImportError:
- from itertools import izip_longest as zip_longest
-
-# This is a placeholder object to distinguish parameters without
-# defaults from parameters that have None as their default.
-class NoDefault(object):
- def __repr__(self):
- return 'NoDefault'
-no_default = NoDefault()
def decorator_factory(*kw_only_parameters):
@@ -59,11 +47,7 @@
if defaults is None:
defaults = ()
names_with_defaults = frozenset(names[len(names) - len(defaults):])
- parameters = list(zip_longest(reversed(names), reversed(defaults),
- fillvalue=no_default))
- parameters.reverse()
- parameters = tuple(parameters)
- names = frozenset(names)
+ names_to_defaults = dict(zip(reversed(names), reversed(defaults)))
if kw_only_parameters:
kw_only_names = frozenset(kw_only_parameters)
else:
@@ -89,17 +73,17 @@
new_args = []
args_index = 0
- for name, default in parameters:
+ for name in names:
if name in kws:
# Check first if there's a bound keyword for this name
new_args.append(kws.pop(name))
elif name in kw_only_names:
# If this name is keyword-only, check for a
# default or raise.
- if default is not no_default:
- new_args.append(default)
+ if name in names_to_defaults:
+ new_args.append(names_to_defaults[name])
else:
- _wrong_args(wrapped, parameters,
+ _wrong_args(wrapped, names,
kw_only_names -
(names_with_defaults | frozenset(kws)),
'keyword-only')
@@ -107,13 +91,14 @@
# Check for a positional arg.
new_args.append(args[args_index])
args_index += 1
- elif default is not no_default:
+ elif name in names_to_defaults:
# Check for a default value.
- new_args.append(default)
+ new_args.append(names_to_defaults[name])
else:
# No positional arg or default for this name so raise.
- _wrong_args(wrapped, parameters,
- names - (names_with_defaults | frozenset(kws)),
+ _wrong_args(wrapped, names,
+ frozenset(names) -
+ (names_with_defaults | frozenset(kws)),
'positional', len(args))
if args_index != len(args) and not varargs:
@@ -126,7 +111,7 @@
len(names) - len(names_with_defaults | frozenset(kws)),
len(args)))
else:
- # Pass the rest of the positional args
+ # Pass the rest of the positional args, if any.
new_args.extend(args[args_index:])
return wrapped(*new_args, **kws)
@@ -135,9 +120,9 @@
return decorator
-def _wrong_args(wrapped, parameters, missing_args, arg_type, number_of_args=0):
+def _wrong_args(wrapped, names, missing_args, arg_type, number_of_args=0):
""" Raise Python 3-style TypeErrors for missing arguments."""
- ordered_args = [a for a, _ in parameters if a in missing_args]
+ ordered_args = [a for a in names if a in missing_args]
ordered_args = ordered_args[number_of_args:]
error_message = ['%s() missing %d required %s argument' %
(wrapped.__name__, len(ordered_args), arg_type)]