positional only arguments decorator

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

On 21 May 2007, at 19:30, Steven Bethard wrote:
I propose a slightly different solution. It may be a bit of a hack, but I don't see why it should not be safe. This solution allows you to use * and ** in order to write a definition like:
Or:
Without * or ** it gives the following behaviour:
This is achieved by 'flattening' the function first. Here is the code: import new import inspect def flattened(f): """Changes a function f(...[, *y] [, **z]) to f(...[, y=None] [, z=None])""" fc = f.func_code defaults = f.func_defaults flags = fc.co_flags argcount = fc.co_argcount has_varargs = flags & 4 if has_varargs: flags ^= 4 argcount += 1 if defaults: defaults += (None,) has_varkwargs = flags & 8 if has_varkwargs: flags ^= 8 argcount += 1 if defaults: defaults += (None,) flat_code = new.code(argcount, fc.co_nlocals, fc.co_stacksize, flags, fc.co_code, fc.co_consts, fc.co_names, fc.co_varnames, fc.co_filename, fc.co_name, fc.co_firstlineno, fc.co_lnotab) flat_f = new.function(flat_code, f.func_globals, f.func_name, defaults, f.func_closure) return flat_f def posonly(f): "posonly(f) makes the arguments of f positional only" f_args, f_varargs, f_varkwargs, f_defaults = inspect.getargspec(f) f_nargs = len(f_args) f_name = f.__name__ flat_f = flattened(f) def posf(*args, **kwargs): if f_varkwargs: kwargs = {f_varkwargs: kwargs} elif kwargs: raise TypeError('posonly function %s() got keyword argument' % f_name) if f_varargs: kwargs[f_varargs] = args[f_nargs:] args = args[:f_nargs] return flat_f(*args, **kwargs) posf.__name__ = f_name return posf -- Arnaud

On 27 May 2007, at 16:37, Steven Bethard wrote:
Wait! I think I've got a devastatingly simple solution. I've thought about this while looking at the PyEval_EvalCodeEx() function in Python/ceval.c. It turns out that the 'co_varnames' attribute of a code object is only used in order to 'slot in' arguments passed as keywords. So by adding a '@' to the names we make these arguments positional only (one could choose any decoration including a character which is illegal in identifiers). This method has two advantages: * there is **no** overhead at all in calling a positional only function. * the function keeps its original signature (positional only arguments are flagged with the trailing '@'). There is one 'drawback' (which could be considered a useful feature): * if x is positional, f(**{'x@':1}) will work. Here is the code. ------------------------------------------------------------------ from types import CodeType code_args = ( 'argcount', 'nlocals', 'stacksize', 'flags', 'code', 'consts', 'names', 'varnames', 'filename', 'name', 'firstlineno', 'lnotab', 'freevars', 'cellvars' ) def copy_code(code_obj, **kwargs): "Make a copy of a code object, maybe changing some attributes" for arg in code_args: if not kwargs.has_key(arg): kwargs[arg] = getattr(code_obj, 'co_%s' % arg) return CodeType(*map(kwargs.__getitem__, code_args)) def posonly(f): code = f.func_code varnames, nargs = code.co_varnames, code.co_argcount varnames = ( tuple(v+'@' for v in varnames[:nargs]) + varnames[nargs:] ) f.func_code = copy_code(code, varnames = varnames) return f ------------------------------------------------------------------ That's it! Example:
update(self@, container@=None, **kwargs)
-- Arnaud

