GrahamDumpleton / wrapt

A Python module for decorators, wrappers and monkey patching.
BSD 2-Clause "Simplified" License
2.06k stars 231 forks source link

NameError in signature-changing decorator with non-builtins #147

Closed tlambert03 closed 4 years ago

tlambert03 commented 4 years ago

I'm trying to create an adaptor decorator that will add/modify the annotation of an argument following the general pattern in the docs. However, if I try to change the annotation to anything but a builtin class, I get a NameError saying the module is not defined. Here's a toy example using requests.Response:

import inspect
import wrapt
import requests

def argspec_factory(wrapped):
    args, *rest, annotations = inspect.getfullargspec(wrapped)
    # change annotation of first argument
    annotations[args[0]] = requests.Response
    return inspect.FullArgSpec(args, *rest, annotations)

@wrapt.decorator(adapter=wrapt.adapter_factory(argspec_factory))
def my_adapter(wrapped, instance, args, kwargs):
    return wrapped(*args, **kwargs)

@my_adapter
def test(arg):
    pass

raises: NameError: name 'requests' is not defined

Traceback ``` Traceback (most recent call last): File "examples/test.py", line 18, in def test(arg): File "...python3.8/site-packages/wrapt/decorators.py", line 392, in _wrapper return _build(target_wrapped, target_wrapper, _enabled, adapter) File "...python3.8/site-packages/wrapt/decorators.py", line 209, in _build exec_('def adapter{}: pass'.format(adapter), ns, ns) File "", line 1, in NameError: name 'requests' is not defined ```

looking through the source code, it seems as if this is by design, as an empty namespace is handed to the exec_ function ... presumably for security reasons. https://github.com/GrahamDumpleton/wrapt/blob/0c98567e78837bb39b2a498c13a0eb1403266bee/src/wrapt/decorators.py#L206-L209

do you have a suggestion for how I could accomplish what I'm trying to do? in the example above, I would like to have the wrapped function show this signature:

def test(arg: requests.Response): ...

thanks!

GrahamDumpleton commented 4 years ago

Totally untested, but maybe something like:

import inspect
import wrapt
import requests

def argspec_factory(wrapped):
    args, *rest, annotations = inspect.getfullargspec(wrapped)
    # change annotation of first argument
    annotations[args[0]] = requests.Response
    argspec = inspect.FullArgSpec(args, *rest, annotations)
    ns = {}
    exec('def adapter{}: pass'.format(inspect.formatargspec(*argspec)), ns, {'requests': requests})
    return ns['adapter']

@wrapt.decorator(adapter=wrapt.adapter_factory(argspec_factory))
def my_adapter(wrapped, instance, args, kwargs):
    return wrapped(*args, **kwargs)

@my_adapter
def test(arg):
    pass

In other words, you do the work to construct the fake function object with the required prototype.

tlambert03 commented 4 years ago

Thanks! Great to have an example. That should get me started. I was also coming to the realization that I could perhaps use typing.ForwardRef, and deal with it on the introspection side?

tlambert03 commented 4 years ago

I needed to make some slight modifications to this example, since 'adapter' was not found in ns unless I changed the call to exec. Here's what I ended up with in case anyone stumbles across this. (I also use Signature Objects instead of ArgSpec tuples)

def argspec_factory(wrapped):
    sig = inspect.signature(wrapped)
    p1, *rest = sig.parameters.values()
    p1 = p1.replace(annotation=requests.Response)
    new_sig = sig.replace(parameters=[p1, *rest])
    ns = {"requests": requests}
    exec(f"def adapter{new_sig}: pass", ns, ns)
    return ns["adapter"]

thanks for the help @GrahamDumpleton