mirror of https://github.com/python/cpython.git
Close #13266: Add inspect.unwrap
Initial patch by Daniel Urban and Aaron Iles
This commit is contained in:
parent
77578204d6
commit
e8c45d6d0e
|
@ -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
|
||||||
|
|
||||||
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.
|
||||||
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):
|
||||||
"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):
|
||||||
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):
|
||||||
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 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 @@ def __call__(self, a, b):
|
||||||
((('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 Wrapped:
|
||||||
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 @@ def bar(b): pass
|
||||||
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():
|
||||||
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
|
||||||
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
|
||||||
|
|
Loading…
Reference in New Issue