As stated by user2357112 in the comments, you can use inspect.signature.bind to obtain a dictionary of the arguments and keyword arguments.
Your decorator could then be something like:
class my_decorator:
def __init__(self, option=None):
self.option = option
def __call__(self, fn):
@functools.wraps(fn)
def decorated(*args, **kwargs):
fn_args = inspect.signature(fn).bind(*args, **kwargs)
# Then if you really needed to make them as variables
# rather than a dict for a reason I can't imagine
locals.update(fn_args)
return fn(*args, **kwargs)
return decorated
If like me you need to keep maintaining python 2.7 and python 3 compatibility, there is a similar function inspect.getcallargs which works for both, even though it's deprecated post 3.5
Answer from Erwan Leroy on Stack OverflowVideos
As stated by user2357112 in the comments, you can use inspect.signature.bind to obtain a dictionary of the arguments and keyword arguments.
Your decorator could then be something like:
class my_decorator:
def __init__(self, option=None):
self.option = option
def __call__(self, fn):
@functools.wraps(fn)
def decorated(*args, **kwargs):
fn_args = inspect.signature(fn).bind(*args, **kwargs)
# Then if you really needed to make them as variables
# rather than a dict for a reason I can't imagine
locals.update(fn_args)
return fn(*args, **kwargs)
return decorated
If like me you need to keep maintaining python 2.7 and python 3 compatibility, there is a similar function inspect.getcallargs which works for both, even though it's deprecated post 3.5
Well, considerable effort researching existing solutions turned nothing up, so I have worked up a solution which works, though I'm not 100% happy with (as I'm using 'exec' which is optional really, the main trick inspect.signature.parameters that helps us interpret incoming arguments in the context of the decorated function's expectations):
from inspect import signature
... then inside 'decorated' ...
allargs = signature(fn).parameters
# allargs is an OrderedDict, the keys of which are the arguments of fn.
# We build therfrom, a list of arguments we are seeking in args and kwargs
seeking = list(allargs.keys())
found = {}
# Methods by convention have the first argument "self". Even if it's
# not a method, setting a local variable 'self' is fraught with issues
# And so we need to remap it. Also if we've explicitly decorated a method
# we remap the first argument. We call it 'selfie' interanally, but in
# provided key_patterns, accept 'self' as a reference.
if seeking[0] == 'self' or is_method:
selfie = args[0]
found['selfie'] = selfie
seeking.pop(0)
sarg = 1
is_method = True
else:
sarg = 0
# For classifying arguments see:
# https://docs.python.org/3/library/inspect.html#inspect.Parameter
#
# We start by consuming all the args.
if seeking:
for arg in args[sarg:]:
# This should never happen, but if someone calls the decorated function
# with more args than the original function can accept that's clearly
# an erroneous call.
if len(seeking) == 0:
raise TooManyArgs(f"Decorated function has been called with {len(args)} positional arguments when only {len(allargs)} args are accepted by the decorated function. ")
# Set a local variable, feigning the conditions that fn would see
# if seeking[0] is 'self' this exhibits odd dysfunctional behaviour
# and so above we mapped 'self' to 'selfie' internall of this decorator.
found[seeking[0]] = arg
exec(f"{seeking[0]} = arg")
seeking.pop(0)
# If we did not find all that we seek by consuming args, consume kwargs
if seeking:
for kwarg, val in kwargs.items():
if kwarg in seeking:
# Should never happen, but if someone calls the decorated function
# with more args than the original function can accept that's clearly
# an erroneous call.
if len(seeking) == 0:
raise TooManyKwargs(f"Decorated function has been called with {len(kwargs)} keyword arguments after {len(args)} positional arguments when only {len(allargs)} args are accepted by the decorated function. ")
arg = seeking.index(kwarg)
found[seeking[arg]] = val
exec(f"{seeking[arg]} = val")
seeking.pop(arg)
if seeking:
# Any that remain we can check for default values
for arg in seeking:
props = allargs[arg]
if props.default != props.empty:
pos = seeking.index(arg)
found[seeking[pos]] = props.default
exec(f"{seeking[pos]} = props.default")
seeking.pop(pos)
# If any remain, then clearly not all the argument fn needs have been supplied
# to its decorated version.
if seeking:
raise TooFewArgs(f"Decorated function expects arguments ({', '.join(seeking)}), which it was not called with.")
That calls on a few custom exceptions:
class TooManyArgs(Exception):
pass
class TooManyKwargs(Exception):
pass
class TooFewArgs(Exception):
pass
but works fine. The upshot is local variables in decorated which are just as they would be seen inside the decorated function.
While a spot need, it is utilised here:
It is used here:
https://pypi.org/project/django-cache-memoized/
and in action here:
https://github.com/bernd-wechner/django-cache-memoized/blob/master/src/django_cache_memoized/__init__.py
(We'll hope the specious vote to close a very sensible, well researched question doesn't find 2 supporters. It's a very interesting question IMHO and seems doable, though a canonical approach would be preferred.)
import inspect
def foo(a, b, x='blah'):
pass
print(inspect.signature(foo))
# (a, b, x='blah')
Python 3.5+ recommends inspect.signature().
Arguably the easiest way to find the signature for a function would be help(function):
>>> def function(arg1, arg2="foo", *args, **kwargs): pass
>>> help(function)
Help on function function in module __main__:
function(arg1, arg2='foo', *args, **kwargs)
Also, in Python 3 a method was added to the inspect module called signature, which is designed to represent the signature of a callable object and its return annotation:
>>> from inspect import signature
>>> def foo(a, *, b:int, **kwargs):
... pass
>>> sig = signature(foo)
>>> str(sig)
'(a, *, b:int, **kwargs)'
>>> str(sig.parameters['b'])
'b:int'
>>> sig.parameters['b'].annotation
<class 'int'>
Note that inspect.getcallargs has been deprecated since Python 3.5, per documentation.
Use inspect.signature.bind instead going forward:
from inspect import signature
def extract_extra_args_and_kwargs(f, *args, **kwargs):
sig = signature(f)
bound = sig.bind(*args, **kwargs)
return bound.arguments['args'], bound.arguments['kwargs']
def fn(a, b, c='default', *args, **kwargs):
print(f'a={a}, b={b}, c={c}')
print(f'args={args}')
print(f'kwargs={kwargs}')
args = (1, 2, 3, 4)
kwargs = {'extra_kw': 5}
print(extract_extra_args_and_kwargs(fn, *args, **kwargs))
This outputs:
((4,), {'extra_kw': 5})
This is inspect.getcallargs:
>>> import inspect
>>> def fn(a, b, c='default', *args, **kwargs):
... pass
...
>>> inspect.getcallargs(fn, 1,2,3,4, extra_kw=5)
{'a': 1, 'b': 2, 'c': 3, 'args': (4,), 'kwargs': {'extra_kw': 5}}
Your extract_extra_args_kwargs as defined in your question would be a simple wrapper.
def extract_extra_args_kwargs(fn, args, kwargs):
x = inspect.getcallargs(fn, *args, **kwargs)
return x['args'], x['kwargs']