]> granicus.if.org Git - python/commitdiff
Close issue 17482: don't overwrite __wrapped__
authorNick Coghlan <ncoghlan@gmail.com>
Mon, 15 Jul 2013 11:13:08 +0000 (21:13 +1000)
committerNick Coghlan <ncoghlan@gmail.com>
Mon, 15 Jul 2013 11:13:08 +0000 (21:13 +1000)
Doc/library/functools.rst
Doc/whatsnew/3.4.rst
Lib/functools.py
Lib/test/test_functools.py
Misc/NEWS

index 3d70955c58b588aae89c0d730d42ec0e23049cf6..2f6d9afe0f1e710bd3697219aa2c04d9fd2d4fc4 100644 (file)
@@ -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)
 
index 406894743d9a72ae9079b04cc24d72b534fd5685..2b114f135b1839eeb17e3f0d07ce2bd5e6db8245 100644 (file)
@@ -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.
index 6aa13a2f9a646a586799eb7d43160c8804d15780..19f88c7f0210b96e5a3c7fd26e94d2167cdc597d 100644 (file)
@@ -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
 
index 99dccb096a14044181a123e59068dfcc92e88bfe..ab76efbfefae2aa1cc41884b30afee1185db46e7 100644 (file)
@@ -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')
index 8e175b399fdfd424615f4354dcdc67c3fba017d2..78d55ea81bcbdbd9d1ff861041537f01588459de 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -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.