[Python-ideas] positional only arguments decorator
Steven Bethard
steven.bethard at gmail.com
Mon May 21 20:30:38 CEST 2007
Ok, looks like there's not much chance of agreeing on a syntax, so
here's a decorator that covers the two use cases I know of::
* matching the signature of dict() and dict.update()
* allowing arguments to have their names changed without worrying
about backwards compatibility (e.g. ``def func(sequence)`` changing to
``def func(iterable)``)
I've pasted the docstring and code below. The docstring should give
you a pretty good idea of the functionality. Let me know if you have
use cases this wouldn't cover.
"""
Functions declared ad @positional_only prohibit any arguments from being
passed in as keyword arguments::
>>> @positional_only
... def foo(a, b=None, *args):
... return a, b, args
...
>>> foo(1, 2, 3)
(1, 2, (3,))
>>> foo(a=1, b=2)
Traceback (most recent call last):
...
TypeError: foo() does not allow keyword arguments
Note that you can't use **kwargs arguments with a @positional_only
function::
>>> @positional_only
... def foo(a, b=None, **kwargs):
... pass
...
Traceback (most recent call last):
...
TypeError: foo() may not have **kwargs
If you need the functionality of **kwargs, you can request it by
adding a final argument with the name _kwargs. The keyword arguments
will be passed in (as a dict) through this argument::
>>> @positional_only
... def foo(a, b=None, _kwargs=None):
... return a, b, _kwargs
...
>>> foo(1)
(1, None, {})
>>> foo(1, 2)
(1, 2, {})
>>> foo(1, bar=42, baz='spam')
(1, None, {'baz': 'spam', 'bar': 42})
When your function is called with the wrong number of arguments, your
callers will only see the number of arguments they expected. That is,
argument counts will not include the _kwargs argument::
>>> foo()
Traceback (most recent call last):
...
TypeError: foo() takes at least 1 positional argument(s) (0 given)
>>> foo(1, 2, 3)
Traceback (most recent call last):
...
TypeError: foo() takes at most 2 positional argument(s) (3 given)
Note that unlike a normal **kwargs, using _kwargs in a
@positional_only function causes *all* keyword arguments to be
collected, even those with the same names as other function
parameters::
>>> foo(1, a=42, b='spam')
(1, None, {'a': 42, 'b': 'spam'})
>>> foo(1, _kwargs='this is just silly')
(1, None, {'_kwargs': 'this is just silly'})
This is of course because all function parameters are considered to
be positional only and therefore unnamed for the purposes of argument
parsing.
Note that you cannot use *args or **kwargs with the _kwargs argument::
>>> @positional_only
... def foo(a, b, _kwargs, *args):
... pass
...
Traceback (most recent call last):
...
TypeError: foo() may not have *args
>>> @positional_only
... def foo(a, b, _kwargs, **kwargs):
... pass
...
Traceback (most recent call last):
...
TypeError: foo() may not have **kwargs
"""
import doctest
import inspect
import functools
def positional_only(func):
name = func.__name__
# don't allow **kwargs
arg_names, args_name, kwargs_name, _ = inspect.getargspec(func)
if kwargs_name is not None:
raise TypeError('%s() may not have **kwargs' % name)
# if no keyword arguments were requested, create a wrapper that
# only accepts positional arguments (as *args)
if not arg_names or arg_names[-1] != '_kwargs':
# wrapper that raises an error for **kwargs
def positional_wrapper(*args, **kwargs):
if kwargs:
msg = '%s() does not allow keyword arguments'
raise TypeError(msg % name)
return func(*args)
# if keyword arguments were requested (through a final _kwargs),
# create a wrapper that collects **kwargs and passes them in as
# the final positional argument
else:
# don't allow *args
if args_name is not None:
msg = '%s() may not have *args'
raise TypeError(msg % func.__name__)
# determine defaults and expected argument counts
defaults = func.func_defaults
defaults = defaults and defaults[:-1] or []
n_expected = func.func_code.co_argcount - 1
min_expected = n_expected - len(defaults)
def positional_wrapper(*args, **kwargs):
# raise a TypeError for wrong number of arguments here
# because func() needs to take the extra 'kwargs'
# argument but the caller shouldn't know it
arg_count_err = None
if len(args) < min_expected:
arg_count_err = 'at least %i' % min_expected
if len(args) > n_expected:
arg_count_err = 'at most %i' % n_expected
if arg_count_err is not None:
msg = '%s() takes %s positional argument(s) (%i given)'
raise TypeError(msg % (name, arg_count_err, len(args)))
# fill in defaults and add the final _kwargs argument,
# then call the function with only positional arguments
n_missing = n_expected - len(args)
args += func.func_defaults[-n_missing - 1:-1]
args += (kwargs,)
return func(*args)
# return the wrapped function
functools.update_wrapper(positional_wrapper, func)
return positional_wrapper
if __name__ == '__main__':
doctest.testmod()
STeVe
--
I'm not *in*-sane. Indeed, I am so far *out* of sane that you appear a
tiny blip on the distant coast of sanity.
--- Bucky Katt, Get Fuzzy
More information about the Python-ideas
mailing list