On 27 May 2007, at 10:03, Arnaud Delobelle wrote: [...]
Well it turns out there can be problems as my code does not initialise the co_freevars and co_cellvars of the code objects it creates. I blame it on the python doc (;-) which shows the signature of new.code as: code(argcount, nlocals, stacksize, flags, codestring, constants, names, varnames, filename, name, firstlineno, lnotab) Missing out on the last two optional arguments (freevars and cellvars). I only found that out by looking at Object/codeobject.c (which shows how clever I am, since I could have simply typed 'help (code.new)'). Moreover the file Lib/new.py says that the 'new' module is now deprecated, and that 'import types' should be used instead. So here is the modified code. The 'posonly' decorator is used in exactly the same way as I've described in my previous email. Compared with the previous version, I have tried to reduce the overhead caused by making a function 'posonly' to a minimum (this is why there are three definitions of 'posf'). Note that when a posonly function is called incorrectly , there should be a helpful error message, which is the 'builtin' one most of the time. Please tell me if it is not appropriate to send code to the list as I'm doing. ----------------------------------------------- from types import CodeType, FunctionType import inspect code_args = ( 'argcount', 'nlocals', 'stacksize', 'flags', 'code', 'consts', 'names', 'varnames', 'filename', 'name', 'firstlineno', 'lnotab', 'freevars', 'cellvars' ) function_args = ('code', 'globals', 'name', 'defaults', 'closure') def copy_code(code_obj, **kwargs): "Make a copy of a code object, maybe changing some attributes" for arg in code_args: if not kwargs.has_key(arg): kwargs[arg] = getattr(code_obj, 'co_%s' % arg) return CodeType(*map(kwargs.__getitem__, code_args)) def copy_function(function_obj, **kwargs): "Make a copy of a function object, maybe changing some attributes" for arg in function_args: if not kwargs.has_key(arg): kwargs[arg] = getattr(function_obj, 'func_%s' % arg) return FunctionType(*map(kwargs.__getitem__, function_args)) def rename_function(f, name): f.__name__ = name f.func_code = copy_code(f.func_code, name=name) def flattened(f): """Changes a function f(...[, *y] [, **z]) to f(...[, y=None] [, z=None])""" fc = f.func_code defaults = f.func_defaults flags = fc.co_flags argcount = fc.co_argcount for bit in 4, 8: if flags & bit: flags ^= bit argcount += 1 if defaults: defaults += (None,) flat_code = copy_code(fc, argcount=argcount, flags=flags) flat_f = copy_function(f, code=flat_code, defaults=defaults) return flat_f def posonly(f): "posonly(f) makes the arguments of f positional only" f_args, f_varargs, f_varkwargs, f_defaults = inspect.getargspec(f) f_nargs = len(f_args) f_name = f.__name__ flat_f = flattened(f) if f_varargs and f_varkwargs: def posf(*args, **kwargs): kwargs = {f_varkwargs: kwargs} kwargs[f_varargs] = args[f_nargs:] args = args[:f_nargs] return flat_f(*args, **kwargs) elif f_varkwargs: # and not f_varargs def posf(*args, **kwargs): kwargs = {f_varkwargs: kwargs} if len(args) > f_nargs: msg = '%s() takes at most %i arguments (%i given)' raise TypeError(msg % (f_name, f_nargs, len(args))) return flat_f(*args, **kwargs) else: # not f_varkwargs def posf(*args): return f(*args) rename_function(posf, f_name) return posf ----------------------------------------------- -- Arnaud

On 21 May 2007, at 19:30, Steven Bethard wrote:
I propose a slightly different solution. It may be a bit of a hack, but I don't see why it should not be safe. This solution allows you to use * and ** in order to write a definition like:
Or:
Without * or ** it gives the following behaviour:
This is achieved by 'flattening' the function first. Here is the code: import new import inspect def flattened(f): """Changes a function f(...[, *y] [, **z]) to f(...[, y=None] [, z=None])""" fc = f.func_code defaults = f.func_defaults flags = fc.co_flags argcount = fc.co_argcount has_varargs = flags & 4 if has_varargs: flags ^= 4 argcount += 1 if defaults: defaults += (None,) has_varkwargs = flags & 8 if has_varkwargs: flags ^= 8 argcount += 1 if defaults: defaults += (None,) flat_code = new.code(argcount, fc.co_nlocals, fc.co_stacksize, flags, fc.co_code, fc.co_consts, fc.co_names, fc.co_varnames, fc.co_filename, fc.co_name, fc.co_firstlineno, fc.co_lnotab) flat_f = new.function(flat_code, f.func_globals, f.func_name, defaults, f.func_closure) return flat_f def posonly(f): "posonly(f) makes the arguments of f positional only" f_args, f_varargs, f_varkwargs, f_defaults = inspect.getargspec(f) f_nargs = len(f_args) f_name = f.__name__ flat_f = flattened(f) def posf(*args, **kwargs): if f_varkwargs: kwargs = {f_varkwargs: kwargs} elif kwargs: raise TypeError('posonly function %s() got keyword argument' % f_name) if f_varargs: kwargs[f_varargs] = args[f_nargs:] args = args[:f_nargs] return flat_f(*args, **kwargs) posf.__name__ = f_name return posf -- Arnaud

