classification
Title: Reference inspect.Signature.bind from functools.wraps documentation
Type: enhancement Stage:
Components: Documentation Versions: Python 3.4, Python 3.5
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: docs@python Nosy List: docs@python, ncoghlan, productivememberofsociety666, r.david.murray, rhettinger
Priority: normal Keywords:

Created on 2015-03-24 18:14 by productivememberofsociety666, last changed 2015-03-26 02:06 by productivememberofsociety666.

Messages (16)
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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) Date: 2015-03-26 00:02
Backport proposal: https://github.com/aliles/funcsigs/issues/12
msg239292 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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) * (Python committer) 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...
History
Date User Action Args
2015-03-26 02:06:26productivememberofsociety666setmessages: + msg239300
components: + Documentation, - Library (Lib)
versions: + Python 3.4
2015-03-26 01:13:02productivememberofsociety666setmessages: + msg239298
components: + Library (Lib), - Documentation
versions: - Python 3.4
2015-03-26 01:05:34ncoghlansetassignee: 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:45ncoghlansetmessages: + msg239295
title: Accept a separate "target -> Accept a separate "call_target" parameter to functools.partial
2015-03-26 00:38:03ncoghlansetstatus: 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:12productivememberofsociety666setmessages: + msg239293
2015-03-26 00:28:23ncoghlansetmessages: + msg239292
2015-03-26 00:02:29ncoghlansetmessages: + msg239291
2015-03-25 23:55:12ncoghlansetmessages: + msg239290
2015-03-25 23:50:56ncoghlansetmessages: + msg239289
2015-03-25 23:50:07productivememberofsociety666setmessages: + msg239288
2015-03-25 13:46:11r.david.murraysetmessages: + msg239254
2015-03-25 13:05:36ncoghlansetstatus: open -> closed
resolution: not a bug
messages: + msg239251

stage: resolved
2015-03-25 00:52:08productivememberofsociety666setmessages: + msg239204
2015-03-24 18:57:57r.david.murraysetnosy: + r.david.murray
messages: + msg239168
2015-03-24 18:15:51SilentGhostsetnosy: + rhettinger, ncoghlan

versions: - Python 3.2, Python 3.3, Python 3.4, Python 3.6
2015-03-24 18:14:33productivememberofsociety666create