'Can I use inspect.signature to interpret a decorator's (*args, **kwargs) as the decorated function's variables would be?
Rather a tricky question to phrase. I hope I did all right. To clarify, here is a sample decorator:
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).parameters
# Code that creates local variables by taking the values that
# arrived in *args and **kwargs in exactly the same way that
# they are inside fn proper.
return fn(*args, **kwargs)
return decorated
If that is not clear as yet, we can add an explanatory level of detail to the proposal by looking at an application of said decorator:
@my_decorator()
def my_function(arg1, arg2, arg3, kwarg1=val1, kwarg2=val2)
# In here we see the variables arg1, arg2, arg3, kwarg1 and kwarg2
The challenge is, can we see create variables arg1, arg2, arg3, kwarg1 and kwarg2 inside of def decorated(*args, **kwargs): by using fn_args.
I have a gut feel this can be done, but it would take some deep thinking, and I wonder if this isn't a wheel already invented. By which I mean, perhaps there is already a canonical method of applying a function signature to anonymous *args, **kwargs to produce local variables in accord with that signature.
I would like to think so. Namely, that I am not the first person to have discovered such a wish/desire. Of course, there is a real risk that I am. As general utility for that seems low to me. I have a very specific application in which the option passed in is a format string (of the style "{arg1}, {arg2} ...") that uses names from the parameter list of the decorated function.
Solution 1:[1]
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:
(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.)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |
