Skip to content

Commit ced13c9

Browse files
authored
gh-79940: add introspection API for asynchronous generators to inspect module (#11590)
1 parent aa0a73d commit ced13c9

File tree

6 files changed

+199
-2
lines changed

6 files changed

+199
-2
lines changed

Doc/library/inspect.rst

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,8 +1440,8 @@ code execution::
14401440
pass
14411441

14421442

1443-
Current State of Generators and Coroutines
1444-
------------------------------------------
1443+
Current State of Generators, Coroutines, and Asynchronous Generators
1444+
--------------------------------------------------------------------
14451445

14461446
When implementing coroutine schedulers and for other advanced uses of
14471447
generators, it is useful to determine whether a generator is currently
@@ -1476,6 +1476,22 @@ generator to be determined easily.
14761476

14771477
.. versionadded:: 3.5
14781478

1479+
.. function:: getasyncgenstate(agen)
1480+
1481+
Get current state of an asynchronous generator object. The function is
1482+
intended to be used with asynchronous iterator objects created by
1483+
:keyword:`async def` functions which use the :keyword:`yield` statement,
1484+
but will accept any asynchronous generator-like object that has
1485+
``ag_running`` and ``ag_frame`` attributes.
1486+
1487+
Possible states are:
1488+
* AGEN_CREATED: Waiting to start execution.
1489+
* AGEN_RUNNING: Currently being executed by the interpreter.
1490+
* AGEN_SUSPENDED: Currently suspended at a yield expression.
1491+
* AGEN_CLOSED: Execution has completed.
1492+
1493+
.. versionadded:: 3.12
1494+
14791495
The current internal state of the generator can also be queried. This is
14801496
mostly useful for testing purposes, to ensure that internal state is being
14811497
updated as expected:
@@ -1507,6 +1523,14 @@ updated as expected:
15071523

15081524
.. versionadded:: 3.5
15091525

1526+
.. function:: getasyncgenlocals(agen)
1527+
1528+
This function is analogous to :func:`~inspect.getgeneratorlocals`, but
1529+
works for asynchronous generator objects created by :keyword:`async def`
1530+
functions which use the :keyword:`yield` statement.
1531+
1532+
.. versionadded:: 3.12
1533+
15101534

15111535
.. _inspect-module-co-flags:
15121536

Doc/whatsnew/3.12.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,10 @@ inspect
244244
a :term:`coroutine` for use with :func:`iscoroutinefunction`.
245245
(Contributed Carlton Gibson in :gh:`99247`.)
246246

247+
* Add :func:`inspect.getasyncgenstate` and :func:`inspect.getasyncgenlocals`
248+
for determining the current state of asynchronous generators.
249+
(Contributed by Thomas Krennwallner in :issue:`35759`.)
250+
247251
pathlib
248252
-------
249253

Lib/inspect.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
'Yury Selivanov <[email protected]>')
3535

