From a11ab0920c720b36d9f0d962c1bbb4df9f513e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maurycy=20Paw=C5=82owski-Wiero=C5=84ski?= Date: Sat, 29 Aug 2026 10:30:57 +0200 Subject: [PATCH 1/4] gh-148085: `datetime` cache `time` module lookups (#148088) Co-authored-by: Stan Ulbrych --- ...-04-04-14-25-43.gh-issue-148085.o97yTo.rst | 6 ++ Modules/_datetimemodule.c | 60 +++++++++++-------- 2 files changed, 41 insertions(+), 25 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-04-04-14-25-43.gh-issue-148085.o97yTo.rst diff --git a/Misc/NEWS.d/next/Library/2026-04-04-14-25-43.gh-issue-148085.o97yTo.rst b/Misc/NEWS.d/next/Library/2026-04-04-14-25-43.gh-issue-148085.o97yTo.rst new file mode 100644 index 000000000000000..55cb29a4e83a4c7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-04-14-25-43.gh-issue-148085.o97yTo.rst @@ -0,0 +1,6 @@ +Speed up :meth:`~datetime.date.timetuple` and :meth:`~datetime.date.strftime` +by caching the ``time.struct_time`` and ``time.strftime`` lookups, and +:meth:`~datetime.datetime.today` by reading the clock directly instead of +calling :func:`time.time`. Patching :mod:`time` after importing +:mod:`datetime` no longer affects these methods. Patch by Maurycy +Pawłowski-Wieroński. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index cd02b298b406e6a..ec71a89a2b04ec6 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -52,6 +52,9 @@ typedef struct { /* The interned Unix epoch datetime instance */ PyObject *epoch; + + PyObject *time_struct_time; + PyObject *time_strftime; } datetime_state; /* The module has a fixed number of static objects, due to being exposed @@ -1892,10 +1895,12 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, assert(object && format && timetuple); assert(PyUnicode_Check(format)); - PyObject *strftime = PyImport_ImportModuleAttrString("time", "strftime"); - if (strftime == NULL) { + PyObject *current_mod = NULL; + datetime_state *st = GET_CURRENT_STATE(current_mod); + if (st == NULL) { return NULL; } + PyObject *strftime = st->time_strftime; /* Scan the input format, looking for %z/%Z/%f escapes, building * a new format. Since computing the replacements for those codes @@ -2055,7 +2060,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, Py_XDECREF(zreplacement); Py_XDECREF(colonzreplacement); Py_XDECREF(Zreplacement); - Py_XDECREF(strftime); + RELEASE_CURRENT_STATE(st, current_mod); return result; Error: @@ -2068,41 +2073,26 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, * from C. Perhaps they should be. */ -/* Call time.time() and return its result (a Python float). */ -static PyObject * -time_time(void) -{ - PyObject *result = NULL; - PyObject *time = PyImport_ImportModuleAttrString("time", "time"); - - if (time != NULL) { - result = PyObject_CallNoArgs(time); - Py_DECREF(time); - } - return result; -} - /* Build a time.struct_time. The weekday and day number are automatically * computed from the y,m,d args. */ static PyObject * build_struct_time(int y, int m, int d, int hh, int mm, int ss, int dstflag) { - PyObject *struct_time; - PyObject *result; - - struct_time = PyImport_ImportModuleAttrString("time", "struct_time"); - if (struct_time == NULL) { + PyObject *current_mod = NULL; + datetime_state *st = GET_CURRENT_STATE(current_mod); + if (st == NULL) { return NULL; } - result = PyObject_CallFunction(struct_time, "((iiiiiiiii))", + PyObject *result = PyObject_CallFunction(st->time_struct_time, + "((iiiiiiiii))", y, m, d, hh, mm, ss, weekday(y, m, d), days_before_month(y, m) + d, dstflag); - Py_DECREF(struct_time); + RELEASE_CURRENT_STATE(st, current_mod); return result; } @@ -3337,7 +3327,11 @@ datetime_date_today_impl(PyTypeObject *type) type); } - PyObject *time = time_time(); + PyTime_t ts; + if (PyTime_Time(&ts) < 0) { + return NULL; + } + PyObject *time = PyFloat_FromDouble(PyTime_AsSecondsDouble(ts)); if (time == NULL) { return NULL; } @@ -7449,6 +7443,8 @@ init_state(datetime_state *st, PyObject *module, PyObject *old_module) .us_per_week = Py_NewRef(st_old->us_per_week), .seconds_per_day = Py_NewRef(st_old->seconds_per_day), .epoch = Py_NewRef(st_old->epoch), + .time_struct_time = Py_NewRef(st_old->time_struct_time), + .time_strftime = Py_NewRef(st_old->time_strftime), }; return 0; } @@ -7493,6 +7489,15 @@ init_state(datetime_state *st, PyObject *module, PyObject *old_module) return -1; } + st->time_struct_time = PyImport_ImportModuleAttrString("time", "struct_time"); + if (st->time_struct_time == NULL) { + return -1; + } + st->time_strftime = PyImport_ImportModuleAttrString("time", "strftime"); + if (st->time_strftime == NULL) { + return -1; + } + return 0; } @@ -7502,6 +7507,9 @@ traverse_state(datetime_state *st, visitproc visit, void *arg) /* heap types */ Py_VISIT(st->isocalendar_date_type); + Py_VISIT(st->time_struct_time); + Py_VISIT(st->time_strftime); + return 0; } @@ -7517,6 +7525,8 @@ clear_state(datetime_state *st) Py_CLEAR(st->us_per_week); Py_CLEAR(st->seconds_per_day); Py_CLEAR(st->epoch); + Py_CLEAR(st->time_struct_time); + Py_CLEAR(st->time_strftime); return 0; } From b0c9fc37dd0ed60456313600e6a01478308f195e Mon Sep 17 00:00:00 2001 From: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com> Date: Sat, 29 Aug 2026 11:39:07 +0300 Subject: [PATCH 2/4] gh-156523: Fix asyncio.as_completed() not recording the awaiting task (#156527) --- Lib/asyncio/tasks.py | 4 ++ Lib/test/test_asyncio/test_graph.py | 62 +++++++++++++++++++ ...-08-28-18-10-25.gh-issue-156523.llJqo9.rst | 2 + 3 files changed, 68 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-08-28-18-10-25.gh-issue-156523.llJqo9.rst diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 498eec3f31b292b..f432cf0afa895a2 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -562,9 +562,11 @@ def __init__(self, aws, timeout): self._timeout_handle = None loop = events.get_event_loop() + self._cur_task = current_task() todo = {ensure_future(aw, loop=loop) for aw in set(aws)} for f in todo: f.add_done_callback(self._handle_completion) + futures.future_add_to_awaited_by(f, self._cur_task) if todo and timeout is not None: self._timeout_handle = ( loop.call_later(timeout, self._handle_timeout) @@ -595,6 +597,7 @@ def __next__(self): def _handle_timeout(self): for f in self._todo: f.remove_done_callback(self._handle_completion) + futures.future_discard_from_awaited_by(f, self._cur_task) self._done.put_nowait(None) # Sentinel for _wait_for_one(). self._todo.clear() # Can't do todo.remove(f) in the loop. @@ -602,6 +605,7 @@ def _handle_completion(self, f): if not self._todo: return # _handle_timeout() was here first. self._todo.remove(f) + futures.future_discard_from_awaited_by(f, self._cur_task) self._done.put_nowait(f) if not self._todo and self._timeout_handle is not None: self._timeout_handle.cancel() diff --git a/Lib/test/test_asyncio/test_graph.py b/Lib/test/test_asyncio/test_graph.py index 928b618fe5c55b7..220c0179f27bf3b 100644 --- a/Lib/test/test_asyncio/test_graph.py +++ b/Lib/test/test_asyncio/test_graph.py @@ -298,6 +298,68 @@ async def main(t1, t2): ] ]) + async def test_stack_as_completed(self): + # gh-156523: as_completed() must record the awaiting task + stack_for_inner = None + + async def inner(): + await asyncio.sleep(0) + nonlocal stack_for_inner + stack_for_inner = capture_test_stack() + + async def main(t): + for f in asyncio.as_completed([t]): + await f + + t = asyncio.create_task(inner(), name='inner') + await main(t) + self.assertFalse(t._asyncio_awaited_by) + + self.assertEqual(stack_for_inner[0], [ + 'T', + ['s capture_test_stack', 'a inner'], + [ + ['T', + ['a get', 'a _wait_for_one', 'a main', + 'a test_stack_as_completed'], + [] + ] + ] + ]) + + async def test_stack_as_completed_timeout(self): + # gh-156523: the awaiting task must be dropped when as_completed() times out + stack_for_inner = None + + async def inner(): + nonlocal stack_for_inner + stack_for_inner = capture_test_stack() + await asyncio.sleep(3600) + + async def main(t): + with self.assertRaises(TimeoutError): + for f in asyncio.as_completed([t], timeout=0.01): + await f + + t = asyncio.create_task(inner(), name='inner') + await main(t) + self.assertFalse(t._asyncio_awaited_by) + t.cancel() + with self.assertRaises(asyncio.CancelledError): + await t + + self.assertEqual(stack_for_inner[0], [ + 'T', + ['s capture_test_stack', 'a inner'], + [ + ['T', + ['a get', 'a _wait_for_one', 'a main', + 'a test_stack_as_completed_timeout'], + [] + ] + ] + ]) + async def test_stack_task(self): stack_for_inner = None diff --git a/Misc/NEWS.d/next/Library/2026-08-28-18-10-25.gh-issue-156523.llJqo9.rst b/Misc/NEWS.d/next/Library/2026-08-28-18-10-25.gh-issue-156523.llJqo9.rst new file mode 100644 index 000000000000000..c1c95d76c0d23bb --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-28-18-10-25.gh-issue-156523.llJqo9.rst @@ -0,0 +1,2 @@ +Fix :func:`asyncio.as_completed` not recording the awaiting task in the call +graph. From 6c425f1d18d3915cb32e3372647db24bad7ab92d Mon Sep 17 00:00:00 2001 From: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com> Date: Sat, 29 Aug 2026 13:04:57 +0300 Subject: [PATCH 3/4] gh-156408: Fix asyncio.print_call_graph() on a finished task (#156410) --- Lib/asyncio/graph.py | 3 ++- Lib/test/test_asyncio/test_graph.py | 18 ++++++++++++++++++ ...6-08-26-12-48-10.gh-issue-156408.-Yd0k7.rst | 2 ++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-26-12-48-10.gh-issue-156408.-Yd0k7.rst diff --git a/Lib/asyncio/graph.py b/Lib/asyncio/graph.py index 7e85e08c4291a06..e240c6dd77d1245 100644 --- a/Lib/asyncio/graph.py +++ b/Lib/asyncio/graph.py @@ -58,7 +58,8 @@ def _build_graph_for_future( while coro is not None: if hasattr(coro, 'cr_await'): # A native coroutine or duck-type compatible iterator - st.append(FrameCallGraphEntry(coro.cr_frame)) + if coro.cr_frame is not None: + st.append(FrameCallGraphEntry(coro.cr_frame)) coro = coro.cr_await elif hasattr(coro, 'ag_await'): # A native async generator or duck-type compatible iterator diff --git a/Lib/test/test_asyncio/test_graph.py b/Lib/test/test_asyncio/test_graph.py index 220c0179f27bf3b..2c5bb2e8f52e668 100644 --- a/Lib/test/test_asyncio/test_graph.py +++ b/Lib/test/test_asyncio/test_graph.py @@ -484,6 +484,24 @@ def test_capture_call_graph_non_future(self): with self.assertRaises(TypeError): asyncio.capture_call_graph("not a future") + async def test_call_graph_finished_task(self): + # gh-156408: the call graph must not record a finished coroutine's None frame + async def boom(): + raise ValueError + + done = asyncio.create_task(asyncio.sleep(0), name='done') + failed = asyncio.create_task(boom(), name='failed') + cancelled = asyncio.create_task(asyncio.Event().wait(), name='cancelled') + cancelled.cancel() + await asyncio.gather(done, failed, cancelled, return_exceptions=True) + + for task in (done, failed, cancelled): + with self.subTest(task=task.get_name()): + buf = io.StringIO() + asyncio.print_call_graph(task, file=buf) + self.assertEqual(asyncio.capture_call_graph(task).call_stack, ()) + self.assertIn(f"name={task.get_name()!r}", buf.getvalue()) + async def test_capture_call_graph_no_current_task(self): results = [] diff --git a/Misc/NEWS.d/next/Library/2026-08-26-12-48-10.gh-issue-156408.-Yd0k7.rst b/Misc/NEWS.d/next/Library/2026-08-26-12-48-10.gh-issue-156408.-Yd0k7.rst new file mode 100644 index 000000000000000..62c92838b5c1266 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-26-12-48-10.gh-issue-156408.-Yd0k7.rst @@ -0,0 +1,2 @@ +Fix :func:`asyncio.print_call_graph` raising :exc:`AttributeError` when +called on a task that has already finished. From f973bd9d383e99eef58846772c69afa8ebeaf9a9 Mon Sep 17 00:00:00 2001 From: sundeep8967 <71071718+sundeep8967@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:40:34 +0530 Subject: [PATCH 4/4] gh-156353: Fix configparser space delimiter parsing (#156382) Co-authored-by: Petr Viktorin Signed-off-by: sundeep8967 --- Lib/configparser.py | 6 ++-- Lib/test/test_configparser.py | 33 +++++++++++++++++-- ...-08-26-02-30-00.gh-issue-156353.abcdef.rst | 1 + 3 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-26-02-30-00.gh-issue-156353.abcdef.rst diff --git a/Lib/configparser.py b/Lib/configparser.py index 3c452afe8ade485..88015ef60865698 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -618,7 +618,8 @@ class RawConfigParser(MutableMapping): _OPT_TMPL = r""" (?P