On 27 May 2007, at 16:37, Steven Bethard wrote:
Wait! I think I've got a devastatingly simple solution. I've thought about this while looking at the PyEval_EvalCodeEx() function in Python/ceval.c. It turns out that the 'co_varnames' attribute of a code object is only used in order to 'slot in' arguments passed as keywords. So by adding a '@' to the names we make these arguments positional only (one could choose any decoration including a character which is illegal in identifiers). This method has two advantages: * there is **no** overhead at all in calling a positional only function. * the function keeps its original signature (positional only arguments are flagged with the trailing '@'). There is one 'drawback' (which could be considered a useful feature): * if x is positional, f(**{'x@':1}) will work. Here is the code. ------------------------------------------------------------------ from types import CodeType code_args = ( 'argcount', 'nlocals', 'stacksize', 'flags', 'code', 'consts', 'names', 'varnames', 'filename', 'name', 'firstlineno', 'lnotab', 'freevars', 'cellvars' ) def copy_code(code_obj, **kwargs): "Make a copy of a code object, maybe changing some attributes" for arg in code_args: if not kwargs.has_key(arg): kwargs[arg] = getattr(code_obj, 'co_%s' % arg) return CodeType(*map(kwargs.__getitem__, code_args)) def posonly(f): code = f.func_code varnames, nargs = code.co_varnames, code.co_argcount varnames = ( tuple(v+'@' for v in varnames[:nargs]) + varnames[nargs:] ) f.func_code = copy_code(code, varnames = varnames) return f ------------------------------------------------------------------ That's it! Example:
update(self@, container@=None, **kwargs)
-- Arnaud

On 27 May 2007, at 10:03, Arnaud Delobelle wrote: [...]
Well it turns out there can be problems as my code does not initialise the co_freevars and co_cellvars of the code objects it creates. I blame it on the python doc (;-) which shows the signature of new.code as: code(argcount, nlocals, stacksize, flags, codestring, constants, names, varnames, filename, name, firstlineno, lnotab) Missing out on the last two optional arguments (freevars and cellvars). I only found that out by looking at Object/codeobject.c (which shows how clever I am, since I could have simply typed 'help (code.new)'). Moreover the file Lib/new.py says that the 'new' module is now deprecated, and that 'import types' should be used instead. So here is the modified code. The 'posonly' decorator is used in exactly the same way as I've described in my previous email. Compared with the previous version, I have tried to reduce the overhead caused by making a function 'posonly' to a minimum (this is why there are three definitions of 'posf'). Note that when a posonly function is called incorrectly , there should be a helpful error message, which is the 'builtin' one most of the time. Please tell me if it is not appropriate to send code to the list as I'm doing. ----------------------------------------------- from types import CodeType, FunctionType import inspect code_args = ( 'argcount', 'nlocals', 'stacksize', 'flags', 'code', 'consts', 'names', 'varnames', 'filename', 'name', 'firstlineno', 'lnotab', 'freevars', 'cellvars' ) function_args = ('code', 'globals', 'name', 'defaults', 'closure') def copy_code(code_obj, **kwargs): "Make a copy of a code object, maybe changing some attributes" for arg in code_args: if not kwargs.has_key(arg): kwargs[arg] = getattr(code_obj, 'co_%s' % arg) return CodeType(*map(kwargs.__getitem__, code_args)) def copy_function(function_obj, **kwargs): "Make a copy of a function object, maybe changing some attributes" for arg in function_args: if not kwargs.has_key(arg): kwargs[arg] = getattr(function_obj, 'func_%s' % arg) return FunctionType(*map(kwargs.__getitem__, function_args)) def rename_function(f, name): f.__name__ = name f.func_code = copy_code(f.func_code, name=name) def flattened(f): """Changes a function f(...[, *y] [, **z]) to f(...[, y=None] [, z=None])""" fc = f.func_code defaults = f.func_defaults flags = fc.co_flags argcount = fc.co_argcount for bit in 4, 8: if flags & bit: flags ^= bit argcount += 1 if defaults: defaults += (None,) flat_code = copy_code(fc, argcount=argcount, flags=flags) flat_f = copy_function(f, code=flat_code, defaults=defaults) return flat_f def posonly(f): "posonly(f) makes the arguments of f positional only" f_args, f_varargs, f_varkwargs, f_defaults = inspect.getargspec(f) f_nargs = len(f_args) f_name = f.__name__ flat_f = flattened(f) if f_varargs and f_varkwargs: def posf(*args, **kwargs): kwargs = {f_varkwargs: kwargs} kwargs[f_varargs] = args[f_nargs:] args = args[:f_nargs] return flat_f(*args, **kwargs) elif f_varkwargs: # and not f_varargs def posf(*args, **kwargs): kwargs = {f_varkwargs: kwargs} if len(args) > f_nargs: msg = '%s() takes at most %i arguments (%i given)' raise TypeError(msg % (f_name, f_nargs, len(args))) return flat_f(*args, **kwargs) else: # not f_varkwargs def posf(*args): return f(*args) rename_function(posf, f_name) return posf ----------------------------------------------- -- Arnaud
participants (2)
-
Arnaud Delobelle
-
Steven Bethard