Commit e8c45d6d authored by Nick Coghlan's avatar Nick Coghlan

Close #13266: Add inspect.unwrap

Initial patch by Daniel Urban and Aaron Iles
parent 77578204
......@@ -797,6 +797,23 @@ Classes and functions
.. versionadded:: 3.3
.. function:: unwrap(func, *, stop=None)
Get the object wrapped by *func*. It follows the chain of :attr:`__wrapped__`
attributes returning the last object in the chain.
*stop* is an optional callback accepting an object in the wrapper chain
as its sole argument that allows the unwrapping to be terminated early if
the callback returns a true value. If the callback never returns a true
value, the last object in the chain is returned as usual. For example,
:func:`signature` uses this to stop unwrapping if any object in the
chain has a ``__signature__`` attribute defined.
:exc:`ValueError` is raised if a cycle is encountered.
.. versionadded:: 3.4
.. _inspect-stack:
The interpreter stack
......
......@@ -185,6 +185,15 @@ functools
New :func:`functools.singledispatch` decorator: see the :pep:`443`.
inspect
-------
:func:`~inspect.unwrap` makes it easy to unravel wrapper function chains
created by :func:`functools.wraps` (and any other API that sets the
``__wrapped__`` attribute on a wrapper function).
smtplib
-------
......@@ -327,6 +336,5 @@ that may require changes to your code.
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.
libraries that assumed the previous behaviour was intentional can use
:func:`inspect.unwrap` to gain equivalent behaviour.
......@@ -360,6 +360,40 @@ def getmro(cls):
"Return tuple of base classes (including cls) in method resolution order."
return cls.__mro__
# -------------------------------------------------------- function helpers
def unwrap(func, *, stop=None):
"""Get the object wrapped by *func*.
Follows the chain of :attr:`__wrapped__` attributes returning the last
object in the chain.
*stop* is an optional callback accepting an object in the wrapper chain
as its sole argument that allows the unwrapping to be terminated early if
the callback returns a true value. If the callback never returns a true
value, the last object in the chain is returned as usual. For example,
:func:`signature` uses this to stop unwrapping if any object in the
chain has a ``__signature__`` attribute defined.
:exc:`ValueError` is raised if a cycle is encountered.
"""
if stop is None:
def _is_wrapper(f):
return hasattr(f, '__wrapped__')
else:
def _is_wrapper(f):
return hasattr(f, '__wrapped__') and not stop(f)
f = func # remember the original func for error reporting
memo = {id(f)} # Memoise by id to tolerate non-hashable objects
while _is_wrapper(func):
func = func.__wrapped__
id_func = id(func)
if id_func in memo:
raise ValueError('wrapper loop when unwrapping {!r}'.format(f))
memo.add(id_func)
return func
# -------------------------------------------------- source code extraction
def indentsize(line):
"""Return the indent size, in spaces, at the start of a line of text."""
......@@ -1346,6 +1380,9 @@ def signature(obj):
sig = signature(obj.__func__)
return sig.replace(parameters=tuple(sig.parameters.values())[1:])
# Was this function wrapped by a decorator?
obj = unwrap(obj, stop=(lambda f: hasattr(f, "__signature__")))
try:
sig = obj.__signature__
except AttributeError:
......@@ -1354,13 +1391,6 @@ def signature(obj):
if sig is not None:
return sig
try:
# Was this function wrapped by a decorator?
wrapped = obj.__wrapped__
except AttributeError:
pass
else:
return signature(wrapped)
if isinstance(obj, types.FunctionType):
return Signature.from_function(obj)
......
......@@ -8,6 +8,7 @@ import datetime
import collections
import os
import shutil
import functools
from os.path import normcase
from test.support import run_unittest, TESTFN, DirsOnSysPath
......@@ -1719,6 +1720,17 @@ class TestSignatureObject(unittest.TestCase):
((('b', ..., ..., "positional_or_keyword"),),
...))
# Test we handle __signature__ partway down the wrapper stack
def wrapped_foo_call():
pass
wrapped_foo_call.__wrapped__ = Foo.__call__
self.assertEqual(self.signature(wrapped_foo_call),
((('a', ..., ..., "positional_or_keyword"),
('b', ..., ..., "positional_or_keyword")),
...))
def test_signature_on_class(self):
class C:
def __init__(self, a):
......@@ -1833,6 +1845,10 @@ class TestSignatureObject(unittest.TestCase):
self.assertEqual(self.signature(Wrapped),
((('a', ..., ..., "positional_or_keyword"),),
...))
# wrapper loop:
Wrapped.__wrapped__ = Wrapped
with self.assertRaisesRegex(ValueError, 'wrapper loop'):
self.signature(Wrapped)
def test_signature_on_lambdas(self):
self.assertEqual(self.signature((lambda a=10: a)),
......@@ -2284,6 +2300,62 @@ class TestBoundArguments(unittest.TestCase):
self.assertNotEqual(ba, ba4)
class TestUnwrap(unittest.TestCase):
def test_unwrap_one(self):
def func(a, b):
return a + b
wrapper = functools.lru_cache(maxsize=20)(func)
self.assertIs(inspect.unwrap(wrapper), func)
def test_unwrap_several(self):
def func(a, b):
return a + b
wrapper = func
for __ in range(10):
@functools.wraps(wrapper)
def wrapper():
pass
self.assertIsNot(wrapper.__wrapped__, func)
self.assertIs(inspect.unwrap(wrapper), func)
def test_stop(self):
def func1(a, b):
return a + b
@functools.wraps(func1)
def func2():
pass
@functools.wraps(func2)
def wrapper():
pass
func2.stop_here = 1
unwrapped = inspect.unwrap(wrapper,
stop=(lambda f: hasattr(f, "stop_here")))
self.assertIs(unwrapped, func2)
def test_cycle(self):
def func1(): pass
func1.__wrapped__ = func1
with self.assertRaisesRegex(ValueError, 'wrapper loop'):
inspect.unwrap(func1)
def func2(): pass
func2.__wrapped__ = func1
func1.__wrapped__ = func2
with self.assertRaisesRegex(ValueError, 'wrapper loop'):
inspect.unwrap(func1)
with self.assertRaisesRegex(ValueError, 'wrapper loop'):
inspect.unwrap(func2)
def test_unhashable(self):
def func(): pass
func.__wrapped__ = None
class C:
__hash__ = None
__wrapped__ = func
self.assertIsNone(inspect.unwrap(C()))
def test_main():
run_unittest(
TestDecorators, TestRetrievingSourceCode, TestOneliners, TestBuggyCases,
......@@ -2291,7 +2363,7 @@ def test_main():
TestGetcallargsFunctions, TestGetcallargsMethods,
TestGetcallargsUnboundMethods, TestGetattrStatic, TestGetGeneratorState,
TestNoEOL, TestSignatureObject, TestSignatureBind, TestParameterObject,
TestBoundArguments, TestGetClosureVars
TestBoundArguments, TestGetClosureVars, TestUnwrap
)
if __name__ == "__main__":
......
......@@ -171,6 +171,9 @@ Core and Builtins
Library
-------
- Issue #13266: Added inspect.unwrap to easily unravel __wrapped__ chains
(initial patch by Daniel Urban and Aaron Iles)
- Issue #18561: Skip name in ctypes' _build_callargs() if name is NULL.
- Issue #18559: Fix NULL pointer dereference error in _pickle module
......
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