mirror of https://github.com/python/cpython
gh-79940: add introspection API for asynchronous generators to `inspect` module (#11590)
This commit is contained in:
parent
aa0a73d1bc
commit
ced13c96a4
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
||||
-------
|
||||
|
||||
|
|
|
@ -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)
|
||||
###############################################################################
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
Add :func:`inspect.getasyncgenstate` and :func:`inspect.getasyncgenlocals`.
|
||||
Patch by Thomas Krennwallner.
|
|
@ -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 */
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue