Skip to content

Commit 37a7c35

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 b108db6 commit 37a7c35

File tree

4 files changed

+204
-2
lines changed

4 files changed

+204
-2
lines changed

Doc/library/inspect.rst

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1352,8 +1352,8 @@ code execution::
13521352
pass
13531353

13541354

1355-
Current State of Generators and Coroutines
1356-
------------------------------------------
1355+
Current State of Generators, Coroutines, and Asynchronous Generators
1356+
--------------------------------------------------------------------
13571357

13581358
When implementing coroutine schedulers and for other advanced uses of
13591359
generators, it is useful to determine whether a generator is currently
@@ -1388,6 +1388,22 @@ generator to be determined easily.
13881388

13891389
.. versionadded:: 3.5
13901390

1391+
.. function:: getasyncgenstate(agen)
1392+
1393+
Get current state of an asynchronous generator object. The function is
1394+
intended to be used with asynchronous iterator objects created by
1395+
:keyword:`async def` functions which use the :keyword:`yield` statement,
1396+
but will accept any asynchronous generator-like object that has
1397+
``ag_running`` and ``ag_frame`` attributes.
1398+
1399+
Possible states are:
1400+
* AGEN_CREATED: Waiting to start execution.
1401+
* AGEN_RUNNING: Currently being executed by the interpreter.
1402+
* AGEN_SUSPENDED: Currently suspended at a yield expression.
1403+
* AGEN_CLOSED: Execution has completed.
1404+
1405+
.. versionadded:: 3.11
1406+
13911407
The current internal state of the generator can also be queried. This is
13921408
mostly useful for testing purposes, to ensure that internal state is being
13931409
updated as expected:
@@ -1419,6 +1435,14 @@ updated as expected:
14191435

14201436
.. versionadded:: 3.5
14211437

1438+
.. function:: getasyncgenlocals(agen)
1439+
1440+
This function is analogous to :func:`~inspect.getgeneratorlocals`, but
1441+
works for asynchronous generator objects created by :keyword:`async def`
1442+
functions which use the :keyword:`yield` statement.
1443+
1444+
.. versionadded:: 3.11
1445+
14221446

14231447
.. _inspect-module-co-flags:
14241448

Lib/inspect.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1852,6 +1852,50 @@ def getcoroutinelocals(coroutine):
18521852
return {}
18531853

18541854

1855+
# ----------------------------------- asynchronous generator introspection
1856+
1857+
AGEN_CREATED = 'AGEN_CREATED'
1858+
AGEN_RUNNING = 'AGEN_RUNNING'
1859+
AGEN_SUSPENDED = 'AGEN_SUSPENDED'
1860+
AGEN_CLOSED = 'AGEN_CLOSED'
1861+
1862+
1863+
def getasyncgenstate(agen):
1864+
"""Get current state of an asynchronous generator object.
1865+
1866+
Possible states are:
1867+
AGEN_CREATED: Waiting to start execution.
1868+
AGEN_RUNNING: Currently being executed by the interpreter.
1869+
AGEN_SUSPENDED: Currently suspended at a yield expression.
1870+
AGEN_CLOSED: Execution has completed.
1871+
"""
1872+
if agen.ag_running:
1873+
return AGEN_RUNNING
1874+
if agen.ag_frame is None:
1875+
return AGEN_CLOSED
1876+
if agen.ag_frame.f_lasti == -1:
1877+
return AGEN_CREATED
1878+
return AGEN_SUSPENDED
1879+
1880+
1881+
def getasyncgenlocals(agen):
1882+
"""
1883+
Get the mapping of asynchronous generator local variables to their current
1884+
values.
1885+
1886+
A dict is returned, with the keys the local variable names and values the
1887+
bound values."""
1888+
1889+
if not isasyncgen(agen):
1890+
raise TypeError("{!r} is not a Python async generator".format(agen))
1891+
1892+
frame = getattr(agen, "ag_frame", None)
1893+
if frame is not None:
1894+
return agen.ag_frame.f_locals
1895+
else:
1896+
return {}
1897+
1898+
18551899
###############################################################################
18561900
### Function Signature Object (PEP 362)
18571901
###############################################################################

Lib/test/test_inspect.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2229,6 +2229,138 @@ async def func(a=None):
22292229
{'a': None, 'gencoro': gencoro, 'b': 'spam'})
22302230

22312231

