Document and test the resolution of issue 3445 (tolerate missing attributes in functools.update_wrapper, previously implemented as a side effect of the __annotations__ copying patch) and implement issue 9567 (add a __wrapped__ attribute when using update_wrapper)

This commit is contained in:
Nick Coghlan 2010-08-17 06:17:18 +00:00
parent 632a0c1476
commit 9887683f74
4 changed files with 65 additions and 2 deletions

View File

@ -54,6 +54,10 @@ The :mod:`functools` module defines the following functions:
The wrapped function also has a :attr:`clear` attribute which can be
called (with no arguments) to clear the cache.
The :attr:`__wrapped__` attribute may be used to access the original
function (e.g. to bypass the cache or to apply a different caching
strategy)
A `LRU (least recently used) cache
<http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used>`_
is indicated when the pattern of calls changes over time, such as
@ -141,12 +145,31 @@ The :mod:`functools` module defines the following functions:
documentation string) and *WRAPPER_UPDATES* (which updates the wrapper
function's *__dict__*, i.e. the instance dictionary).
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 the wrapped that refers to
the original function.
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
not updated, the metadata of the returned function will reflect the wrapper
definition rather than the original function definition, which is typically less
than helpful.
:func:`update_wrapper` may be used with callables other than functions. Any
attributes named in *assigned* or *updated* that are missing from the object
being wrapped are ignored (i.e. this function will not attempt to set them
on the wrapper function). :exc:`AttributeError` is still raised if the
wrapper function itself is missing any attributes named in *updated*.
.. versionadded:: 3.2
Automatic addition of the __wrapped__ attribute
.. versionadded:: 3.2
Copying of the __annotations__ attribute by default
.. versionchanged:: 3.2
Missing attributes no longer trigger an AttributeError
.. decorator:: wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)

View File

@ -38,9 +38,14 @@ 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:
if hasattr(wrapped, attr):
setattr(wrapper, attr, getattr(wrapped, attr))
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
# Return the wrapper so this can be used as a decorator via partial()

View File

@ -194,6 +194,7 @@ class TestUpdateWrapper(unittest.TestCase):
def test_default_update(self):
wrapper, f = self._default_update()
self.check_wrapper(wrapper, f)
self.assertIs(wrapper.__wrapped__, f)
self.assertEqual(wrapper.__name__, 'f')
self.assertEqual(wrapper.attr, 'This is also a test')
self.assertEqual(wrapper.__annotations__['a'], 'This is a new annotation')
@ -236,6 +237,28 @@ class TestUpdateWrapper(unittest.TestCase):
self.assertEqual(wrapper.attr, 'This is a different test')
self.assertEqual(wrapper.dict_attr, f.dict_attr)
def test_missing_attributes(self):
def f():
pass
def wrapper():
pass
wrapper.dict_attr = {}
assign = ('attr',)
update = ('dict_attr',)
# Missing attributes on wrapped object are ignored
functools.update_wrapper(wrapper, f, assign, update)
self.assertNotIn('attr', wrapper.__dict__)
self.assertEqual(wrapper.dict_attr, {})
# Wrapper must have expected attributes for updating
del wrapper.dict_attr
with self.assertRaises(AttributeError):
functools.update_wrapper(wrapper, f, assign, update)
wrapper.dict_attr = 1
with self.assertRaises(AttributeError):
functools.update_wrapper(wrapper, f, assign, update)
@unittest.skipIf(sys.flags.optimize >= 2,
"Docstrings are omitted with -O2 and above")
def test_builtin_update(self):
# Test for bug #1576241
def wrapper():
@ -495,6 +518,12 @@ class TestLRU(unittest.TestCase):
self.assertEqual(f.hits, 0)
self.assertEqual(f.misses, 1)
# Test bypassing the cache
self.assertIs(f.__wrapped__, orig)
f.__wrapped__(x, y)
self.assertEqual(f.hits, 0)
self.assertEqual(f.misses, 1)
# test size zero (which means "never-cache")
@functools.lru_cache(0)
def f():

View File

@ -90,6 +90,12 @@ Extensions
Library
-------
- Issue #9567: functools.update_wrapper now adds a __wrapped__ attribute
pointing to the original callable
- Issue #3445: functools.update_wrapper now tolerates missing attributes
on wrapped callables
- Issue #5867: Add abc.abstractclassmethod and abc.abstractstaticmethod.
- Issue #9605: posix.getlogin() decodes the username with file filesystem