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 ...@@ -797,6 +797,23 @@ Classes and functions
.. versionadded:: 3.3 .. 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: .. _inspect-stack:
The interpreter stack The interpreter stack
......
...@@ -185,6 +185,15 @@ functools ...@@ -185,6 +185,15 @@ functools
New :func:`functools.singledispatch` decorator: see the :pep:`443`. 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 smtplib
------- -------
...@@ -327,6 +336,5 @@ that may require changes to your code. ...@@ -327,6 +336,5 @@ that may require changes to your code.
wrapped attribute set. This means ``__wrapped__`` attributes now correctly wrapped attribute set. This means ``__wrapped__`` attributes now correctly
link a stack of decorated functions rather than every ``__wrapped__`` link a stack of decorated functions rather than every ``__wrapped__``
attribute in the chain referring to the innermost function. Introspection attribute in the chain referring to the innermost function. Introspection
libraries that assumed the previous behaviour was intentional will need to libraries that assumed the previous behaviour was intentional can use
be updated to walk the chain of ``__wrapped__`` attributes to find the :func:`inspect.unwrap` to gain equivalent behaviour.
innermost function.
...@@ -360,6 +360,40 @@ def getmro(cls): ...@@ -360,6 +360,40 @@ def getmro(cls):
"Return tuple of base classes (including cls) in method resolution order." "Return tuple of base classes (including cls) in method resolution order."
return cls.__mro__ 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 # -------------------------------------------------- source code extraction
def indentsize(line): def indentsize(line):
"""Return the indent size, in spaces, at the start of a line of text.""" """Return the indent size, in spaces, at the start of a line of text."""
...@@ -1346,6 +1380,9 @@ def signature(obj): ...@@ -1346,6 +1380,9 @@ def signature(obj):
sig = signature(obj.__func__) sig = signature(obj.__func__)
return sig.replace(parameters=tuple(sig.parameters.values())[1:]) 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: try:
sig = obj.__signature__ sig = obj.__signature__
except AttributeError: except AttributeError:
...@@ -1354,13 +1391,6 @@ def signature(obj): ...@@ -1354,13 +1391,6 @@ def signature(obj):
if sig is not None: if sig is not None:
return sig 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): if isinstance(obj, types.FunctionType):
return Signature.from_function(obj) return Signature.from_function(obj)
......
...@@ -8,6 +8,7 @@ import datetime ...@@ -8,6 +8,7 @@ import datetime
import collections import collections
import os import os
import shutil import shutil
import functools
from os.path import normcase from os.path import normcase
from test.support import run_unittest, TESTFN, DirsOnSysPath from test.support import run_unittest, TESTFN, DirsOnSysPath
...@@ -1719,6 +1720,17 @@ class TestSignatureObject(unittest.TestCase): ...@@ -1719,6 +1720,17 @@ class TestSignatureObject(unittest.TestCase):
((('b', ..., ..., "positional_or_keyword"),), ((('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): def test_signature_on_class(self):
class C: class C:
def __init__(self, a): def __init__(self, a):
...@@ -1833,6 +1845,10 @@ class TestSignatureObject(unittest.TestCase): ...@@ -1833,6 +1845,10 @@ class TestSignatureObject(unittest.TestCase):
self.assertEqual(self.signature(Wrapped), self.assertEqual(self.signature(Wrapped),
((('a', ..., ..., "positional_or_keyword"),), ((('a', ..., ..., "positional_or_keyword"),),
...)) ...))
# wrapper loop:
Wrapped.__wrapped__ = Wrapped
with self.assertRaisesRegex(ValueError, 'wrapper loop'):
self.signature(Wrapped)
def test_signature_on_lambdas(self): def test_signature_on_lambdas(self):
self.assertEqual(self.signature((lambda a=10: a)), self.assertEqual(self.signature((lambda a=10: a)),
...@@ -2284,6 +2300,62 @@ class TestBoundArguments(unittest.TestCase): ...@@ -2284,6 +2300,62 @@ class TestBoundArguments(unittest.TestCase):
self.assertNotEqual(ba, ba4) 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(): def test_main():
run_unittest( run_unittest(
TestDecorators, TestRetrievingSourceCode, TestOneliners, TestBuggyCases, TestDecorators, TestRetrievingSourceCode, TestOneliners, TestBuggyCases,
...@@ -2291,7 +2363,7 @@ def test_main(): ...@@ -2291,7 +2363,7 @@ def test_main():
TestGetcallargsFunctions, TestGetcallargsMethods, TestGetcallargsFunctions, TestGetcallargsMethods,
TestGetcallargsUnboundMethods, TestGetattrStatic, TestGetGeneratorState, TestGetcallargsUnboundMethods, TestGetattrStatic, TestGetGeneratorState,
TestNoEOL, TestSignatureObject, TestSignatureBind, TestParameterObject, TestNoEOL, TestSignatureObject, TestSignatureBind, TestParameterObject,
TestBoundArguments, TestGetClosureVars TestBoundArguments, TestGetClosureVars, TestUnwrap
) )
if __name__ == "__main__": if __name__ == "__main__":
......
...@@ -171,6 +171,9 @@ Core and Builtins ...@@ -171,6 +171,9 @@ Core and Builtins
Library 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 #18561: Skip name in ctypes' _build_callargs() if name is NULL.
- Issue #18559: Fix NULL pointer dereference error in _pickle module - 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