summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Krennwallner <tk@postsubmeta.net>2023-03-11 13:19:40 (GMT)
committerGitHub <noreply@github.com>2023-03-11 13:19:40 (GMT)
commitced13c96a4eb9391a9d27e3e13218f70c579670f (patch)
tree2e48fbdb4e1539ae821c5b3ac0a7a70e6d1fbf93
parentaa0a73d1bc53dcb6348a869df1e775138991e561 (diff)
downloadcpython-ced13c96a4eb9391a9d27e3e13218f70c579670f.zip
cpython-ced13c96a4eb9391a9d27e3e13218f70c579670f.tar.gz
cpython-ced13c96a4eb9391a9d27e3e13218f70c579670f.tar.bz2
gh-79940: add introspection API for asynchronous generators to `inspect` module (#11590)
-rw-r--r--Doc/library/inspect.rst28
-rw-r--r--Doc/whatsnew/3.12.rst4
-rw-r--r--Lib/inspect.py50
-rw-r--r--Lib/test/test_inspect.py107
-rw-r--r--Misc/NEWS.d/next/Library/2023-02-26-17-29-57.gh-issue-79940.SAfmAy.rst2
-rw-r--r--Objects/genobject.c10
6 files changed, 199 insertions, 2 deletions
diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst
index 789e983..ccf2401 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 48b7aab..9f33dbc 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 edc23b0..0eceaaf 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -34,6 +34,10 @@ __author__ = ('Ka-Ping Yee <ping@lfw.org>',
'Yury Selivanov <yselivanov@sprymix.com>')
__all__ = [
+ "AGEN_CLOSED",
+ "AGEN_CREATED",
+ "AGEN_RUNNING",
+ "AGEN_SUSPENDED",
"ArgInfo",
"Arguments",
"Attribute",
@@ -77,6 +81,8 @@ __all__ = [
"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 02f8378..410a2e5 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 @@ class TestGetCoroutineState(unittest.TestCase):
{'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 0000000..31b8ead
--- /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 6146377..6316fa9 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 */
};