From d4825ac27cba68d02221e13910ea42ee9caac38c Mon Sep 17 00:00:00 2001 From: Jeffrey Bosboom Date: Tue, 9 Sep 2025 02:05:54 -0700 Subject: [PATCH 1/3] gh-138535: Optimize fill_time for typical timestamps (#138537) While file timestamps can be anything the file system can store, most lie between the recent past and the near future. Optimize fill_time() for typical timestamps in three ways: - When possible, convert to nanoseconds with C arithmetic. - When using C arithmetic and the seconds member is not required (for st_birthtime), avoid creating a long object. - When using C arithmetic, reorder the code to avoid the null checks implied in Py_XDECREF(). Co-authored-by: Victor Stinner --- Lib/test/test_os.py | 12 ++- ...-09-06-20-09-32.gh-issue-138535.mlntEe.rst | 2 + Modules/posixmodule.c | 89 +++++++++++-------- 3 files changed, 62 insertions(+), 41 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-09-06-20-09-32.gh-issue-138535.mlntEe.rst diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index b476b431ad6f96..cd15aa10f16de8 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -1064,9 +1064,15 @@ def test_large_time(self): if self.get_file_system(self.dirname) != "NTFS": self.skipTest("requires NTFS") - large = 5000000000 # some day in 2128 - os.utime(self.fname, (large, large)) - self.assertEqual(os.stat(self.fname).st_mtime, large) + times = ( + 5000000000, # some day in 2128 + # boundaries of the fast path cutoff in posixmodule.c:fill_time + -9223372037, -9223372036, 9223372035, 9223372036, + ) + for large in times: + with self.subTest(large=large): + os.utime(self.fname, (large, large)) + self.assertEqual(os.stat(self.fname).st_mtime, large) def test_utime_invalid_arguments(self): # seconds and nanoseconds parameters are mutually exclusive diff --git a/Misc/NEWS.d/next/Library/2025-09-06-20-09-32.gh-issue-138535.mlntEe.rst b/Misc/NEWS.d/next/Library/2025-09-06-20-09-32.gh-issue-138535.mlntEe.rst new file mode 100644 index 00000000000000..3fa8f48d56a2bd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-06-20-09-32.gh-issue-138535.mlntEe.rst @@ -0,0 +1,2 @@ +Speed up :func:`os.stat` for files with reasonable timestamps. Contributed +by Jeffrey Bosboom. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 53b21e99376485..74edd28998b5a1 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -2588,55 +2588,68 @@ static int fill_time(PyObject *module, PyObject *v, int s_index, int f_index, int ns_index, time_t sec, unsigned long nsec) { assert(!PyErr_Occurred()); - - int res = -1; - PyObject *s_in_ns = NULL; - PyObject *ns_total = NULL; - PyObject *float_s = NULL; - - PyObject *s = _PyLong_FromTime_t(sec); - PyObject *ns_fractional = PyLong_FromUnsignedLong(nsec); - if (!(s && ns_fractional)) { - goto exit; - } - - s_in_ns = PyNumber_Multiply(s, get_posix_state(module)->billion); - if (!s_in_ns) { - goto exit; - } - - ns_total = PyNumber_Add(s_in_ns, ns_fractional); - if (!ns_total) - goto exit; - - float_s = PyFloat_FromDouble(sec + 1e-9*nsec); - if (!float_s) { - goto exit; - } +#define SEC_TO_NS (1000000000LL) + assert(nsec < SEC_TO_NS); if (s_index >= 0) { + PyObject *s = _PyLong_FromTime_t(sec); + if (s == NULL) { + return -1; + } PyStructSequence_SET_ITEM(v, s_index, s); - s = NULL; } + if (f_index >= 0) { + PyObject *float_s = PyFloat_FromDouble((double)sec + 1e-9 * nsec); + if (float_s == NULL) { + return -1; + } PyStructSequence_SET_ITEM(v, f_index, float_s); - float_s = NULL; } + + int res = -1; if (ns_index >= 0) { - PyStructSequence_SET_ITEM(v, ns_index, ns_total); - ns_total = NULL; - } + /* 1677-09-21 00:12:44 to 2262-04-11 23:47:15 UTC inclusive */ + if ((LLONG_MIN/SEC_TO_NS) <= sec && sec <= (LLONG_MAX/SEC_TO_NS - 1)) { + PyObject *ns_total = PyLong_FromLongLong(sec * SEC_TO_NS + nsec); + if (ns_total == NULL) { + return -1; + } + PyStructSequence_SET_ITEM(v, ns_index, ns_total); + assert(!PyErr_Occurred()); + res = 0; + } + else { + PyObject *s_in_ns = NULL; + PyObject *ns_total = NULL; + PyObject *s = _PyLong_FromTime_t(sec); + PyObject *ns_fractional = PyLong_FromUnsignedLong(nsec); + if (s == NULL || ns_fractional == NULL) { + goto exit; + } - assert(!PyErr_Occurred()); - res = 0; + s_in_ns = PyNumber_Multiply(s, get_posix_state(module)->billion); + if (s_in_ns == NULL) { + goto exit; + } + + ns_total = PyNumber_Add(s_in_ns, ns_fractional); + if (ns_total == NULL) { + goto exit; + } + PyStructSequence_SET_ITEM(v, ns_index, ns_total); + assert(!PyErr_Occurred()); + res = 0; + + exit: + Py_XDECREF(s); + Py_XDECREF(ns_fractional); + Py_XDECREF(s_in_ns); + } + } -exit: - Py_XDECREF(s); - Py_XDECREF(ns_fractional); - Py_XDECREF(s_in_ns); - Py_XDECREF(ns_total); - Py_XDECREF(float_s); return res; + #undef SEC_TO_NS } #ifdef MS_WINDOWS From fd2e3d163340a29fc3ad1e9be3289fee64979dbd Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 9 Sep 2025 12:22:40 +0300 Subject: [PATCH 2/3] Add impl-detail block for PyLong_FromLong docs (#126422) Co-authored-by: Brian Schubert --- Doc/c-api/long.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Doc/c-api/long.rst b/Doc/c-api/long.rst index 31e2cd57034467..8370dcecad3344 100644 --- a/Doc/c-api/long.rst +++ b/Doc/c-api/long.rst @@ -40,9 +40,11 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate. Return a new :c:type:`PyLongObject` object from *v*, or ``NULL`` on failure. - The current implementation keeps an array of integer objects for all integers - between ``-5`` and ``256``. When you create an int in that range you actually - just get back a reference to the existing object. + .. impl-detail:: + + CPython keeps an array of integer objects for all integers + between ``-5`` and ``256``. When you create an int in that range + you actually just get back a reference to the existing object. .. c:function:: PyObject* PyLong_FromUnsignedLong(unsigned long v) From 074f3b20b3f05e2740f5324f1b0653152a41ca82 Mon Sep 17 00:00:00 2001 From: yagggi Date: Tue, 9 Sep 2025 17:41:13 +0800 Subject: [PATCH 3/3] gh-138577: Mention Unix-specific limitations of `getpass.getpass(echo_char=...)` (#138677) In bf8bbe9a813dd9fc2dd14be06df172b7d26ca1af, `getpass.getpass` gained the ability to provide keyboard feedback through `echo_char`. On Unix, line editing shortcuts such as Ctrl+U were previously handled as the terminal operates in canonical mode (see termios(3)). However, since keyboard feedback requires to switch to noncanonical mode, this now results in an inconsistency when `getpass.getpass` uses `echo_char` as those shortcuts are no more supported. This limitation is specific to Unix and does not affect Windows users where line editing shortcuts were never supported. --- Doc/library/getpass.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Doc/library/getpass.rst b/Doc/library/getpass.rst index 0fb0fc88683c03..af9c9e9f39d9a6 100644 --- a/Doc/library/getpass.rst +++ b/Doc/library/getpass.rst @@ -39,6 +39,14 @@ The :mod:`getpass` module provides two functions: If you call getpass from within IDLE, the input may be done in the terminal you launched IDLE from rather than the idle window itself. + .. note:: + On Unix systems, when *echo_char* is set, the terminal will be + configured to operate in + :manpage:`noncanonical mode `. + In particular, this means that line editing shortcuts such as + :kbd:`Ctrl+U` will not work and may insert unexpected characters into + the input. + .. versionchanged:: 3.14 Added the *echo_char* parameter for keyboard feedback.