-
-
Notifications
You must be signed in to change notification settings - Fork 34k
bpo-41232: Updated WRAPPER_ASSIGNMENTS #21379
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
**WHAT**
Adding '__defaults__', '__kwdefaults__' to `WRAPPER_ASSIGNMENTS` so that actual defaults can be consistent with signature defaults.
**REASON**
When using `functools.wraps`, the signature claims one set of defaults, but the (wrapped) function uses the original (wrappee) defaults.
Why might that be the desirable default?
**CODE DEMO**
```python
from functools import wraps
from inspect import signature
def g(a: float, b=10):
return a * b
def f(a: int, b=1):
return a * b
assert f(3) == 3
f = wraps(g)(f)
assert str(signature(f)) == '(a: float, b=10)' # signature says that b defaults to 10
assert f.__defaults__ == (1,) # ... but it's a lie!
assert f(3) == 3 != g(3) == 30 # ... and one that can lead to problems!
```
Why is this so? Because `functools.wraps` updates the `__signature__` (including annotations and defaults), `__annotations__`, but not `__defaults__`, which python apparently looks to in order to assign defaults.
One solution is to politely ask wraps to include these defaults.
```python
from functools import wraps, WRAPPER_ASSIGNMENTS, partial
my_wraps = partial(wraps, assigned=(list(WRAPPER_ASSIGNMENTS) + ['__defaults__', '__kwdefaults__']))
def g(a: float, b=10):
return a * b
def f(a: int, b=1):
return a * b
assert f(3) == 3
f = my_wraps(g)(f)
assert f(3) == 30 == g(3)
assert f.__defaults__ == (10,) # ... because now got g defaults!
```
Wouldn't it be better to get this out of the box?
When would I want the defaults that are actually used be different than those mentioned in the signature?
|
Hello, and thanks for your contribution! I'm a bot set up to make sure that the project can legally accept this contribution by verifying everyone involved has signed the PSF contributor agreement (CLA). Recognized GitHub usernameWe couldn't find a bugs.python.org (b.p.o) account corresponding to the following GitHub usernames: This might be simply due to a missing "GitHub Name" entry in one's b.p.o account settings. This is necessary for legal reasons before we can look at this contribution. Please follow the steps outlined in the CPython devguide to rectify this issue. You can check yourself to see if the CLA has been received. Thanks again for the contribution, we look forward to reviewing it! |
|
Further, note that even with the additional 'defaults', and 'kwdefaults', from functools import wraps, WRAPPER_ASSIGNMENTS, partial
# First, I need to add `__defaults__` and `__kwdefaults__` to wraps, because they don't come for free...
my_wraps = partial(wraps, assigned=(list(WRAPPER_ASSIGNMENTS) + ['__defaults__', '__kwdefaults__']))
def g(a: float, b=10):
return a * b
def f(a: int, *, b=1):
return a * b
# all is well (for now)...
assert f(3) == 3
assert g(3) == 30This: my_wraps(g)(f)(3)raises TypeError (missing required positional argument 'b'), expected Note that See for example, that third-party from boltons.funcutils import wraps works as expected. And so do (the very popular) wrapt and decorator packages. Note that the boltons version works for |
|
Tests are failing, so I had a look at them, and my conclusion is that the tests are the mistake here. I'm usint python 3.82 on a Mac. The tests fail here: # In test/test_inspect.py:849
def test_argspec_api_ignores_wrapped(self):
# Issue 20684: low level introspection API must ignore __wrapped__
@functools.wraps(mod.spam)
def ham(x, y):
pass
# Basic check
self.assertArgSpecEquals(ham, ['x', 'y'], formatted='(x, y)') # <--- this what failsNote that def spam(a, /, b, c, d=3, e=4, f=5, *g, **h):
eggs(b + d, c + f)The self.assertEqual(defaults, defaults_e)where In the current version of Here are two code blocks that may help you to investigate: # With the current functools.py
# WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
# '__annotations__', '__defaults__', '__kwdefaults__')
import inspect
import functools
from test import inspect_fodder as mod
@functools.wraps(mod.spam)
def ham(x, y):
pass
assert ham.__defaults__ == None == inspect.getargspec(ham).defaults # With proposed change, adding '__defaults__', '__kwdefaults__' to WRAPPER_ASSIGNMENTS:
# WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
# '__annotations__', '__defaults__', '__kwdefaults__')
import inspect
import functools
from test import inspect_fodder as mod
@functools.wraps(mod.spam)
def ham(x, y):
pass
assert ham.__defaults__ == (3, 4, 5)
assert inspect.getargspec(ham).defaults == (3,) |
|
@thorwhalen, please sign the CLA. Thank you! |
|
@thorwhalen Reminding you what csabella said |
|
Where do I find this? |
|
Please read the message posted by @the-knights-who-say-ni above. Not only you need to sign the CLA, you'll also need to create an account on the bug tracker, and associate your GitHub username there. |
|
This PR is stale because it has been open for 30 days with no activity. If the CLA is not signed within 14 days, it will be closed. See also https://devguide.python.org/pullrequest/#licensing |
|
Closing this stale PR because the CLA is still not signed. |
Important Note: A single test (but replicated in many systems) fails, but I believe the test is not correct (see #21379 (comment) for detals)
WHAT
Adding 'defaults', 'kwdefaults' to
WRAPPER_ASSIGNMENTSso that actual defaults can be consistent with signature defaults.REASON
When using
functools.wraps, the signature claims one set of defaults, but the (wrapped) function uses the original (wrappee) defaults.Why might that be the desirable default?
CODE DEMO
Why is this so? Because
functools.wrapsupdates the__signature__(including annotations and defaults),__annotations__, but not__defaults__, which python apparently looks to in order to assign defaults.One solution is to politely ask wraps to include these defaults.
Wouldn't it be better to get this out of the box?
When would I want the defaults that are actually used be different than those mentioned in the signature?
https://bugs.python.org/issue41232