Commit 24c05bc1 authored by Nick Coghlan's avatar Nick Coghlan

Close issue 17482: don't overwrite __wrapped__

parent 6180a2f4
...@@ -306,8 +306,8 @@ The :mod:`functools` module defines the following functions: ...@@ -306,8 +306,8 @@ The :mod:`functools` module defines the following functions:
To allow access to the original function for introspection and other purposes 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 (e.g. bypassing a caching decorator such as :func:`lru_cache`), this function
automatically adds a __wrapped__ attribute to the wrapper that refers to automatically adds a ``__wrapped__`` attribute to the wrapper that refers to
the original function. the function being wrapped.
The main intended use for this function is in :term:`decorator` functions which 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 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: ...@@ -330,6 +330,11 @@ The :mod:`functools` module defines the following functions:
.. versionchanged:: 3.2 .. versionchanged:: 3.2
Missing attributes no longer trigger an :exc:`AttributeError`. 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) .. decorator:: wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
......
...@@ -315,3 +315,12 @@ that may require changes to your code. ...@@ -315,3 +315,12 @@ that may require changes to your code.
found but improperly structured. If you were catching ImportError before and found but improperly structured. If you were catching ImportError before and
wish to continue to ignore syntax or decoding issues, catch all three wish to continue to ignore syntax or decoding issues, catch all three
exceptions now. 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, ...@@ -55,7 +55,6 @@ def update_wrapper(wrapper,
are updated with the corresponding attribute from the wrapped are updated with the corresponding attribute from the wrapped
function (defaults to functools.WRAPPER_UPDATES) function (defaults to functools.WRAPPER_UPDATES)
""" """
wrapper.__wrapped__ = wrapped
for attr in assigned: for attr in assigned:
try: try:
value = getattr(wrapped, attr) value = getattr(wrapped, attr)
...@@ -65,6 +64,9 @@ def update_wrapper(wrapper, ...@@ -65,6 +64,9 @@ def update_wrapper(wrapper,
setattr(wrapper, attr, value) setattr(wrapper, attr, value)
for attr in updated: for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {})) 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 the wrapper so this can be used as a decorator via partial()
return wrapper return wrapper
......
...@@ -224,19 +224,26 @@ class TestUpdateWrapper(unittest.TestCase): ...@@ -224,19 +224,26 @@ class TestUpdateWrapper(unittest.TestCase):
updated=functools.WRAPPER_UPDATES): updated=functools.WRAPPER_UPDATES):
# Check attributes were assigned # Check attributes were assigned
for name in 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 # Check attributes were updated
for name in updated: for name in updated:
wrapper_attr = getattr(wrapper, name) wrapper_attr = getattr(wrapper, name)
wrapped_attr = getattr(wrapped, name) wrapped_attr = getattr(wrapped, name)
for key in wrapped_attr: 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 _default_update(self):
def f(a:'This is a new annotation'): def f(a:'This is a new annotation'):
"""This is a test""" """This is a test"""
pass pass
f.attr = 'This is also a test' f.attr = 'This is also a test'
f.__wrapped__ = "This is a bald faced lie"
def wrapper(b:'This is the prior annotation'): def wrapper(b:'This is the prior annotation'):
pass pass
functools.update_wrapper(wrapper, f) functools.update_wrapper(wrapper, f)
...@@ -331,14 +338,15 @@ class TestWraps(TestUpdateWrapper): ...@@ -331,14 +338,15 @@ class TestWraps(TestUpdateWrapper):
"""This is a test""" """This is a test"""
pass pass
f.attr = 'This is also a test' f.attr = 'This is also a test'
f.__wrapped__ = "This is still a bald faced lie"
@functools.wraps(f) @functools.wraps(f)
def wrapper(): def wrapper():
pass pass
self.check_wrapper(wrapper, f)
return wrapper, f return wrapper, f
def test_default_update(self): def test_default_update(self):
wrapper, f = self._default_update() wrapper, f = self._default_update()
self.check_wrapper(wrapper, f)
self.assertEqual(wrapper.__name__, 'f') self.assertEqual(wrapper.__name__, 'f')
self.assertEqual(wrapper.__qualname__, f.__qualname__) self.assertEqual(wrapper.__qualname__, f.__qualname__)
self.assertEqual(wrapper.attr, 'This is also a test') self.assertEqual(wrapper.attr, 'This is also a test')
......
...@@ -154,6 +154,10 @@ Core and Builtins ...@@ -154,6 +154,10 @@ Core and Builtins
Library 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 - Issue #18431: The new email header parser now decodes RFC2047 encoded words
in structured headers. in structured headers.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment