diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 5133f9f0c8e43b..f6bc904bdab4bd 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -262,6 +262,12 @@ attributes (see :ref:`import-mod-attrs` for module attributes): | | | ``yield from``, or | | | | ``None`` | +-----------------+-------------------+---------------------------+ +| | gi_state | state of the generator, | +| | | one of ``GEN_CREATED``, | +| | | ``GEN_RUNNING``, | +| | | ``GEN_SUSPENDED``, or | +| | | ``GEN_CLOSED`` | ++-----------------+-------------------+---------------------------+ | async generator | __name__ | name | +-----------------+-------------------+---------------------------+ | | __qualname__ | qualified name | @@ -278,6 +284,13 @@ attributes (see :ref:`import-mod-attrs` for module attributes): +-----------------+-------------------+---------------------------+ | | ag_code | code | +-----------------+-------------------+---------------------------+ +| | ag_state | state of the async | +| | | generator, one of | +| | | ``AGEN_CREATED``, | +| | | ``AGEN_RUNNING``, | +| | | ``AGEN_SUSPENDED``, or | +| | | ``AGEN_CLOSED`` | ++-----------------+-------------------+---------------------------+ | coroutine | __name__ | name | +-----------------+-------------------+---------------------------+ | | __qualname__ | qualified name | @@ -298,6 +311,12 @@ attributes (see :ref:`import-mod-attrs` for module attributes): | | | created, or ``None``. See | | | | |coroutine-origin-link| | +-----------------+-------------------+---------------------------+ +| | cr_state | state of the coroutine, | +| | | one of ``CORO_CREATED``, | +| | | ``CORO_RUNNING``, | +| | | ``CORO_SUSPENDED``, or | +| | | ``CORO_CLOSED`` | ++-----------------+-------------------+---------------------------+ | builtin | __doc__ | documentation string | +-----------------+-------------------+---------------------------+ | | __name__ | original name of this | @@ -341,6 +360,11 @@ attributes (see :ref:`import-mod-attrs` for module attributes): Add ``f_generator`` attribute to frames. +.. versionchanged:: next + + Add ``gi_state`` attribute to generators, ``cr_state`` attribute to + coroutines, and ``ag_state`` attribute to async generators. + .. function:: getmembers(object[, predicate]) Return all the members of an object in a list of ``(name, value)`` diff --git a/Include/internal/pycore_frame.h b/Include/internal/pycore_frame.h index 50908f2cb7a1d2..5e73ae3c8549b8 100644 --- a/Include/internal/pycore_frame.h +++ b/Include/internal/pycore_frame.h @@ -44,18 +44,16 @@ extern PyFrameObject* _PyFrame_New_NoTrack(PyCodeObject *code); /* other API */ typedef enum _framestate { - FRAME_CREATED = -4, - FRAME_SUSPENDED = -3, - FRAME_SUSPENDED_YIELD_FROM = -2, - FRAME_SUSPENDED_YIELD_FROM_LOCKED = -1, - FRAME_EXECUTING = 0, - FRAME_COMPLETED = 1, - FRAME_CLEARED = 4 + FRAME_CREATED = 0, + FRAME_SUSPENDED = 1, + FRAME_SUSPENDED_YIELD_FROM = 2, + FRAME_SUSPENDED_YIELD_FROM_LOCKED = 3, + FRAME_EXECUTING = 4, + FRAME_CLEARED = 5 } PyFrameState; #define FRAME_STATE_SUSPENDED(S) ((S) >= FRAME_SUSPENDED && (S) <= FRAME_SUSPENDED_YIELD_FROM_LOCKED) -#define FRAME_STATE_FINISHED(S) ((S) >= FRAME_COMPLETED) - +#define FRAME_STATE_FINISHED(S) ((S) == FRAME_CLEARED) #ifdef __cplusplus } #endif diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index fc297a2933a786..cb62d9424a86b0 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -1335,11 +1335,23 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(str_replace_inf)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(type_params)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(utf_8)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(AGEN_CLOSED)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(AGEN_CREATED)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(AGEN_RUNNING)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(AGEN_SUSPENDED)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(CANCELLED)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(CORO_CLOSED)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(CORO_CREATED)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(CORO_RUNNING)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(CORO_SUSPENDED)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(Emax)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(Emin)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(FINISHED)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(False)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(GEN_CLOSED)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(GEN_CREATED)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(GEN_RUNNING)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(GEN_SUSPENDED)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(JSONDecodeError)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(PENDING)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(Py_Repr)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 563ccd7cf6d3f4..92de92079379ea 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -58,11 +58,23 @@ struct _Py_global_strings { } literals; struct { + STRUCT_FOR_ID(AGEN_CLOSED) + STRUCT_FOR_ID(AGEN_CREATED) + STRUCT_FOR_ID(AGEN_RUNNING) + STRUCT_FOR_ID(AGEN_SUSPENDED) STRUCT_FOR_ID(CANCELLED) + STRUCT_FOR_ID(CORO_CLOSED) + STRUCT_FOR_ID(CORO_CREATED) + STRUCT_FOR_ID(CORO_RUNNING) + STRUCT_FOR_ID(CORO_SUSPENDED) STRUCT_FOR_ID(Emax) STRUCT_FOR_ID(Emin) STRUCT_FOR_ID(FINISHED) STRUCT_FOR_ID(False) + STRUCT_FOR_ID(GEN_CLOSED) + STRUCT_FOR_ID(GEN_CREATED) + STRUCT_FOR_ID(GEN_RUNNING) + STRUCT_FOR_ID(GEN_SUSPENDED) STRUCT_FOR_ID(JSONDecodeError) STRUCT_FOR_ID(PENDING) STRUCT_FOR_ID(Py_Repr) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index ba7c0e68434517..dc05495e20d69d 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1333,11 +1333,23 @@ extern "C" { } #define _Py_str_identifiers_INIT { \ + INIT_ID(AGEN_CLOSED), \ + INIT_ID(AGEN_CREATED), \ + INIT_ID(AGEN_RUNNING), \ + INIT_ID(AGEN_SUSPENDED), \ INIT_ID(CANCELLED), \ + INIT_ID(CORO_CLOSED), \ + INIT_ID(CORO_CREATED), \ + INIT_ID(CORO_RUNNING), \ + INIT_ID(CORO_SUSPENDED), \ INIT_ID(Emax), \ INIT_ID(Emin), \ INIT_ID(FINISHED), \ INIT_ID(False), \ + INIT_ID(GEN_CLOSED), \ + INIT_ID(GEN_CREATED), \ + INIT_ID(GEN_RUNNING), \ + INIT_ID(GEN_SUSPENDED), \ INIT_ID(JSONDecodeError), \ INIT_ID(PENDING), \ INIT_ID(Py_Repr), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 44063794293990..10085149f09b6c 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -12,10 +12,42 @@ extern "C" { static inline void _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { PyObject *string; + string = &_Py_ID(AGEN_CLOSED); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(AGEN_CREATED); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(AGEN_RUNNING); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(AGEN_SUSPENDED); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(CANCELLED); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(CORO_CLOSED); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(CORO_CREATED); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(CORO_RUNNING); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(CORO_SUSPENDED); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(Emax); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -32,6 +64,22 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(GEN_CLOSED); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(GEN_CREATED); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(GEN_RUNNING); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(GEN_SUSPENDED); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(JSONDecodeError); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Lib/inspect.py b/Lib/inspect.py index 0dba3c6628c6e5..0eed68d17c702b 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1813,13 +1813,7 @@ def getgeneratorstate(generator): GEN_SUSPENDED: Currently suspended at a yield expression. GEN_CLOSED: Execution has completed. """ - if generator.gi_running: - return GEN_RUNNING - if generator.gi_suspended: - return GEN_SUSPENDED - if generator.gi_frame is None: - return GEN_CLOSED - return GEN_CREATED + return generator.gi_state def getgeneratorlocals(generator): @@ -1855,13 +1849,7 @@ def getcoroutinestate(coroutine): CORO_SUSPENDED: Currently suspended at an await expression. CORO_CLOSED: Execution has completed. """ - if coroutine.cr_running: - return CORO_RUNNING - if coroutine.cr_suspended: - return CORO_SUSPENDED - if coroutine.cr_frame is None: - return CORO_CLOSED - return CORO_CREATED + return coroutine.cr_state def getcoroutinelocals(coroutine): @@ -1894,13 +1882,7 @@ def getasyncgenstate(agen): 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 + return agen.ag_state def getasyncgenlocals(agen): diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py index c181da2e349915..6eb25960101a9e 100644 --- a/Lib/test/test_generators.py +++ b/Lib/test/test_generators.py @@ -1366,7 +1366,7 @@ def b(): >>> type(i) >>> [s for s in dir(i) if not s.startswith('_')] -['close', 'gi_code', 'gi_frame', 'gi_running', 'gi_suspended', 'gi_yieldfrom', 'send', 'throw'] +['close', 'gi_code', 'gi_frame', 'gi_running', 'gi_state', 'gi_suspended', 'gi_yieldfrom', 'send', 'throw'] >>> from test.support import HAVE_DOCSTRINGS >>> print(i.__next__.__doc__ if HAVE_DOCSTRINGS else 'Implement next(self).') Implement next(self). diff --git a/Lib/types.py b/Lib/types.py index 73a69c40c8d4b8..99f23c3f44270f 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -279,6 +279,12 @@ def gi_yieldfrom(self): @property def gi_suspended(self): return self.__wrapped.gi_suspended + @property + def gi_state(self): + return self.__wrapped.gi_state + @property + def cr_state(self): + return self.__wrapped.gi_state.replace('GEN_', 'CORO_') cr_code = gi_code cr_frame = gi_frame cr_running = gi_running diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-02-17-50-14.gh-issue-120321.Xfr7tL.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-02-17-50-14.gh-issue-120321.Xfr7tL.rst new file mode 100644 index 00000000000000..3e868c837839e2 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-02-17-50-14.gh-issue-120321.Xfr7tL.rst @@ -0,0 +1,5 @@ +Add ``gi_state``, ``cr_state``, and ``ag_state`` attributes to generators, +coroutines, and async generators that return the current state as a string +(e.g., ``GEN_RUNNING``). The :mod:`inspect` module functions +:func:`~inspect.getgeneratorstate`, :func:`~inspect.getcoroutinestate`, and +:func:`~inspect.getasyncgenstate` now return these attributes directly. diff --git a/Objects/genobject.c b/Objects/genobject.c index 5ff4618255c852..5088500fc4142b 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -926,6 +926,26 @@ gen_getsuspended(PyObject *self, void *Py_UNUSED(ignored)) return FRAME_STATE_SUSPENDED(frame_state) ? Py_True : Py_False; } +static PyObject * +gen_getstate(PyObject *self, void *Py_UNUSED(ignored)) +{ + PyGenObject *gen = _PyGen_CAST(self); + int8_t frame_state = FT_ATOMIC_LOAD_INT8_RELAXED(gen->gi_frame_state); + + static PyObject *const state_strings[] = { + [FRAME_CREATED] = &_Py_ID(GEN_CREATED), + [FRAME_SUSPENDED] = &_Py_ID(GEN_SUSPENDED), + [FRAME_SUSPENDED_YIELD_FROM] = &_Py_ID(GEN_SUSPENDED), + [FRAME_SUSPENDED_YIELD_FROM_LOCKED] = &_Py_ID(GEN_SUSPENDED), + [FRAME_EXECUTING] = &_Py_ID(GEN_RUNNING), + [FRAME_CLEARED] = &_Py_ID(GEN_CLOSED), + }; + + assert(frame_state >= 0 && + (size_t)frame_state < Py_ARRAY_LENGTH(state_strings)); + return state_strings[frame_state]; +} + static PyObject * _gen_getframe(PyGenObject *gen, const char *const name) { @@ -974,6 +994,8 @@ static PyGetSetDef gen_getsetlist[] = { {"gi_frame", gen_getframe, NULL, NULL}, {"gi_suspended", gen_getsuspended, NULL, NULL}, {"gi_code", gen_getcode, NULL, NULL}, + {"gi_state", gen_getstate, NULL, + PyDoc_STR("state of the generator")}, {NULL} /* Sentinel */ }; @@ -1291,6 +1313,26 @@ cr_getcode(PyObject *coro, void *Py_UNUSED(ignored)) return _gen_getcode(_PyGen_CAST(coro), "cr_code"); } +static PyObject * +cr_getstate(PyObject *self, void *Py_UNUSED(ignored)) +{ + PyGenObject *gen = _PyGen_CAST(self); + int8_t frame_state = FT_ATOMIC_LOAD_INT8_RELAXED(gen->gi_frame_state); + + static PyObject *const state_strings[] = { + [FRAME_CREATED] = &_Py_ID(CORO_CREATED), + [FRAME_SUSPENDED] = &_Py_ID(CORO_SUSPENDED), + [FRAME_SUSPENDED_YIELD_FROM] = &_Py_ID(CORO_SUSPENDED), + [FRAME_SUSPENDED_YIELD_FROM_LOCKED] = &_Py_ID(CORO_SUSPENDED), + [FRAME_EXECUTING] = &_Py_ID(CORO_RUNNING), + [FRAME_CLEARED] = &_Py_ID(CORO_CLOSED), + }; + + assert(frame_state >= 0 && + (size_t)frame_state < Py_ARRAY_LENGTH(state_strings)); + return state_strings[frame_state]; +} + static PyGetSetDef coro_getsetlist[] = { {"__name__", gen_get_name, gen_set_name, PyDoc_STR("name of the coroutine")}, @@ -1302,6 +1344,8 @@ static PyGetSetDef coro_getsetlist[] = { {"cr_frame", cr_getframe, NULL, NULL}, {"cr_code", cr_getcode, NULL, NULL}, {"cr_suspended", gen_getsuspended, NULL, NULL}, + {"cr_state", cr_getstate, NULL, + PyDoc_STR("state of the coroutine")}, {NULL} /* Sentinel */ }; @@ -1717,6 +1761,26 @@ ag_getcode(PyObject *gen, void *Py_UNUSED(ignored)) return _gen_getcode((PyGenObject*)gen, "ag_code"); } +static PyObject * +ag_getstate(PyObject *self, void *Py_UNUSED(ignored)) +{ + PyGenObject *gen = _PyGen_CAST(self); + int8_t frame_state = FT_ATOMIC_LOAD_INT8_RELAXED(gen->gi_frame_state); + + static PyObject *const state_strings[] = { + [FRAME_CREATED] = &_Py_ID(AGEN_CREATED), + [FRAME_SUSPENDED] = &_Py_ID(AGEN_SUSPENDED), + [FRAME_SUSPENDED_YIELD_FROM] = &_Py_ID(AGEN_SUSPENDED), + [FRAME_SUSPENDED_YIELD_FROM_LOCKED] = &_Py_ID(AGEN_SUSPENDED), + [FRAME_EXECUTING] = &_Py_ID(AGEN_RUNNING), + [FRAME_CLEARED] = &_Py_ID(AGEN_CLOSED), + }; + + assert(frame_state >= 0 && + (size_t)frame_state < Py_ARRAY_LENGTH(state_strings)); + return state_strings[frame_state]; +} + static PyGetSetDef async_gen_getsetlist[] = { {"__name__", gen_get_name, gen_set_name, PyDoc_STR("name of the async generator")}, @@ -1727,6 +1791,8 @@ static PyGetSetDef async_gen_getsetlist[] = { {"ag_frame", ag_getframe, NULL, NULL}, {"ag_code", ag_getcode, NULL, NULL}, {"ag_suspended", gen_getsuspended, NULL, NULL}, + {"ag_state", ag_getstate, NULL, + PyDoc_STR("state of the async generator")}, {NULL} /* Sentinel */ }; diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index 6ad3fc5f76e57a..91bbf94990ecc1 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -332,6 +332,9 @@ Objects/complexobject.c - c_1 - Objects/exceptions.c - static_exceptions - Objects/genobject.c - ASYNC_GEN_IGNORED_EXIT_MSG - Objects/genobject.c - NON_INIT_CORO_MSG - +Objects/genobject.c gen_getstate state_strings - +Objects/genobject.c cr_getstate state_strings - +Objects/genobject.c ag_getstate state_strings - Objects/longobject.c - _PyLong_DigitValue - Objects/longobject.c - PyLong_LAYOUT - Objects/object.c - _Py_SwappedOp -