diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -480,6 +480,28 @@ Classes and functions .. versionadded:: 3.2 +.. function:: getclosure(func) + + Get the mapping of free variables in *func* to their current values. A + dictionary is returned that maps from variable names to values. If + *func* is a :term:`function` and does not have a closure, then an empty + dictionary is returned. An :exc:`TypeError` is raised if *func* is not a + function. + + .. versionadded:: 3.3 + + +.. function:: getgeneratorlocals(generator) + + Get the mapping of live local variables in *generator* to their current + values. A dictionary is returned that maps from variable names to values. + If *generator* is a :term:`generator` and does not have a frame, then an + empty dictionary is returned. An :exc:`TypeError` is raised if *generator* + is not a generator. + + .. versionadded:: 3.3 + + .. _inspect-stack: The interpreter stack diff --git a/Lib/inspect.py b/Lib/inspect.py --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1014,6 +1014,39 @@ def getcallargs(func, *positional, **nam _missing_arguments(f_name, kwonlyargs, False, arg2value) return arg2value +def getclosure(func): + """ + Get the mapping of free variables to their current values. + + A dict is returned, with keys the free variable names and values the + bound values.""" + + if not isfunction(func): + raise TypeError("'{!r}' is not a function".format(func)) + + if func.__closure__: + return { + var : cell.cell_contents + for var, cell in zip(func.__code__.co_freevars, func.__closure__) + } + else: + return {} + +def getgeneratorlocals(generator): + """ + Get the mapping of generator local variables to their current values. + + A dict is returned, with the keys the local variable names and values the + bound values.""" + + if not isgenerator(generator): + raise TypeError("'{!r}' is not a generator".format(generator)) + + if generator.gi_frame: + return generator.gi_frame.f_locals + else: + return {} + # -------------------------------------------------- stack frame extraction Traceback = namedtuple('Traceback', 'filename lineno function code_context index') diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -572,6 +572,98 @@ class TestClassesAndFunctions(unittest.T self.assertIn(('m1', 'method', D), attrs, 'missing plain method') self.assertIn(('datablob', 'data', A), attrs, 'missing data') + def test_getclosure(self): + def make_adder(x): + def add(y): + return x + y + return add + + def curry(func, arg1): + return lambda arg2: func(arg1, arg2) + + def less_than(a, b): + return a < b + + # The infamous Y combinator. + def Y(le): + def g(f): + return le(lambda x: f(f)(x)) + Y.g_ref = g + return g(g) + + def check_y_combinator(func): + self.assertEqual(inspect.getclosure(func), + {'f': Y.g_ref}) + + inc = make_adder(1) + add_two = make_adder(2) + greater_than_five = curry(less_than, 5) + + self.assertEqual(inspect.getclosure(inc), {'x': 1}) + self.assertEqual(inspect.getclosure(add_two), {'x': 2}) + self.assertEqual(inspect.getclosure(greater_than_five), + {'arg1': 5, 'func': less_than}) + self.assertEqual(inspect.getclosure((lambda x: lambda y: x + y)(3)), + {'x': 3}) + Y(check_y_combinator) + + def test_getclosure_empty(self): + def foo(): pass + self.assertEqual(inspect.getclosure(lambda: True), {}) + self.assertEqual(inspect.getclosure(foo), {}) + + def test_getclosure_error(self): + class T: pass + self.assertRaises(TypeError, inspect.getclosure, 1) + self.assertRaises(TypeError, inspect.getclosure, list) + self.assertRaises(TypeError, inspect.getclosure, {}) + + def test_getgeneratorlocals(self): + def each(lst, a=None): + b=(1, 2, 3) + for v in lst: + if v == 3: + c = 12 + yield v + + numbers = each([1, 2, 3]) + self.assertEqual(inspect.getgeneratorlocals(numbers), + {'a': None, 'lst': [1, 2, 3]}) + next(numbers) + self.assertEqual(inspect.getgeneratorlocals(numbers), + {'a': None, 'lst': [1, 2, 3], 'v': 1, + 'b': (1, 2, 3)}) + next(numbers) + self.assertEqual(inspect.getgeneratorlocals(numbers), + {'a': None, 'lst': [1, 2, 3], 'v': 2, + 'b': (1, 2, 3)}) + next(numbers) + self.assertEqual(inspect.getgeneratorlocals(numbers), + {'a': None, 'lst': [1, 2, 3], 'v': 3, + 'b': (1, 2, 3), 'c': 12}) + try: + next(numbers) + except StopIteration: + pass + self.assertEqual(inspect.getgeneratorlocals(numbers), {}) + + def test_getgeneratorlocals_empty(self): + def yield_one(): + yield 1 + one = yield_one() + self.assertEqual(inspect.getgeneratorlocals(one), {}) + try: + next(one) + except StopIteration: + pass + self.assertEqual(inspect.getgeneratorlocals(one), {}) + + def test_getgeneratorlocals_error(self): + self.assertRaises(TypeError, inspect.getgeneratorlocals, 1) + self.assertRaises(TypeError, inspect.getgeneratorlocals, lambda x: True) + self.assertRaises(TypeError, inspect.getgeneratorlocals, set) + self.assertRaises(TypeError, inspect.getgeneratorlocals, (2,3)) + class TestGetcallargsFunctions(unittest.TestCase): def assertEqualCallArgs(self, func, call_params_string, locs=None):