[Python-checkins] gh-79940: add introspection API for asynchronous generators to `inspect` module (#11590)

kumaraditya303 webhook-mailer at python.org
Sat Mar 11 08:20:03 EST 2023


https://github.com/python/cpython/commit/ced13c96a4eb9391a9d27e3e13218f70c579670f
commit: ced13c96a4eb9391a9d27e3e13218f70c579670f
branch: main
author: Thomas Krennwallner <tk at postsubmeta.net>
committer: kumaraditya303 <59607654+kumaraditya303 at users.noreply.github.com>
date: 2023-03-11T18:49:40+05:30
summary:

gh-79940: add introspection API for asynchronous generators to `inspect` module (#11590)

files:
A Misc/NEWS.d/next/Library/2023-02-26-17-29-57.gh-issue-79940.SAfmAy.rst
M Doc/library/inspect.rst
M Doc/whatsnew/3.12.rst
M Lib/inspect.py
M Lib/test/test_inspect.py
M Objects/genobject.c

diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst
index 789e9839d22f..ccf240193d36 100644
--- a/Doc/library/inspect.rst
+++ b/Doc/library/inspect.rst
@@ -1440,8 +1440,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
@@ -1476,6 +1476,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.12
+
 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:
@@ -1507,6 +1523,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.12
+
 
 .. _inspect-module-co-flags:
 
diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst
index 48b7aab0595e..9f33dbc808dd 100644
--- a/Doc/whatsnew/3.12.rst
+++ b/Doc/whatsnew/3.12.rst
@@ -244,6 +244,10 @@ inspect
   a :term:`coroutine` for use with :func:`iscoroutinefunction`.
   (Contributed Carlton Gibson in :gh:`99247`.)
 
+* Add :func:`inspect.getasyncgenstate` and :func:`inspect.getasyncgenlocals`
+  for determining the current state of asynchronous generators.
+  (Contributed by Thomas Krennwallner in :issue:`35759`.)
+
 pathlib
 -------
 
diff --git a/Lib/inspect.py b/Lib/inspect.py
index edc23b0ffa92..0eceaaf9a24f 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -34,6 +34,10 @@
               'Yury Selivanov <yselivanov at sprymix.com>')
 
 __all__ = [
+    "AGEN_CLOSED",
+    "AGEN_CREATED",
+    "AGEN_RUNNING",
+    "AGEN_SUSPENDED",
     "ArgInfo",
     "Arguments",
     "Attribute",
@@ -77,6 +81,8 @@
     "getabsfile",
     "getargs",
     "getargvalues",
+    "getasyncgenlocals",
+    "getasyncgenstate",
     "getattr_static",
     "getblock",
     "getcallargs",
@@ -1935,6 +1941,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_suspended:
+        return AGEN_SUSPENDED
+    if agen.ag_frame is None:
+        return AGEN_CLOSED
+    return AGEN_CREATED
+
+
+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(f"{agen!r} is not a Python async generator")
+
+    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 02f8378d0413..410a2e5b5468 100644
--- a/Lib/test/test_inspect.py
+++ b/Lib/test/test_inspect.py
@@ -1,3 +1,4 @@
+import asyncio
 import builtins
 import collections
 import datetime
@@ -65,6 +66,10 @@ def revise(filename, *args):
 git = mod.StupidGit()
 
 
+def tearDownModule():
+    asyncio.set_event_loop_policy(None)
+
+
 def signatures_with_lexicographic_keyword_only_parameters():
     """
     Yields a whole bunch of functions with only keyword-only parameters,
@@ -2321,6 +2326,108 @@ async def func(a=None):
                          {'a': None, 'gencoro': gencoro, 'b': 'spam'})
 
 
+class TestGetAsyncGenState(unittest.IsolatedAsyncioTestCase):
+
+    def setUp(self):
+        async def number_asyncgen():
+            for number in range(5):
+                yield number
+        self.asyncgen = number_asyncgen()
+
+    async def asyncTearDown(self):
+        await self.asyncgen.aclose()
+
+    def _asyncgenstate(self):
+        return inspect.getasyncgenstate(self.asyncgen)
+
+    def test_created(self):
+        self.assertEqual(self._asyncgenstate(), inspect.AGEN_CREATED)
+
+    async def test_suspended(self):
+        value = await anext(self.asyncgen)
+        self.assertEqual(self._asyncgenstate(), inspect.AGEN_SUSPENDED)
+        self.assertEqual(value, 0)
+
+    async def test_closed_after_exhaustion(self):
+        countdown = 7
+        with self.assertRaises(StopAsyncIteration):
+            while countdown := countdown - 1:
+                await anext(self.asyncgen)
+        self.assertEqual(countdown, 1)
+        self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED)
+
+    async def test_closed_after_immediate_exception(self):
+        with self.assertRaises(RuntimeError):
+            await self.asyncgen.athrow(RuntimeError)
+        self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED)
+
+    async 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
+        await anext(self.asyncgen)
+        self.assertEqual(self._asyncgenstate(), inspect.AGEN_SUSPENDED)
+        # Running after the first yield
+        await anext(self.asyncgen)
+        self.assertEqual(self._asyncgenstate(), inspect.AGEN_SUSPENDED)
+
+    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))
+
+    async 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]})
+        await anext(numbers)
+        self.assertEqual(inspect.getasyncgenlocals(numbers),
+                         {'a': None, 'lst': [1, 2, 3], 'v': 1,
+                          'b': (1, 2, 3)})
+        await anext(numbers)
+        self.assertEqual(inspect.getasyncgenlocals(numbers),
+                         {'a': None, 'lst': [1, 2, 3], 'v': 2,
+                          'b': (1, 2, 3)})
+        await anext(numbers)
+        self.assertEqual(inspect.getasyncgenlocals(numbers),
+                         {'a': None, 'lst': [1, 2, 3], 'v': 3,
+                          'b': (1, 2, 3), 'c': 12})
+        with self.assertRaises(StopAsyncIteration):
+            await anext(numbers)
+        self.assertEqual(inspect.getasyncgenlocals(numbers), {})
+
+    async def test_getasyncgenlocals_empty(self):
+        async def yield_one():
+            yield 1
+        one = yield_one()
+        self.assertEqual(inspect.getasyncgenlocals(one), {})
+        await anext(one)
+        self.assertEqual(inspect.getasyncgenlocals(one), {})
+        with self.assertRaises(StopAsyncIteration):
+            await anext(one)
+        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
diff --git a/Misc/NEWS.d/next/Library/2023-02-26-17-29-57.gh-issue-79940.SAfmAy.rst b/Misc/NEWS.d/next/Library/2023-02-26-17-29-57.gh-issue-79940.SAfmAy.rst
new file mode 100644
index 000000000000..31b8ead84332
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-02-26-17-29-57.gh-issue-79940.SAfmAy.rst
@@ -0,0 +1,2 @@
+Add :func:`inspect.getasyncgenstate` and :func:`inspect.getasyncgenlocals`.
+Patch by Thomas Krennwallner.
diff --git a/Objects/genobject.c b/Objects/genobject.c
index 61463774310f..6316fa9865fe 100644
--- a/Objects/genobject.c
+++ b/Objects/genobject.c
@@ -1520,6 +1520,15 @@ ag_getcode(PyGenObject *gen, void *Py_UNUSED(ignored))
     return _gen_getcode(gen, "ag_code");
 }
 
+static PyObject *
+ag_getsuspended(PyAsyncGenObject *ag, void *Py_UNUSED(ignored))
+{
+    if (ag->ag_frame_state == FRAME_SUSPENDED) {
+        Py_RETURN_TRUE;
+    }
+    Py_RETURN_FALSE;
+}
+
 static PyGetSetDef async_gen_getsetlist[] = {
     {"__name__", (getter)gen_get_name, (setter)gen_set_name,
      PyDoc_STR("name of the async generator")},
@@ -1529,6 +1538,7 @@ static PyGetSetDef async_gen_getsetlist[] = {
      PyDoc_STR("object being awaited on, or None")},
      {"ag_frame",  (getter)ag_getframe, NULL, NULL},
      {"ag_code",  (getter)ag_getcode, NULL, NULL},
+     {"ag_suspended",  (getter)ag_getsuspended, NULL, NULL},
     {NULL} /* Sentinel */
 };
 



More information about the Python-checkins mailing list