Close issue 17482: don't overwrite __wrapped__
This commit is contained in:
parent
6180a2f453
commit
24c05bc154
|
@ -306,8 +306,8 @@ The :mod:`functools` module defines the following functions:
|
|||
|
||||
To allow access to the original function for introspection and other purposes
|
||||
(e.g. bypassing a caching decorator such as :func:`lru_cache`), this function
|
||||
automatically adds a __wrapped__ attribute to the wrapper that refers to
|
||||
the original function.
|
||||
automatically adds a ``__wrapped__`` attribute to the wrapper that refers to
|
||||
the function being wrapped.
|
||||
|
||||
The main intended use for this function is in :term:`decorator` functions which
|
||||
wrap the decorated function and return the wrapper. If the wrapper function is
|
||||
|
@ -330,6 +330,11 @@ The :mod:`functools` module defines the following functions:
|
|||
.. versionchanged:: 3.2
|
||||
Missing attributes no longer trigger an :exc:`AttributeError`.
|
||||
|
||||
.. versionchanged:: 3.4
|
||||
The ``__wrapped__`` attribute now always refers to the wrapped
|
||||
function, even if that function defined a ``__wrapped__`` attribute.
|
||||
(see :issue:`17482`)
|
||||
|
||||
|
||||
.. decorator:: wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
|
||||
|
||||
|
|
|
@ -315,3 +315,12 @@ that may require changes to your code.
|
|||
found but improperly structured. If you were catching ImportError before and
|
||||
wish to continue to ignore syntax or decoding issues, catch all three
|
||||
exceptions now.
|
||||
|
||||
* :func:`functools.update_wrapper` and :func:`functools.wraps` now correctly
|
||||
set the ``__wrapped__`` attribute even if the wrapped function had a
|
||||
wrapped attribute set. This means ``__wrapped__`` attributes now correctly
|
||||
link a stack of decorated functions rather than every ``__wrapped__``
|
||||
attribute in the chain referring to the innermost function. Introspection
|
||||
libraries that assumed the previous behaviour was intentional will need to
|
||||
be updated to walk the chain of ``__wrapped__`` attributes to find the
|
||||
innermost function.
|
||||
|
|
|
@ -55,7 +55,6 @@ def update_wrapper(wrapper,
|
|||
are updated with the corresponding attribute from the wrapped
|
||||
function (defaults to functools.WRAPPER_UPDATES)
|
||||
"""
|
||||
wrapper.__wrapped__ = wrapped
|
||||
for attr in assigned:
|
||||
try:
|
||||
value = getattr(wrapped, attr)
|
||||
|
@ -65,6 +64,9 @@ def update_wrapper(wrapper,
|
|||
setattr(wrapper, attr, value)
|
||||
for attr in updated:
|
||||
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
|
||||
# Issue #17482: set __wrapped__ last so we don't inadvertently copy it
|
||||
# from the wrapped function when updating __dict__
|
||||
wrapper.__wrapped__ = wrapped
|
||||
# Return the wrapper so this can be used as a decorator via partial()
|
||||
return wrapper
|
||||
|
||||
|
|
|
@ -224,19 +224,26 @@ class TestUpdateWrapper(unittest.TestCase):
|
|||
updated=functools.WRAPPER_UPDATES):
|
||||
# Check attributes were assigned
|
||||
for name in assigned:
|
||||
self.assertTrue(getattr(wrapper, name) is getattr(wrapped, name))
|
||||
self.assertIs(getattr(wrapper, name), getattr(wrapped, name))
|
||||
# Check attributes were updated
|
||||
for name in updated:
|
||||
wrapper_attr = getattr(wrapper, name)
|
||||
wrapped_attr = getattr(wrapped, name)
|
||||
for key in wrapped_attr:
|
||||
self.assertTrue(wrapped_attr[key] is wrapper_attr[key])
|
||||
if name == "__dict__" and key == "__wrapped__":
|
||||
# __wrapped__ is overwritten by the update code
|
||||
continue
|
||||
self.assertIs(wrapped_attr[key], wrapper_attr[key])
|
||||
# Check __wrapped__
|
||||
self.assertIs(wrapper.__wrapped__, wrapped)
|
||||
|
||||
|
||||
def _default_update(self):
|
||||
def f(a:'This is a new annotation'):
|
||||
"""This is a test"""
|
||||
pass
|
||||
f.attr = 'This is also a test'
|
||||
f.__wrapped__ = "This is a bald faced lie"
|
||||
def wrapper(b:'This is the prior annotation'):
|
||||
pass
|
||||
functools.update_wrapper(wrapper, f)
|
||||
|
@ -331,14 +338,15 @@ class TestWraps(TestUpdateWrapper):
|
|||
"""This is a test"""
|
||||
pass
|
||||
f.attr = 'This is also a test'
|
||||
f.__wrapped__ = "This is still a bald faced lie"
|
||||
@functools.wraps(f)
|
||||
def wrapper():
|
||||
pass
|
||||
self.check_wrapper(wrapper, f)
|
||||
return wrapper, f
|
||||
|
||||
def test_default_update(self):
|
||||
wrapper, f = self._default_update()
|
||||
self.check_wrapper(wrapper, f)
|
||||
self.assertEqual(wrapper.__name__, 'f')
|
||||
self.assertEqual(wrapper.__qualname__, f.__qualname__)
|
||||
self.assertEqual(wrapper.attr, 'This is also a test')
|
||||
|
|
|
@ -154,6 +154,10 @@ Core and Builtins
|
|||
Library
|
||||
-------
|
||||
|
||||
- Issue #17482: functools.update_wrapper (and functools.wraps) now set the
|
||||
__wrapped__ attribute correctly even if the underlying function has a
|
||||
__wrapped__ attribute set.
|
||||
|
||||
- Issue #18431: The new email header parser now decodes RFC2047 encoded words
|
||||
in structured headers.
|
||||
|
||||
|
|
Loading…
Reference in New Issue