Skip to content

Commit bca9939

Browse files
committed
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.
1 parent 6daf42b commit bca9939

File tree

5 files changed

+220
-2
lines changed

5 files changed

+220
-2
lines changed

Doc/library/inspect.rst

+26-2
Original file line numberDiff line numberDiff line change
@@ -1439,8 +1439,8 @@ code execution::
14391439
pass
14401440

14411441

1442-
Current State of Generators and Coroutines
1443-
------------------------------------------
1442+
Current State of Generators, Coroutines, and Asynchronous Generators
1443+
--------------------------------------------------------------------
14441444

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

14761476
.. versionadded:: 3.5
14771477

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

15071523
.. versionadded:: 3.5
15081524

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

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

Lib/inspect.py

+50
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("{!r} is not a Python async generator".format(agen))
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

+132
Original file line numberDiff line numberDiff line change
@@ -2321,6 +2321,138 @@ async def func(a=None):
23212321
{'a': None, 'gencoro': gencoro, 'b': 'spam'})
23222322

23232323

2324+
class TestGetAsyncGenState(unittest.TestCase):
2325+
2326+
def setUp(self):
2327+
async def number_asyncgen():
2328+
for number in range(5):
2329+
yield number
2330+
self.asyncgen = number_asyncgen()
2331+
2332+
def tearDown(self):
2333+
try:
2334+
self.asyncgen.aclose().send(None)
2335+
except StopIteration:
2336+
pass
2337+
2338+
def _asyncgenstate(self):
2339+
return inspect.getasyncgenstate(self.asyncgen)
2340+
2341+
def test_created(self):
2342+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_CREATED)
2343+
2344+
def test_suspended(self):
2345+
try:
2346+
next(self.asyncgen.__anext__())
2347+
except StopIteration as exc:
2348+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_SUSPENDED)
2349+
self.assertEqual(exc.args, (0,))
2350+
2351+
def test_closed_after_exhaustion(self):
2352+
while True:
2353+
try:
2354+
next(self.asyncgen.__anext__())
2355+
except StopAsyncIteration:
2356+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED)
2357+
break
2358+
except StopIteration as exc:
2359+
if exc.args is None:
2360+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED)
2361+
break
2362+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED)
2363+
2364+
def test_closed_after_immediate_exception(self):
2365+
with self.assertRaises(RuntimeError):
2366+
self.asyncgen.athrow(RuntimeError).send(None)
2367+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED)
2368+
2369+
def test_running(self):
2370+
async def running_check_asyncgen():
2371+
for number in range(5):
2372+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_RUNNING)
2373+
yield number
2374+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_RUNNING)
2375+
self.asyncgen = running_check_asyncgen()
2376+
# Running up to the first yield
2377+
try:
2378+
next(self.asyncgen.__anext__())
2379+
except StopIteration:
2380+
pass
2381+
# Running after the first yield
2382+
try:
2383+
next(self.asyncgen.__anext__())
2384+
except StopIteration:
2385+
pass
2386+
2387+
def test_easy_debugging(self):
2388+
# repr() and str() of a asyncgen state should contain the state name
2389+
names = 'AGEN_CREATED AGEN_RUNNING AGEN_SUSPENDED AGEN_CLOSED'.split()
2390+
for name in names:
2391+
state = getattr(inspect, name)
2392+
self.assertIn(name, repr(state))
2393+
self.assertIn(name, str(state))
2394+
2395+
def test_getasyncgenlocals(self):
2396+
async def each(lst, a=None):
2397+
b=(1, 2, 3)
2398+
for v in lst:
2399+
if v == 3:
2400+
c = 12
2401+
yield v
2402+
2403+
numbers = each([1, 2, 3])
2404+
self.assertEqual(inspect.getasyncgenlocals(numbers),
2405+
{'a': None, 'lst': [1, 2, 3]})
2406+
try:
2407+
next(numbers.__anext__())
2408+
except StopIteration:
2409+
pass
2410+
self.assertEqual(inspect.getasyncgenlocals(numbers),
2411+
{'a': None, 'lst': [1, 2, 3], 'v': 1,
2412+
'b': (1, 2, 3)})
2413+
try:
2414+
next(numbers.__anext__())
2415+
except StopIteration:
2416+
pass
2417+
self.assertEqual(inspect.getasyncgenlocals(numbers),
2418+
{'a': None, 'lst': [1, 2, 3], 'v': 2,
2419+
'b': (1, 2, 3)})
2420+
try:
2421+
next(numbers.__anext__())
2422+
except StopIteration:
2423+
pass
2424+
self.assertEqual(inspect.getasyncgenlocals(numbers),
2425+
{'a': None, 'lst': [1, 2, 3], 'v': 3,
2426+
'b': (1, 2, 3), 'c': 12})
2427+
try:
2428+
next(numbers.__anext__())
2429+
except StopAsyncIteration:
2430+
pass
2431+
self.assertEqual(inspect.getasyncgenlocals(numbers), {})
2432+
2433+
def test_getasyncgenlocals_empty(self):
2434+
async def yield_one():
2435+
yield 1
2436+
one = yield_one()
2437+
self.assertEqual(inspect.getasyncgenlocals(one), {})
2438+
try:
2439+
next(one.__anext__())
2440+
except StopIteration:
2441+
pass
2442+
self.assertEqual(inspect.getasyncgenlocals(one), {})
2443+
try:
2444+
next(one.__anext__())
2445+
except StopAsyncIteration:
2446+
pass
2447+
self.assertEqual(inspect.getasyncgenlocals(one), {})
2448+
2449+
def test_getasyncgenlocals_error(self):
2450+
self.assertRaises(TypeError, inspect.getasyncgenlocals, 1)
2451+
self.assertRaises(TypeError, inspect.getasyncgenlocals, lambda x: True)
2452+
self.assertRaises(TypeError, inspect.getasyncgenlocals, set)
2453+
self.assertRaises(TypeError, inspect.getasyncgenlocals, (2,3))
2454+
2455+
23242456
class MySignature(inspect.Signature):
23252457
# Top-level to make it picklable;
23262458
# used in test_signature_object_pickle
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

+10
Original file line numberDiff line numberDiff line change
@@ -1552,6 +1552,15 @@ ag_getcode(PyGenObject *gen, void *Py_UNUSED(ignored))
15521552
return _gen_getcode(gen, "ag__code");
15531553
}
15541554

1555+
static PyObject *
1556+
ag_getsuspended(PyAsyncGenObject *ag, void *Py_UNUSED(ignored))
1557+
{
1558+
if (ag->ag_frame_state == FRAME_SUSPENDED) {
1559+
Py_RETURN_TRUE;
1560+
}
1561+
Py_RETURN_FALSE;
1562+
}
1563+
15551564
static PyGetSetDef async_gen_getsetlist[] = {
15561565
{"__name__", (getter)gen_get_name, (setter)gen_set_name,
15571566
PyDoc_STR("name of the async generator")},
@@ -1561,6 +1570,7 @@ static PyGetSetDef async_gen_getsetlist[] = {
15611570
PyDoc_STR("object being awaited on, or None")},
15621571
{"ag_frame", (getter)ag_getframe, NULL, NULL},
15631572
{"ag_code", (getter)ag_getcode, NULL, NULL},
1573+
{"ag_suspended", (getter)ag_getsuspended, NULL, NULL},
15641574
{NULL} /* Sentinel */
15651575
};
15661576

0 commit comments

Comments
 (0)