summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorThomas Krennwallner <tk@postsubmeta.net>2023-03-11 13:19:40 (GMT)
committerGitHub <noreply@github.com>2023-03-11 13:19:40 (GMT)
commitced13c96a4eb9391a9d27e3e13218f70c579670f (patch)
tree2e48fbdb4e1539ae821c5b3ac0a7a70e6d1fbf93 /Lib
parentaa0a73d1bc53dcb6348a869df1e775138991e561 (diff)
downloadcpython-ced13c96a4eb9391a9d27e3e13218f70c579670f.zip
cpython-ced13c96a4eb9391a9d27e3e13218f70c579670f.tar.gz
cpython-ced13c96a4eb9391a9d27e3e13218f70c579670f.tar.bz2
gh-79940: add introspection API for asynchronous generators to `inspect` module (#11590)
Diffstat (limited to 'Lib')
-rw-r--r--Lib/inspect.py50
-rw-r--r--Lib/test/test_inspect.py107
2 files changed, 157 insertions, 0 deletions
diff --git a/Lib/inspect.py b/Lib/inspect.py
index edc23b0..0eceaaf 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -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)
###############################################################################
diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py
index 02f8378..410a2e5 100644
--- a/Lib/test/test_inspect.py
+++ b/Lib/test/test_inspect.py
@@ -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