From c0cd1383ff30ced976fd44129db94fb4d2ce7fb4 Mon Sep 17 00:00:00 2001 From: Thomas Krennwallner Date: Wed, 16 Jan 2019 11:20:57 +0000 Subject: [PATCH] inspect: add introspection API for asynchronous generators The functions inspect.getasyncgenstate and inspect.getasyncgenlocals allow to determine the current state of asynchronous generators and mirror the introspection API for generators and coroutines. --- Doc/library/inspect.rst | 28 +++++++++- Lib/inspect.py | 44 +++++++++++++++ Lib/test/test_inspect.py | 135 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 204 insertions(+), 3 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index dfd78a9714..acc8b80484 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -1255,8 +1255,8 @@ code execution:: pass -Current State of Generators and Coroutines ------------------------------------------- +Current State of Generators, Coroutines, and Asynchronous Generators +-------------------------------------------------------------------- When implementing coroutine schedulers and for other advanced uses of generators, it is useful to determine whether a generator is currently @@ -1291,6 +1291,22 @@ generator to be determined easily. .. versionadded:: 3.5 +.. function:: getasyncgenstate(agen) + + Get current state of an asynchronous generator object. The function is + intended to be used with asynchronous iterator objects created by + :keyword:`async def` functions which use the :keyword:`yield` statement, + but will accept any asynchronous generator-like object that has + ``ag_running`` and ``ag_frame`` attributes. + + Possible states are: + * AGEN_CREATED: Waiting to start execution. + * AGEN_RUNNING: Currently being executed by the interpreter. + * AGEN_SUSPENDED: Currently suspended at a yield expression. + * AGEN_CLOSED: Execution has completed. + + .. versionadded:: 3.8 + The current internal state of the generator can also be queried. This is mostly useful for testing purposes, to ensure that internal state is being updated as expected: @@ -1322,6 +1338,14 @@ updated as expected: .. versionadded:: 3.5 +.. function:: getasyncgenlocals(agen) + + This function is analogous to :func:`~inspect.getgeneratorlocals`, but + works for asynchronous generator objects created by :keyword:`async def` + functions which use the :keyword:`yield` statement. + + .. versionadded:: 3.8 + .. _inspect-module-co-flags: diff --git a/Lib/inspect.py b/Lib/inspect.py index b8a142232b..385ee17a8b 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1692,6 +1692,50 @@ def getcoroutinelocals(coroutine): return {} +# ----------------------------------- asynchronous generator introspection + +AGEN_CREATED = 'AGEN_CREATED' +AGEN_RUNNING = 'AGEN_RUNNING' +AGEN_SUSPENDED = 'AGEN_SUSPENDED' +AGEN_CLOSED = 'AGEN_CLOSED' + + +def getasyncgenstate(agen): + """Get current state of an asynchronous generator object. + + Possible states are: + AGEN_CREATED: Waiting to start execution. + AGEN_RUNNING: Currently being executed by the interpreter. + AGEN_SUSPENDED: Currently suspended at a yield expression. + AGEN_CLOSED: Execution has completed. + """ + if agen.ag_running: + return AGEN_RUNNING + if agen.ag_frame is None: + return AGEN_CLOSED + if agen.ag_frame.f_lasti == -1: + return AGEN_CREATED + return AGEN_SUSPENDED + + +def getasyncgenlocals(agen): + """ + Get the mapping of asynchronous 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 isasyncgen(agen): + raise TypeError("{!r} is not a Python async generator".format(agen)) + + frame = getattr(agen, "ag_frame", None) + if frame is not None: + return agen.ag_frame.f_locals + else: + return {} + + ############################################################################### ### Function Signature Object (PEP 362) ############################################################################### diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index b9072e0137..9f5b5e0b09 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -2005,6 +2005,138 @@ class TestGetCoroutineState(unittest.TestCase): {'a': None, 'gencoro': gencoro, 'b': 'spam'}) +class TestGetAsyncGenState(unittest.TestCase): + + def setUp(self): + async def number_asyncgen(): + for number in range(5): + yield number + self.asyncgen = number_asyncgen() + + def tearDown(self): + try: + self.asyncgen.aclose().send(None) + except StopIteration: + pass + + def _asyncgenstate(self): + return inspect.getasyncgenstate(self.asyncgen) + + def test_created(self): + self.assertEqual(self._asyncgenstate(), inspect.AGEN_CREATED) + + def test_suspended(self): + try: + next(self.asyncgen.__anext__()) + except StopIteration as exc: + self.assertEqual(self._asyncgenstate(), inspect.AGEN_SUSPENDED) + self.assertEqual(exc.args, (0,)) + + def test_closed_after_exhaustion(self): + while True: + try: + next(self.asyncgen.__anext__()) + except StopAsyncIteration: + self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED) + break + except StopIteration as exc: + if exc.args is None: + self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED) + break + self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED) + + def test_closed_after_immediate_exception(self): + with self.assertRaises(RuntimeError): + self.asyncgen.athrow(RuntimeError).send(None) + self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED) + + def test_running(self): + async def running_check_asyncgen(): + for number in range(5): + self.assertEqual(self._asyncgenstate(), inspect.AGEN_RUNNING) + yield number + self.assertEqual(self._asyncgenstate(), inspect.AGEN_RUNNING) + self.asyncgen = running_check_asyncgen() + # Running up to the first yield + try: + next(self.asyncgen.__anext__()) + except StopIteration: + pass + # Running after the first yield + try: + next(self.asyncgen.__anext__()) + except StopIteration: + pass + + def test_easy_debugging(self): + # repr() and str() of a asyncgen state should contain the state name + names = 'AGEN_CREATED AGEN_RUNNING AGEN_SUSPENDED AGEN_CLOSED'.split() + for name in names: + state = getattr(inspect, name) + self.assertIn(name, repr(state)) + self.assertIn(name, str(state)) + + def test_getasyncgenlocals(self): + async 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.getasyncgenlocals(numbers), + {'a': None, 'lst': [1, 2, 3]}) + try: + next(numbers.__anext__()) + except StopIteration: + pass + self.assertEqual(inspect.getasyncgenlocals(numbers), + {'a': None, 'lst': [1, 2, 3], 'v': 1, + 'b': (1, 2, 3)}) + try: + next(numbers.__anext__()) + except StopIteration: + pass + self.assertEqual(inspect.getasyncgenlocals(numbers), + {'a': None, 'lst': [1, 2, 3], 'v': 2, + 'b': (1, 2, 3)}) + try: + next(numbers.__anext__()) + except StopIteration: + pass + self.assertEqual(inspect.getasyncgenlocals(numbers), + {'a': None, 'lst': [1, 2, 3], 'v': 3, + 'b': (1, 2, 3), 'c': 12}) + try: + next(numbers.__anext__()) + except StopAsyncIteration: + pass + self.assertEqual(inspect.getasyncgenlocals(numbers), {}) + + def test_getasyncgenlocals_empty(self): + async def yield_one(): + yield 1 + one = yield_one() + self.assertEqual(inspect.getasyncgenlocals(one), {}) + try: + next(one.__anext__()) + except StopIteration: + pass + self.assertEqual(inspect.getasyncgenlocals(one), {}) + try: + next(one.__anext__()) + except StopAsyncIteration: + pass + self.assertEqual(inspect.getasyncgenlocals(one), {}) + + def test_getasyncgenlocals_error(self): + self.assertRaises(TypeError, inspect.getasyncgenlocals, 1) + self.assertRaises(TypeError, inspect.getasyncgenlocals, lambda x: True) + self.assertRaises(TypeError, inspect.getasyncgenlocals, set) + self.assertRaises(TypeError, inspect.getasyncgenlocals, (2,3)) + + class MySignature(inspect.Signature): # Top-level to make it picklable; # used in test_signature_object_pickle @@ -3905,7 +4037,8 @@ def test_main(): TestBoundArguments, TestSignaturePrivateHelpers, TestSignatureDefinitions, TestIsDataDescriptor, TestGetClosureVars, TestUnwrap, TestMain, TestReload, - TestGetCoroutineState, TestGettingSourceOfToplevelFrames + TestGetCoroutineState, TestGettingSourceOfToplevelFrames, + TestGetAsyncGenState ) if __name__ == "__main__": -- 2.11.0