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:
parent
632a0c1476
commit
9887683f74
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue