diff -r 6961c8af54f8 Doc/library/inspect.rst --- a/Doc/library/inspect.rst Mon Oct 29 18:20:18 2012 -0500 +++ b/Doc/library/inspect.rst Tue Oct 30 15:26:12 2012 +1100 @@ -795,6 +795,16 @@ .. versionadded:: 3.3 +.. function:: unwrap(func) + + Get the object wrapped by *func*. If follows the chain of *__wrapped__* + attributes returning the last object in the chain. + + :exc:`ValueError` is raised if a cycle is encountered. + + .. versionadded:: 3.4 + + .. _inspect-stack: The interpreter stack diff -r 6961c8af54f8 Lib/inspect.py --- a/Lib/inspect.py Mon Oct 29 18:20:18 2012 -0500 +++ b/Lib/inspect.py Tue Oct 30 15:26:12 2012 +1100 @@ -361,6 +361,24 @@ "Return tuple of base classes (including cls) in method resolution order." return cls.__mro__ +# -------------------------------------------------------- function helpers + +def unwrap(func): + """Get the object wrapped by 'func'. + + Follow the chain of __wrapped__ attributes, + and return the last object in the chain. + If there is a cycle, raise a ValueError. + """ + memo = {id(func)} + while hasattr(func, '__wrapped__'): + func = func.__wrapped__ + if id(func) in memo: + raise ValueError('wrapper loop') + else: + 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.""" diff -r 6961c8af54f8 Lib/test/test_inspect.py --- a/Lib/test/test_inspect.py Mon Oct 29 18:20:18 2012 -0500 +++ b/Lib/test/test_inspect.py Tue Oct 30 15:26:12 2012 +1100 @@ -8,6 +8,7 @@ import collections import os import shutil +import functools from os.path import normcase from test.support import run_unittest, TESTFN, DirsOnSysPath @@ -2268,6 +2269,37 @@ self.assertNotEqual(ba, ba4) +class TestUnwrap(unittest.TestCase): + + def test_unwrap(self): + def func(a, b): + return a + b + wrapper = functools.lru_cache(maxsize=20)(func) + self.assertIs(inspect.unwrap(wrapper), func) + + 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.assertIs(inspect.unwrap(C()), None) + + def test_main(): run_unittest( TestDecorators, TestRetrievingSourceCode, TestOneliners, TestBuggyCases, @@ -2275,7 +2307,7 @@ TestGetcallargsFunctions, TestGetcallargsMethods, TestGetcallargsUnboundMethods, TestGetattrStatic, TestGetGeneratorState, TestNoEOL, TestSignatureObject, TestSignatureBind, TestParameterObject, - TestBoundArguments, TestGetClosureVars + TestBoundArguments, TestGetClosureVars, TestUnwrap ) if __name__ == "__main__":