msg239165 - (view) |
Author: productivememberofsociety666 (productivememberofsociety666) |
Date: 2015-03-24 18:14 |
functools.wraps currently only changes the wrapped function's "superficial" attributes such as docstring or annotations.
But it would be useful to be able to change the function's actual argspec as well so it matches up with the changed annotations (e.g. to get proper error messages when the wrapped and wrapper functions' argspecs don't match up).
To avoid breaking existing code, this could be achieved by adding another argument change_argspec (defaulting to False) to functools.wraps. Of course, the way functools.wraps is implemented now as well as its current documentation would have to be thrown out of the window in order for this to work (at least for the case of change_argspec==True).
There is an existing 3rd party package ( https://pypi.python.org/pypi/decorator ) which has a decorator called "decorator" that does kind of what I'd want functools.wraps to do, but the details of how it is called are different and either way it's embarassing that you have to fall back on a 3rd party library to get functionality that is present but incomplete in Python's standard library.
|
msg239168 - (view) |
Author: R. David Murray (r.david.murray) * |
Date: 2015-03-24 18:57 |
See also issue 15731 (this might be effectively a duplicate of that one, I'm not sure). I believe the idea of incorporating decorator into the stdlib has been brought up in the past.
|
msg239204 - (view) |
Author: productivememberofsociety666 (productivememberofsociety666) |
Date: 2015-03-25 00:52 |
I'm not sure if I understand issue 15731 correctly, but isn't that one just about docstrings and signatures? These are both purely "cosmetic" and don't have an effect on calling behaviour, do they?
This issue wouldn't be a duplicate then.
|
msg239251 - (view) |
Author: Nick Coghlan (ncoghlan) * |
Date: 2015-03-25 13:05 |
Correctly processing a function's signature involves following the __wrapped__ chains to get to the underlying callable (or to a callable that defines an explicitly modified __signature__ value).
inspect.signature follows these chains automatically, and in 3.4+ inspect.getargspec and inspect.getfullargspec have been updated to use inspect.signature internally.
Using these functions will also allow introspection of builtin and extension module functions that have been processed through Argument Clinic to produce appropriate signature information in their docstrings.
If an IDE or other tool is still producing incorrect signature information for functions wrapped with functools.wraps in 3.4+ then that's either a bug in the affected tool, or else a bug report against the inspect module.
|
msg239254 - (view) |
Author: R. David Murray (r.david.murray) * |
Date: 2015-03-25 13:46 |
I think you need to explain exactly what it is you are looking for, because it doesn't seem to me that you can change the argspec of a function. What is it that decorator is doing that is helpful?
|
msg239288 - (view) |
Author: productivememberofsociety666 (productivememberofsociety666) |
Date: 2015-03-25 23:50 |
You're probably right and it's next to impossible to implement what I want in a clean manner. I'll get back to that at the end, but first for completeness's sake two examples to illustrate what this issue is even about.
````
import functools
def wrapper(func):
@functools.wraps(func)
def new_func(*args, **kwargs):
pass
return new_func
def to_be_wrapped(a):
pass
wrapped = wrapper(to_be_wrapped)
wrapped(1) # Ok
wrapped(1, 2, 3) # Also ok, but shouldn't be! Should raise TypeError
````
The second call to wrapped() should fail because it's supposed to be a wrapper around to_be_wrapped(), which only takes 1 argument. But it succeeds because functools.wraps only changes "superficial" function attributes such as its signature or docstring. This creates a mismatch between expected and actual behaviour of wrapped().
Contrast this with how it works when using the decorator package I mentioned:
````
import decorator
def to_be_wrapped(x):
print("f(x) called")
pass
def _wrapper(func, *args, **kwargs):
# Put actual functionality of your decorator here
pass
def wrapper(func):
return decorator.decorator(_wrapper, func)
wrapped = wrapper(to_be_wrapped)
wrapped(1) # Ok, because to_be_wrapped takes exactly 1 argument
wrapped(1, 2, 3) # raises TypeError for too many arguments, as it should
````
Like I said, the details of how it is used are different from those of functools.wraps. But the important thing is that, at the end, wrapped()'s argspec matches that of to_be_wrapped() and an appropriate error is raised by the second call.
Note that this does NOT work via propagation from a call to to_be_wrapped() or anything of the sort, as can be verified by the lack of output to stdout.
Now, the only problem is: I had a look at how this is achieved in the decorator package's source code and if I understand it correctly, they are doing some nasty nasty things with exec() to create a function with an argspec of their choosing at runtime. If this is in fact the only way to do it, I agree that it has no place in the standard library and should only be available via 3rd party libraries. Sorry for wasting your time then.
|
msg239289 - (view) |
Author: Nick Coghlan (ncoghlan) * |
Date: 2015-03-25 23:50 |
I double checked the current behaviour, and rediscovered something I had forgotten: when inspect.getargspec and inspect.getfullargspec were converted to be based on the inspect.signature machinery, we had to decide whether or not to follow wrapper chains to report the underlying signature or not.
We opted to continue reporting the "surface signature" for compatibility with the behaviour of these APIs in previous versions of Python (including introspection tools that handle wrapper chains themselves), while encouraging introspection tools to migrate to using the more capable inspect.signature API instead: https://docs.python.org/3/library/inspect.html#inspect.getfullargspec
If a particular introspection tool reports incorrect signatures in 3.4+, then that's an issue with that particular tool needing to be made wrapper chain aware.
I'll also ping Aaron Iles about potentially bringing https://funcsigs.readthedocs.org/ up to date with the features in the Python 3.4 version of the library, including the helper to traverse wrapper chains correctly: https://docs.python.org/3/library/inspect.html#inspect.unwrap
|
msg239290 - (view) |
Author: Nick Coghlan (ncoghlan) * |
Date: 2015-03-25 23:55 |
Regarding the PyPI decorator module, the difference there is between using a "def f(*args, **kwargs)" wrapper (which requires following wrapper chains to read the signature correctly) and using functools.partial (which reports the correct surface signature directly).
You can define your own wrapper decorators like this to reproduce that behaviour with standard library components:
def wrapper(func):
return functools.wraps(func)(functools.partial(func))
|
msg239291 - (view) |
Author: Nick Coghlan (ncoghlan) * |
Date: 2015-03-26 00:02 |
Backport proposal: https://github.com/aliles/funcsigs/issues/12
|
msg239292 - (view) |
Author: Nick Coghlan (ncoghlan) * |
Date: 2015-03-26 00:28 |
Full example showing the functools.partial based implementation:
>>> def wrapper(func):
... return functools.wraps(func)(functools.partial(func))
...
>>> def to_be_wrapped(x):
... pass
...
>>> import inspect
>>> inspect.getargspec(wrapper(to_be_wrapped))
ArgSpec(args=['x'], varargs=None, keywords=None, defaults=None)
Th usage of functools.partial is also what gives the PyPI decorator module eager validation of the argument structure, even if the original function is never actually called.
When you use the pass-through "*args, **kwds" signature on a wrapper function it really is just a pass-through - even setting __signature__ won't get the *interpreter* to change the way it processes the arguments, as that's baked directly into the compiled code object:
>>> def f(*args, **kwds):
... pass
...
>>> import dis
>>> dis.show_code(f)
Name: f
Filename: <stdin>
Argument count: 0
Kw-only arguments: 0
Number of locals: 2
Stack size: 1
Flags: OPTIMIZED, NEWLOCALS, VARARGS, VARKEYWORDS, NOFREE
Constants:
0: None
Variable names:
0: args
1: kwds
|
msg239293 - (view) |
Author: productivememberofsociety666 (productivememberofsociety666) |
Date: 2015-03-26 00:30 |
def wrapper(func):
return functools.wraps(func)(functools.partial(func))
^ doesn't that just return something that is completely equivalent to func itself? Where do I put the actual wrapper functionality, i.e. code that is executed with each call to the "new" (wrapped) function?
|
msg239294 - (view) |
Author: Nick Coghlan (ncoghlan) * |
Date: 2015-03-26 00:38 |
Right, I started asking myself the same question, and then began poking around in the functools.partial implementation (https://hg.python.org/cpython/file/9be2405385ec/Modules/_functoolsmodule.c#l12)
|
msg239295 - (view) |
Author: Nick Coghlan (ncoghlan) * |
Date: 2015-03-26 00:43 |
Well, there's an expected keyboard shortcut (accidentally hitting Shift-Enter submits the page...)
Anyway, the idea I came up with after looking at that is potentially adjusting functools.partial to accept a "call_target" parameter, which would allow it to *claim* to be a partial object calling "func" (and reporting its signature accordingly) while *actually* calling "wrapped_func" (which may not have accurate signature information).
That underlying feature could then be used as the basis for a new "wraps_validated" API that works by creating a partial object around the underlying function with the call target set to the decorated wrapper function.
|
msg239296 - (view) |
Author: Nick Coghlan (ncoghlan) * |
Date: 2015-03-26 01:05 |
Ah, unfortunately, on closer inspection, even partial itself ends up suffering from the same problem as the current inspect.wraps - validation ultimately happens at the point of calling the original wrapper function, so if you don't actually call it, you won't get any argument validation.
You can use https://docs.python.org/3/library/inspect.html#inspect.Signature.bind to force early validation without actually calling the underlying callable, but that's always going to be substantially slower than "just try it and see if it works". functools is also further down in the stdlib dependency hierarchy than the inspect module so it isn't possible to offer that behaviour without creating a circular dependency.
What would be possible is to point out explicitly that "wraps" only updates introspection metadata, and if you're after eager argument validation (e.g. before queuing a command for delayed execution), then you likely want inspect.Signature.bind.
|
msg239298 - (view) |
Author: productivememberofsociety666 (productivememberofsociety666) |
Date: 2015-03-26 01:13 |
Sounds good! I think the docs on functools.wraps already list precisely which data is updated, but maybe mentioning explicitly that it can not affect the argspec could indeed make it clearer.
Either way, thank you for your help!
|
msg239300 - (view) |
Author: productivememberofsociety666 (productivememberofsociety666) |
Date: 2015-03-26 02:06 |
Sorry, did not mean to change the Components and Versions thingies...
|
|
Date |
User |
Action |
Args |
2022-04-11 14:58:14 | admin | set | github: 67952 |
2021-11-29 17:10:30 | iritkatriel | set | title: Reference inspect.Signature.bind from functools.wraps documentation -> [doc] Reference inspect.Signature.bind from functools.wraps documentation versions:
+ Python 3.11, - Python 3.4, Python 3.5 |
2015-03-26 02:06:26 | productivememberofsociety666 | set | messages:
+ msg239300 components:
+ Documentation, - Library (Lib) versions:
+ Python 3.4 |
2015-03-26 01:13:02 | productivememberofsociety666 | set | messages:
+ msg239298 components:
+ Library (Lib), - Documentation versions:
- Python 3.4 |
2015-03-26 01:05:34 | ncoghlan | set | assignee: docs@python
components:
+ Documentation, - Library (Lib) title: Accept a separate "call_target" parameter to functools.partial -> Reference inspect.Signature.bind from functools.wraps documentation nosy:
+ docs@python versions:
+ Python 3.4 messages:
+ msg239296 |
2015-03-26 00:43:45 | ncoghlan | set | messages:
+ msg239295 title: Accept a separate "target -> Accept a separate "call_target" parameter to functools.partial |
2015-03-26 00:38:03 | ncoghlan | set | status: closed -> open title: functools.wraps should be able to change function argspec as well -> Accept a separate "target messages:
+ msg239294
resolution: not a bug -> stage: resolved -> |
2015-03-26 00:30:12 | productivememberofsociety666 | set | messages:
+ msg239293 |
2015-03-26 00:28:23 | ncoghlan | set | messages:
+ msg239292 |
2015-03-26 00:02:29 | ncoghlan | set | messages:
+ msg239291 |
2015-03-25 23:55:12 | ncoghlan | set | messages:
+ msg239290 |
2015-03-25 23:50:56 | ncoghlan | set | messages:
+ msg239289 |
2015-03-25 23:50:07 | productivememberofsociety666 | set | messages:
+ msg239288 |
2015-03-25 13:46:11 | r.david.murray | set | messages:
+ msg239254 |
2015-03-25 13:05:36 | ncoghlan | set | status: open -> closed resolution: not a bug messages:
+ msg239251
stage: resolved |
2015-03-25 00:52:08 | productivememberofsociety666 | set | messages:
+ msg239204 |
2015-03-24 18:57:57 | r.david.murray | set | nosy:
+ r.david.murray messages:
+ msg239168
|
2015-03-24 18:15:51 | SilentGhost | set | nosy:
+ rhettinger, ncoghlan
versions:
- Python 3.2, Python 3.3, Python 3.4, Python 3.6 |
2015-03-24 18:14:33 | productivememberofsociety666 | create | |