2232+
class TestGetAsyncGenState(unittest.TestCase):
2233+
2234+
def setUp(self):
2235+
async def number_asyncgen():
2236+
for number in range(5):
2237+
yield number
2238+
self.asyncgen = number_asyncgen()
2239+
2240+
def tearDown(self):
2241+
try:
2242+
self.asyncgen.aclose().send(None)
2243+
except StopIteration:
2244+
pass
2245+
2246+
def _asyncgenstate(self):
2247+
return inspect.getasyncgenstate(self.asyncgen)
2248+
2249+
def test_created(self):
2250+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_CREATED)
2251+
2252+
def test_suspended(self):
2253+
try:
2254+
next(self.asyncgen.__anext__())
2255+
except StopIteration as exc:
2256+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_SUSPENDED)
2257+
self.assertEqual(exc.args, (0,))
2258+
2259+
def test_closed_after_exhaustion(self):
2260+
while True:
2261+
try:
2262+
next(self.asyncgen.__anext__())
2263+
except StopAsyncIteration:
2264+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED)
2265+
break
2266+
except StopIteration as exc:
2267+
if exc.args is None:
2268+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED)
2269+
break
2270+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED)
2271+
2272+
def test_closed_after_immediate_exception(self):
2273+
with self.assertRaises(RuntimeError):
2274+
self.asyncgen.athrow(RuntimeError).send(None)
2275+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED)
2276+
2277+
def test_running(self):
2278+
async def running_check_asyncgen():
2279+
for number in range(5):
2280+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_RUNNING)
2281+
yield number
2282+
self.assertEqual(self._asyncgenstate(), inspect.AGEN_RUNNING)
2283+
self.asyncgen = running_check_asyncgen()
2284+
# Running up to the first yield
2285+
try:
2286+
next(self.asyncgen.__anext__())
2287+
except StopIteration:
2288+
pass
2289+
# Running after the first yield
2290+
try:
2291+
next(self.asyncgen.__anext__())
2292+
except StopIteration:
2293+
pass
2294+
2295+
def test_easy_debugging(self):
2296+
# repr() and str() of a asyncgen state should contain the state name
2297+
names = 'AGEN_CREATED AGEN_RUNNING AGEN_SUSPENDED AGEN_CLOSED'.split()
2298+
for name in names:
2299+
state = getattr(inspect, name)
2300+
self.assertIn(name, repr(state))
2301+
self.assertIn(name, str(state))
2302+
2303+
def test_getasyncgenlocals(self):
2304+
async def each(lst, a=None):
2305+
b=(1, 2, 3)
2306+
for v in lst:
2307+
if v == 3:
2308+
c = 12
2309+
yield v
2310+
2311+
numbers = each([1, 2, 3])
2312+
self.assertEqual(inspect.getasyncgenlocals(numbers),
2313+
{'a': None, 'lst': [1, 2, 3]})
2314+
try:
2315+
next(numbers.__anext__())
2316+
except StopIteration:
2317+
pass
2318+
self.assertEqual(inspect.getasyncgenlocals(numbers),
2319+
{'a': None, 'lst': [1, 2, 3], 'v': 1,
2320+
'b': (1, 2, 3)})
2321+
try:
2322+
next(numbers.__anext__())
2323+
except StopIteration:
2324+
pass
2325+
self.assertEqual(inspect.getasyncgenlocals(numbers),
2326+
{'a': None, 'lst': [1, 2, 3], 'v': 2,
2327+
'b': (1, 2, 3)})
2328+
try:
2329+
next(numbers.__anext__())
2330+
except StopIteration:
2331+
pass
2332+
self.assertEqual(inspect.getasyncgenlocals(numbers),
2333+
{'a': None, 'lst': [1, 2, 3], 'v': 3,
2334+
'b': (1, 2, 3), 'c': 12})
2335+
try:
2336+
next(numbers.__anext__())
2337+
except StopAsyncIteration:
2338+
pass
2339+
self.assertEqual(inspect.getasyncgenlocals(numbers), {})
2340+
2341+
def test_getasyncgenlocals_empty(self):
2342+
async def yield_one():
2343+
yield 1
2344+
one = yield_one()
2345+
self.assertEqual(inspect.getasyncgenlocals(one), {})
2346+
try:
2347+
next(one.__anext__())
2348+
except StopIteration:
2349+
pass
2350+
self.assertEqual(inspect.getasyncgenlocals(one), {})
2351+
try:
2352+
next(one.__anext__())
2353+
except StopAsyncIteration:
2354+
pass
2355+
self.assertEqual(inspect.getasyncgenlocals(one), {})
2356+
2357+
def test_getasyncgenlocals_error(self):
2358+
self.assertRaises(TypeError, inspect.getasyncgenlocals, 1)
2359+
self.assertRaises(TypeError, inspect.getasyncgenlocals, lambda x: True)
2360+
self.assertRaises(TypeError, inspect.getasyncgenlocals, set)
2361+
self.assertRaises(TypeError, inspect.getasyncgenlocals, (2,3))
2362+
2363+
22322364
class MySignature(inspect.Signature):
22332365
# Top-level to make it picklable;
22342366
# 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.

0 commit comments

Comments
 (0)