3636
__all__ = [
37+
"AGEN_CLOSED",
38+
"AGEN_CREATED",
39+
"AGEN_RUNNING",
40+
"AGEN_SUSPENDED",
3741
"ArgInfo",
3842
"Arguments",
3943
"Attribute",
@@ -77,6 +81,8 @@
7781
"getabsfile",
7882
"getargs",
7983
"getargvalues",
84+
"getasyncgenlocals",
85+
"getasyncgenstate",
8086
"getattr_static",
8187
"getblock",
8288
"getcallargs",
@@ -1935,6 +1941,50 @@ def getcoroutinelocals(coroutine):
19351941
return {}
19361942

19371943

1944+
# ----------------------------------- asynchronous generator introspection
1945+
1946+
AGEN_CREATED = 'AGEN_CREATED'
1947+
AGEN_RUNNING = 'AGEN_RUNNING'
1948+
AGEN_SUSPENDED = 'AGEN_SUSPENDED'
1949+
AGEN_CLOSED = 'AGEN_CLOSED'
1950+
1951+
1952+
def getasyncgenstate(agen):
1953+
"""Get current state of an asynchronous generator object.
1954+
1955+
Possible states are:
1956+
AGEN_CREATED: Waiting to start execution.
1957+
AGEN_RUNNING: Currently being executed by the interpreter.
1958+
AGEN_SUSPENDED: Currently suspended at a yield expression.
1959+
AGEN_CLOSED: Execution has completed.
1960+
"""
1961+
if agen.ag_running:
1962+
return AGEN_RUNNING
1963+
if agen.ag_suspended:
1964+
return AGEN_SUSPENDED
1965+
if agen.ag_frame is None:
1966+
return AGEN_CLOSED
1967+
return AGEN_CREATED
1968+
1969+
1970+
def getasyncgenlocals(agen):
1971+
"""
1972+
Get the mapping of asynchronous generator local variables to their current
1973+
values.
1974+
1975+
A dict is returned, with the keys the local variable names and values the
1976+
bound values."""
1977+
1978+
if not isasyncgen(agen):
1979+
raise TypeError(f"{agen!r} is not a Python async generator")
1980+
1981+
frame = getattr(agen, "ag_frame", None)
1982+
if frame is not None:
1983+
return agen.ag_frame.f_locals
1984+
else:
1985+
return {}
1986+
1987+
19381988
###############################################################################
19391989
### Function Signature Object (PEP 362)
19401990
###############################################################################

Lib/test/test_inspect.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import builtins
23
import collections
34
import datetime
@@ -65,6 +66,10 @@ def revise(filename, *args):
6566
git = mod.StupidGit()
6667

6768

69+
def tearDownModule():
70+
asyncio.set_event_loop_policy(None)
71+
72+
6873
def signatures_with_lexicographic_keyword_only_parameters():
6974
"""
7075
Yields a whole bunch of functions with only keyword-only parameters,
@@ -2321,6 +2326,108 @@ async def func(a=None):
23212326
{'a': None, 'gencoro': gencoro, 'b': 'spam'})
23222327

23232328

2329+
class TestGetAsyncGenState(unittest.IsolatedAsyncioTestCase):
2330+
2331+
def setUp(self):
2332+
async def number_asyncgen():
2333+
for number in range(5):
2334+
yield number
2335+
self.asyncgen = number_asyncgen()
2336+
2337+
async def asyncTearDown(self):
2338+
await self.asyncgen.aclose()
2339+
2340+
def _asyncgenstate(self):
2341+
return inspect.getasyncgenstate(self.asyncgen)
2342+
2343+
def test_created(self):
2344+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_CREATED)
2345+
2346+
async def test_suspended(self):
2347+
value = await anext(self.asyncgen)
2348+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_SUSPENDED)
2349+
self.assertEqual(value, 0)
2350+
2351+
async def test_closed_after_exhaustion(self):
2352+
countdown = 7
2353+
with self.assertRaises(StopAsyncIteration):
2354+
while countdown := countdown - 1:
2355+
await anext(self.asyncgen)
2356+
self.assertEqual(countdown, 1)
2357+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED)
2358+
2359+
async def test_closed_after_immediate_exception(self):
2360+
with self.assertRaises(RuntimeError):
2361+
await self.asyncgen.athrow(RuntimeError)
2362+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED)
2363+
2364+
async def test_running(self):
2365+
async def running_check_asyncgen():
2366+
for number in range(5):
2367+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_RUNNING)
2368+
yield number
2369+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_RUNNING)
2370+
self.asyncgen = running_check_asyncgen()
2371+
# Running up to the first yield
2372+
await anext(self.asyncgen)
2373+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_SUSPENDED)
2374+
# Running after the first yield
2375+
await anext(self.asyncgen)
2376+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_SUSPENDED)
2377+
2378+
def test_easy_debugging(self):
2379+
# repr() and str() of a asyncgen state should contain the state name
2380+
names = 'AGEN_CREATED AGEN_RUNNING AGEN_SUSPENDED AGEN_CLOSED'.split()
2381+
for name in names:
2382+
state = getattr(inspect, name)
2383+
self.assertIn(name, repr(state))
2384+
self.assertIn(name, str(state))
2385+
2386+
async def test_getasyncgenlocals(self):
2387+
async def each(lst, a=None):
2388+
b=(1, 2, 3)
2389+
for v in lst:
2390+
if v == 3:
2391+
c = 12
2392+
yield v
2393+
2394+
numbers = each([1, 2, 3])
2395+
self.assertEqual(inspect.getasyncgenlocals(numbers),
2396+
{'a': None, 'lst': [1, 2, 3]})
2397+
await anext(numbers)
2398+
self.assertEqual(inspect.getasyncgenlocals(numbers),
2399+
{'a': None, 'lst': [1, 2, 3], 'v': 1,
2400+
'b': (1, 2, 3)})
2401+
await anext(numbers)
2402+
self.assertEqual(inspect.getasyncgenlocals(numbers),
2403+
{'a': None, 'lst': [1, 2, 3], 'v': 2,
2404+
'b': (1, 2, 3)})
2405+
await anext(numbers)
2406+
self.assertEqual(inspect.getasyncgenlocals(numbers),
2407+
{'a': None, 'lst': [1, 2, 3], 'v': 3,
2408+
'b': (1, 2, 3), 'c': 12})
2409+
with self.assertRaises(StopAsyncIteration):
2410+
await anext(numbers)
2411+
self.assertEqual(inspect.getasyncgenlocals(numbers), {})
2412+
2413+
async def test_getasyncgenlocals_empty(self):
2414+
async def yield_one():
2415+
yield 1
2416+
one = yield_one()
2417+
self.assertEqual(inspect.getasyncgenlocals(one), {})
2418+
await anext(one)
2419+
self.assertEqual(inspect.getasyncgenlocals(one), {})
2420+
with self.assertRaises(StopAsyncIteration):
2421+
await anext(one)
2422+
self.assertEqual(inspect.getasyncgenlocals(one), {})
2423+
2424+
def test_getasyncgenlocals_error(self):
2425+
self.assertRaises(TypeError, inspect.getasyncgenlocals, 1)
2426+
self.assertRaises(TypeError, inspect.getasyncgenlocals, lambda x: True)
2427+
self.assertRaises(TypeError, inspect.getasyncgenlocals, set)
2428+
self.assertRaises(TypeError, inspect.getasyncgenlocals, (2,3))
2429+
2430+
23242431
class MySignature(inspect.Signature):
23252432
# Top-level to make it picklable;
23262433
# used in test_signature_object_pickle
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :func:`inspect.getasyncgenstate` and :func:`inspect.getasyncgenlocals`.
2+
Patch by Thomas Krennwallner.

Objects/genobject.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,6 +1520,15 @@ ag_getcode(PyGenObject *gen, void *Py_UNUSED(ignored))
15201520
return _gen_getcode(gen, "ag_code");
15211521
}
15221522

1523+
static PyObject *
1524+
ag_getsuspended(PyAsyncGenObject *ag, void *Py_UNUSED(ignored))
1525+
{
1526+
if (ag->ag_frame_state == FRAME_SUSPENDED) {
1527+
Py_RETURN_TRUE;
1528+
}
1529+
Py_RETURN_FALSE;
1530+
}
1531+
15231532
static PyGetSetDef async_gen_getsetlist[] = {
15241533
{"__name__", (getter)gen_get_name, (setter)gen_set_name,
15251534
PyDoc_STR("name of the async generator")},
@@ -1529,6 +1538,7 @@ static PyGetSetDef async_gen_getsetlist[] = {
15291538
PyDoc_STR("object being awaited on, or None")},
15301539
{"ag_frame", (getter)ag_getframe, NULL, NULL},
15311540
{"ag_code", (getter)ag_getcode, NULL, NULL},
1541+
{"ag_suspended", (getter)ag_getsuspended, NULL, NULL},
15321542
{NULL} /* Sentinel */
15331543
};
15341544

0 commit comments

Comments
 (0)