diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index 9cc25eda44f9de1..2778d26dff3dae9 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -924,6 +924,8 @@ struct _is { PyObject *sysdict_copy; PyObject *builtins_copy; + // _pybuiltins._anext_with_default(), used by anext() with a default. + PyObject *anext_with_default; // Initialized to _PyEval_EvalFrameDefault(). _PyFrameEvalFunction eval_frame; int eval_frame_allow_specialization; diff --git a/Lib/_pybuiltins.py b/Lib/_pybuiltins.py index 01da295f53d0c73..b0e0b24d75fc5dd 100644 --- a/Lib/_pybuiltins.py +++ b/Lib/_pybuiltins.py @@ -5,38 +5,12 @@ copied into the ``builtins`` module. """ -__all__ = ['anext'] - -_NOT_GIVEN = sentinel("_NOT_GIVEN") - - -def anext(async_iterator, default=_NOT_GIVEN, /): - """Return the next item from the async iterator. - - If default is given and the async iterator is exhausted, - it is returned instead of raising StopAsyncIteration. - """ - cls = type(async_iterator) - try: - # Looked up on the type, like the C slot am_anext. - anext_method = cls.__anext__ - except AttributeError: - raise TypeError( - f"{cls.__name__!r} object is not an async iterator" - ) from None - awaitable = anext_method(async_iterator) - if default is _NOT_GIVEN: - return awaitable - return _anext_with_default(awaitable, default) +__all__ = [] +# Used by the C implementation of anext() when a default is given. async def _anext_with_default(awaitable, default): try: return await awaitable except StopAsyncIteration: return default - - -for _name in __all__: - globals()[_name].__module__ = 'builtins' -del _name diff --git a/Lib/test/test_asyncgen.py b/Lib/test/test_asyncgen.py index b5e0891feb794e8..989b9e50ab62b1c 100644 --- a/Lib/test/test_asyncgen.py +++ b/Lib/test/test_asyncgen.py @@ -1048,9 +1048,10 @@ async def do_test(): self.assertEqual(result, "completed") def test_anext_traceback_filename(self): - # anext() is implemented in Python in Lib/_pybuiltins.py, which is - # frozen under the builtins ID, so its frames name builtins rather - # than the module they are frozen from. + # The coroutine awaited by anext() with a default is implemented in + # Python in Lib/_pybuiltins.py, which is frozen under the builtins + # ID, so its frames name builtins rather than the module they are + # frozen from. def filenames(exc): return [frame.filename for frame in traceback.extract_tb(exc.__traceback__)] @@ -1063,13 +1064,6 @@ async def __anext__(self): # assertRaises() drops the traceback, so catch the exceptions here. async def do_test(): - try: - anext(42, "default") - except TypeError as exc: - self.assertIn("", filenames(exc)) - else: - self.fail("TypeError was not raised") - try: await anext(AIter(), "default") except ZeroDivisionError as exc: diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 3f14a9e66226eaa..7b15213af18f0e8 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -6229,7 +6229,7 @@ def test_builtins_have_signatures(self): needs_groups = {"range", "slice", "getattr", "next"} no_signature |= needs_groups # These have unrepresentable parameter default values of NULL - unsupported_signature = {"aiter", "iter", "dir", "vars"} + unsupported_signature = {"anext", "aiter", "iter", "dir", "vars"} # These need *args support in Argument Clinic needs_varargs = {"min", "max", "__build_class__"} no_signature |= needs_varargs diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-29-18-00-00.gh-issue-158407.anxtFst.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-29-18-00-00.gh-issue-158407.anxtFst.rst new file mode 100644 index 000000000000000..6dc8a84c3f125ad --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-29-18-00-00.gh-issue-158407.anxtFst.rst @@ -0,0 +1,3 @@ +Fix a performance regression in :func:`anext` introduced in :gh:`157361`. +The one-argument form is implemented in C again and calls the async +iterator's :meth:`~object.__anext__` slot directly. diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index 796897ff24cdc02..91848099fb0a998 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -1963,6 +1963,53 @@ builtin_aiter_impl(PyObject *module, PyObject *object, PyObject *stop_value, return _PyACallIter_New(object, stop_value, stop_exception); } +/*[clinic input] +anext as builtin_anext + + async_iterator as aiterator: object + default: object = NULL + / + +Return the next item from the async iterator. + +If default is given and the async iterator is exhausted, +it is returned instead of raising StopAsyncIteration. +[clinic start generated code]*/ + +static PyObject * +builtin_anext_impl(PyObject *module, PyObject *aiterator, + PyObject *default_value) +/*[clinic end generated code: output=f02c060c163a81fa input=f3dc5a93f073e5ac]*/ +{ + PyTypeObject *t = Py_TYPE(aiterator); + if (t->tp_as_async == NULL || t->tp_as_async->am_anext == NULL) { + PyErr_Format(PyExc_TypeError, + "'%.200s' object is not an async iterator", + t->tp_name); + return NULL; + } + + PyObject *awaitable = (*t->tp_as_async->am_anext)(aiterator); + if (awaitable == NULL || default_value == NULL) { + return awaitable; + } + + /* gh-157361: the default is handled by a Python coroutine so that + introspection tools can see through it into the awaitable. */ + PyObject *helper = _PyInterpreterState_GET()->anext_with_default; + if (helper == NULL) { + Py_DECREF(awaitable); + PyErr_SetString(PyExc_RuntimeError, + "anext() with a default is not available"); + return NULL; + } + PyObject *args[2] = {awaitable, default_value}; + PyObject *res = PyObject_Vectorcall(helper, args, 2, NULL); + Py_DECREF(awaitable); + return res; +} + + /*[clinic input] len as builtin_len @@ -3461,6 +3508,7 @@ static PyMethodDef builtin_methods[] = { {"max", _PyCFunction_CAST(builtin_max), METH_FASTCALL | METH_KEYWORDS, max_doc}, {"min", _PyCFunction_CAST(builtin_min), METH_FASTCALL | METH_KEYWORDS, min_doc}, {"next", _PyCFunction_CAST(builtin_next), METH_FASTCALL, next_doc}, + BUILTIN_ANEXT_METHODDEF BUILTIN_OCT_METHODDEF BUILTIN_ORD_METHODDEF BUILTIN_POW_METHODDEF @@ -3504,7 +3552,8 @@ static struct PyModuleDef builtinsmodule = { Lib/_pybuiltins.py is frozen into the interpreter as a bootstrap module (see Tools/build/freeze_modules.py), so it can be imported here before the import system exists. The names in its __all__ are copied into the - builtins dict. */ + builtins dict, and helpers used by C builtins are kept on the + interpreter. */ int _PyBuiltin_InitPythonFunctions(PyObject *dict) @@ -3542,6 +3591,13 @@ _PyBuiltin_InitPythonFunctions(PyObject *dict) goto done; } } + + PyInterpreterState *interp = _PyInterpreterState_GET(); + interp->anext_with_default = PyObject_GetAttrString( + mod, "_anext_with_default"); + if (interp->anext_with_default == NULL) { + goto done; + } rc = 0; done: diff --git a/Python/clinic/bltinmodule.c.h b/Python/clinic/bltinmodule.c.h index 0a32ca28da2ef2d..229e30fd2e638f7 100644 --- a/Python/clinic/bltinmodule.c.h +++ b/Python/clinic/bltinmodule.c.h @@ -1050,6 +1050,44 @@ builtin_aiter(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObjec return return_value; } +PyDoc_STRVAR(builtin_anext__doc__, +"anext($module, async_iterator, default=, /)\n" +"--\n" +"\n" +"Return the next item from the async iterator.\n" +"\n" +"If default is given and the async iterator is exhausted,\n" +"it is returned instead of raising StopAsyncIteration."); + +#define BUILTIN_ANEXT_METHODDEF \ + {"anext", _PyCFunction_CAST(builtin_anext), METH_FASTCALL, builtin_anext__doc__}, + +static PyObject * +builtin_anext_impl(PyObject *module, PyObject *aiterator, + PyObject *default_value); + +static PyObject * +builtin_anext(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + PyObject *aiterator; + PyObject *default_value = NULL; + + if (!_PyArg_CheckPositional("anext", nargs, 1, 2)) { + goto exit; + } + aiterator = args[0]; + if (nargs < 2) { + goto skip_optional; + } + default_value = args[1]; +skip_optional: + return_value = builtin_anext_impl(module, aiterator, default_value); + +exit: + return return_value; +} + PyDoc_STRVAR(builtin_len__doc__, "len($module, obj, /)\n" "--\n" @@ -1575,4 +1613,4 @@ builtin_issubclass(PyObject *module, PyObject *const *args, Py_ssize_t nargs) exit: return return_value; } -/*[clinic end generated code: output=ac4800dc25f87f6e input=a9049054013a1b77]*/ +/*[clinic end generated code: output=fbacde0daee9f78e input=a9049054013a1b77]*/ diff --git a/Python/pystate.c b/Python/pystate.c index 97323ea2ba2bce1..7ddd82b1a7e8308 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -891,6 +891,7 @@ interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate) Py_CLEAR(interp->sysdict_copy); Py_CLEAR(interp->builtins_copy); + Py_CLEAR(interp->anext_with_default); Py_CLEAR(interp->dict); #ifdef HAVE_FORK Py_CLEAR(interp->before_forkers);