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
.. 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)
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.
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)
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
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)
"""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')
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.