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
|
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
|
When implementing coroutine schedulers and for other advanced uses of
|
||||||
generators, it is useful to determine whether a generator is currently
|
generators, it is useful to determine whether a generator is currently
|
||||||
|
@ -1476,6 +1476,22 @@ generator to be determined easily.
|
||||||
|
|
||||||
.. versionadded:: 3.5
|
.. 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
|
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
|
mostly useful for testing purposes, to ensure that internal state is being
|
||||||
updated as expected:
|
updated as expected:
|
||||||
|
@ -1507,6 +1523,14 @@ updated as expected:
|
||||||
|
|
||||||
.. versionadded:: 3.5
|
.. 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:
|
.. _inspect-module-co-flags:
|
||||||
|
|
||||||
|
|
|
@ -244,6 +244,10 @@ inspect
|
||||||
a :term:`coroutine` for use with :func:`iscoroutinefunction`.
|
a :term:`coroutine` for use with :func:`iscoroutinefunction`.
|
||||||
(Contributed Carlton Gibson in :gh:`99247`.)
|
(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
|
pathlib
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,10 @@ __author__ = ('Ka-Ping Yee <ping@lfw.org>',
|
||||||
'Yury Selivanov <yselivanov@sprymix.com>')
|
'Yury Selivanov <yselivanov@sprymix.com>')
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"AGEN_CLOSED",
|
||||||
|
"AGEN_CREATED",
|
||||||
|
"AGEN_RUNNING",
|
||||||
|
"AGEN_SUSPENDED",
|
||||||
"ArgInfo",
|
"ArgInfo",
|
||||||
"Arguments",
|
"Arguments",
|
||||||
"Attribute",
|
"Attribute",
|
||||||
|
@ -77,6 +81,8 @@ __all__ = [
|
||||||
"getabsfile",
|
"getabsfile",
|
||||||
"getargs",
|
"getargs",
|
||||||
"getargvalues",
|
"getargvalues",
|
||||||
|
"getasyncgenlocals",
|
||||||
|
"getasyncgenstate",
|
||||||
"getattr_static",
|
"getattr_static",
|
||||||
"getblock",
|
"getblock",
|
||||||
"getcallargs",
|
"getcallargs",
|
||||||
|
@ -1935,6 +1941,50 @@ def getcoroutinelocals(coroutine):
|
||||||
return {}
|
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)
|
### Function Signature Object (PEP 362)
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import asyncio
|
||||||
import builtins
|
import builtins
|
||||||
import collections
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
|
@ -65,6 +66,10 @@ def revise(filename, *args):
|
||||||
git = mod.StupidGit()
|
git = mod.StupidGit()
|
||||||
|
|
||||||
|
|
||||||
|
def tearDownModule():
|
||||||
|
asyncio.set_event_loop_policy(None)
|
||||||
|
|
||||||
|
|
||||||
def signatures_with_lexicographic_keyword_only_parameters():
|
def signatures_with_lexicographic_keyword_only_parameters():
|
||||||
"""
|
"""
|
||||||
Yields a whole bunch of functions with only 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'})
|
{'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):
|
class MySignature(inspect.Signature):
|
||||||
# Top-level to make it picklable;
|
# Top-level to make it picklable;
|
||||||
# used in test_signature_object_pickle
|
# 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");
|
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[] = {
|
static PyGetSetDef async_gen_getsetlist[] = {
|
||||||
{"__name__", (getter)gen_get_name, (setter)gen_set_name,
|
{"__name__", (getter)gen_get_name, (setter)gen_set_name,
|
||||||
PyDoc_STR("name of the async generator")},
|
PyDoc_STR("name of the async generator")},
|
||||||
|
@ -1529,6 +1538,7 @@ static PyGetSetDef async_gen_getsetlist[] = {
|
||||||
PyDoc_STR("object being awaited on, or None")},
|
PyDoc_STR("object being awaited on, or None")},
|
||||||
{"ag_frame", (getter)ag_getframe, NULL, NULL},
|
{"ag_frame", (getter)ag_getframe, NULL, NULL},
|
||||||
{"ag_code", (getter)ag_getcode, NULL, NULL},
|
{"ag_code", (getter)ag_getcode, NULL, NULL},
|
||||||
|
{"ag_suspended", (getter)ag_getsuspended, NULL, NULL},
|
||||||
{NULL} /* Sentinel */
|
{NULL} /* Sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue