Skip to content

Commit 8a8a809

Browse files
gh-158407: Restore the C fast path for anext()
gh-157361 moved anext() to Python, which added a Python frame, a type() call and an attribute lookup to every call, and made the call itself go through the __anext__ slot wrapper for C types such as async generators. Implement anext() in C again, calling the am_anext slot directly. The two-argument form keeps using the Python coroutine from _pybuiltins, so that introspection tools can still see through it into the awaitable.
1 parent 37cc8dd commit 8a8a809

8 files changed

Lines changed: 109 additions & 41 deletions

File tree

‎Include/internal/pycore_interp_structs.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,8 @@ struct _is {
924924

925925
PyObject *sysdict_copy;
926926
PyObject *builtins_copy;
927+
// _pybuiltins._anext_with_default(), used by anext() with a default.
928+
PyObject *anext_with_default;
927929
// Initialized to _PyEval_EvalFrameDefault().
928930
_PyFrameEvalFunction eval_frame;
929931
int eval_frame_allow_specialization;

‎Lib/_pybuiltins.py‎

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,12 @@
55
copied into the ``builtins`` module.
66
"""
77

8-
__all__ = ['anext']
9-
10-
_NOT_GIVEN = sentinel("_NOT_GIVEN")
11-
12-
13-
def anext(async_iterator, default=_NOT_GIVEN, /):
14-
"""Return the next item from the async iterator.
15-
16-
If default is given and the async iterator is exhausted,
17-
it is returned instead of raising StopAsyncIteration.
18-
"""
19-
cls = type(async_iterator)
20-
try:
21-
# Looked up on the type, like the C slot am_anext.
22-
anext_method = cls.__anext__
23-
except AttributeError:
24-
raise TypeError(
25-
f"{cls.__name__!r} object is not an async iterator"
26-
) from None
27-
awaitable = anext_method(async_iterator)
28-
if default is _NOT_GIVEN:
29-
return awaitable
30-
return _anext_with_default(awaitable, default)
8+
__all__ = []
319

3210

11+
# Used by the C implementation of anext() when a default is given.
3312
async def _anext_with_default(awaitable, default):
3413
try:
3514
return await awaitable
3615
except StopAsyncIteration:
3716
return default
38-
39-
40-
for _name in __all__:
41-
globals()[_name].__module__ = 'builtins'
42-
del _name

‎Lib/test/test_asyncgen.py‎

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,9 +1048,10 @@ async def do_test():
10481048
self.assertEqual(result, "completed")
10491049

10501050
def test_anext_traceback_filename(self):
1051-
# anext() is implemented in Python in Lib/_pybuiltins.py, which is
1052-
# frozen under the builtins ID, so its frames name builtins rather
1053-
# than the module they are frozen from.
1051+
# The coroutine awaited by anext() with a default is implemented in
1052+
# Python in Lib/_pybuiltins.py, which is frozen under the builtins
1053+
# ID, so its frames name builtins rather than the module they are
1054+
# frozen from.
10541055
def filenames(exc):
10551056
return [frame.filename
10561057
for frame in traceback.extract_tb(exc.__traceback__)]
@@ -1063,13 +1064,6 @@ async def __anext__(self):
10631064

10641065
# assertRaises() drops the traceback, so catch the exceptions here.
10651066
async def do_test():
1066-
try:
1067-
anext(42, "default")
1068-
except TypeError as exc:
1069-
self.assertIn("<frozen builtins>", filenames(exc))
1070-
else:
1071-
self.fail("TypeError was not raised")
1072-
10731067
try:
10741068
await anext(AIter(), "default")
10751069
except ZeroDivisionError as exc:

‎Lib/test/test_inspect/test_inspect.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6229,7 +6229,7 @@ def test_builtins_have_signatures(self):
62296229
needs_groups = {"range", "slice", "getattr", "next"}
62306230
no_signature |= needs_groups
62316231
# These have unrepresentable parameter default values of NULL
6232-
unsupported_signature = {"aiter", "iter", "dir", "vars"}
6232+
unsupported_signature = {"anext", "aiter", "iter", "dir", "vars"}
62336233
# These need *args support in Argument Clinic
62346234
needs_varargs = {"min", "max", "__build_class__"}
62356235
no_signature |= needs_varargs
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a performance regression in :func:`anext` introduced in :gh:`157361`.
2+
The one-argument form is implemented in C again and calls the async
3+
iterator's :meth:`~object.__anext__` slot directly.

‎Python/bltinmodule.c‎

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1963,6 +1963,53 @@ builtin_aiter_impl(PyObject *module, PyObject *object, PyObject *stop_value,
19631963
return _PyACallIter_New(object, stop_value, stop_exception);
19641964
}
19651965

1966+
/*[clinic input]
1967+
anext as builtin_anext
1968+
1969+
async_iterator as aiterator: object
1970+
default: object = NULL
1971+
/
1972+
1973+
Return the next item from the async iterator.
1974+
1975+
If default is given and the async iterator is exhausted,
1976+
it is returned instead of raising StopAsyncIteration.
1977+
[clinic start generated code]*/
1978+
1979+
static PyObject *
1980+
builtin_anext_impl(PyObject *module, PyObject *aiterator,
1981+
PyObject *default_value)
1982+
/*[clinic end generated code: output=f02c060c163a81fa input=f3dc5a93f073e5ac]*/
1983+
{
1984+
PyTypeObject *t = Py_TYPE(aiterator);
1985+
if (t->tp_as_async == NULL || t->tp_as_async->am_anext == NULL) {
1986+
PyErr_Format(PyExc_TypeError,
1987+
"'%.200s' object is not an async iterator",
1988+
t->tp_name);
1989+
return NULL;
1990+
}
1991+
1992+
PyObject *awaitable = (*t->tp_as_async->am_anext)(aiterator);
1993+
if (awaitable == NULL || default_value == NULL) {
1994+
return awaitable;
1995+
}
1996+
1997+
/* gh-157361: the default is handled by a Python coroutine so that
1998+
introspection tools can see through it into the awaitable. */
1999+
PyObject *helper = _PyInterpreterState_GET()->anext_with_default;
2000+
if (helper == NULL) {
2001+
Py_DECREF(awaitable);
2002+
PyErr_SetString(PyExc_RuntimeError,
2003+
"anext() with a default is not available");
2004+
return NULL;
2005+
}
2006+
PyObject *args[2] = {awaitable, default_value};
2007+
PyObject *res = PyObject_Vectorcall(helper, args, 2, NULL);
2008+
Py_DECREF(awaitable);
2009+
return res;
2010+
}
2011+
2012+
19662013
/*[clinic input]
19672014
len as builtin_len
19682015
@@ -3461,6 +3508,7 @@ static PyMethodDef builtin_methods[] = {
34613508
{"max", _PyCFunction_CAST(builtin_max), METH_FASTCALL | METH_KEYWORDS, max_doc},
34623509
{"min", _PyCFunction_CAST(builtin_min), METH_FASTCALL | METH_KEYWORDS, min_doc},
34633510
{"next", _PyCFunction_CAST(builtin_next), METH_FASTCALL, next_doc},
3511+
BUILTIN_ANEXT_METHODDEF
34643512
BUILTIN_OCT_METHODDEF
34653513
BUILTIN_ORD_METHODDEF
34663514
BUILTIN_POW_METHODDEF
@@ -3504,7 +3552,8 @@ static struct PyModuleDef builtinsmodule = {
35043552
Lib/_pybuiltins.py is frozen into the interpreter as a bootstrap module
35053553
(see Tools/build/freeze_modules.py), so it can be imported here before
35063554
the import system exists. The names in its __all__ are copied into the
3507-
builtins dict. */
3555+
builtins dict, and helpers used by C builtins are kept on the
3556+
interpreter. */
35083557

35093558
int
35103559
_PyBuiltin_InitPythonFunctions(PyObject *dict)
@@ -3542,6 +3591,13 @@ _PyBuiltin_InitPythonFunctions(PyObject *dict)
35423591
goto done;
35433592
}
35443593
}
3594+
3595+
PyInterpreterState *interp = _PyInterpreterState_GET();
3596+
interp->anext_with_default = PyObject_GetAttrString(
3597+
mod, "_anext_with_default");
3598+
if (interp->anext_with_default == NULL) {
3599+
goto done;
3600+
}
35453601
rc = 0;
35463602

35473603
done:

‎Python/clinic/bltinmodule.c.h‎

Lines changed: 39 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Python/pystate.c‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,7 @@ interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate)
891891

892892
Py_CLEAR(interp->sysdict_copy);
893893
Py_CLEAR(interp->builtins_copy);
894+
Py_CLEAR(interp->anext_with_default);
894895
Py_CLEAR(interp->dict);
895896
#ifdef HAVE_FORK
896897
Py_CLEAR(interp->before_forkers);

0 commit comments

Comments
 (0)