From fa12c6bae47a41dd84e54b39d96bb73c4ba625c0 Mon Sep 17 00:00:00 2001 From: Savannah Bailey Date: Mon, 15 Sep 2025 17:09:51 +0100 Subject: [PATCH 01/13] GH-132732: Remove textwrap import (#138933) --- Tools/cases_generator/optimizer_generator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Tools/cases_generator/optimizer_generator.py b/Tools/cases_generator/optimizer_generator.py index 7486fca245f5b9..41df073cf6df23 100644 --- a/Tools/cases_generator/optimizer_generator.py +++ b/Tools/cases_generator/optimizer_generator.py @@ -4,7 +4,6 @@ """ import argparse -import textwrap from analyzer import ( Analysis, From 07d0b95b05dfaf5832f44c2fbc956761f9e29571 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 15 Sep 2025 19:20:31 +0300 Subject: [PATCH 02/13] gh-137490: Fix signal.sigwaitinfo() on NetBSD (GH-137523) Handle ECANCELED in the same way as EINTR to work around the Posix violation in the NetBSD's implementation. --- .../2025-08-07-17-18-57.gh-issue-137490.s89ieZ.rst | 2 ++ Modules/signalmodule.c | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2025-08-07-17-18-57.gh-issue-137490.s89ieZ.rst diff --git a/Misc/NEWS.d/next/Library/2025-08-07-17-18-57.gh-issue-137490.s89ieZ.rst b/Misc/NEWS.d/next/Library/2025-08-07-17-18-57.gh-issue-137490.s89ieZ.rst new file mode 100644 index 00000000000000..bcb0938b8e3acb --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-08-07-17-18-57.gh-issue-137490.s89ieZ.rst @@ -0,0 +1,2 @@ +Handle :data:`~errno.ECANCELED` in the same way as :data:`~errno.EINTR` in +:func:`signal.sigwaitinfo` on NetBSD. diff --git a/Modules/signalmodule.c b/Modules/signalmodule.c index 02bfdab957fc52..3c79ef1429087a 100644 --- a/Modules/signalmodule.c +++ b/Modules/signalmodule.c @@ -1183,7 +1183,13 @@ signal_sigwaitinfo_impl(PyObject *module, sigset_t sigset) err = sigwaitinfo(&sigset, &si); Py_END_ALLOW_THREADS } while (err == -1 - && errno == EINTR && !(async_err = PyErr_CheckSignals())); + && (errno == EINTR +#if defined(__NetBSD__) + /* NetBSD's implementation violates POSIX by setting + * errno to ECANCELED instead of EINTR. */ + || errno == ECANCELED +#endif + ) && !(async_err = PyErr_CheckSignals())); if (err == -1) return (!async_err) ? PyErr_SetFromErrno(PyExc_OSError) : NULL; From 26cfb1794255222b20cd7b502ab9193861df3184 Mon Sep 17 00:00:00 2001 From: 00ll00 <40747228+00ll00@users.noreply.github.com> Date: Tue, 16 Sep 2025 00:21:41 +0800 Subject: [PATCH 03/13] gh-138239: Fix incorrect highlighting of "type" in type statements in the REPL (GH-138241) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/_pyrepl/utils.py | 6 ++++++ Lib/test/test_pyrepl/test_reader.py | 8 +++++--- .../2025-08-29-12-56-55.gh-issue-138239.uthZFI.rst | 2 ++ 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-08-29-12-56-55.gh-issue-138239.uthZFI.rst diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py index d32fce591fadcc..2ffd547f99d7c5 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -263,6 +263,12 @@ def is_soft_keyword_used(*tokens: TI | None) -> bool: return True case (TI(string="case"), TI(string="_"), TI(string=":")): return True + case ( + None | TI(T.NEWLINE) | TI(T.INDENT) | TI(T.DEDENT) | TI(string=":"), + TI(string="type"), + TI(T.NAME, string=s) + ): + return not keyword.iskeyword(s) case _: return False diff --git a/Lib/test/test_pyrepl/test_reader.py b/Lib/test/test_pyrepl/test_reader.py index 9a02dff7387563..b1b6ae16a1e592 100644 --- a/Lib/test/test_pyrepl/test_reader.py +++ b/Lib/test/test_pyrepl/test_reader.py @@ -378,6 +378,7 @@ def funct(case: str = sys.platform) -> None: case "ios" | "android": print("on the phone") case _: print('arms around', match.group(1)) + type type = type[type] """ ) expected = dedent( @@ -397,6 +398,7 @@ def funct(case: str = sys.platform) -> None: {K}case{z} {s}"ios"{z} {o}|{z} {s}"android"{z}{o}:{z} {b}print{z}{o}({z}{s}"on the phone"{z}{o}){z} {K}case{z} {K}_{z}{o}:{z} {b}print{z}{o}({z}{s}'arms around'{z}{o},{z} match{o}.{z}group{o}({z}{n}1{z}{o}){z}{o}){z} + {K}type{z} {b}type{z} {o}={z} {b}type{z}{o}[{z}{b}type{z}{o}]{z} """ ) expected_sync = expected.format(a="", **colors) @@ -404,14 +406,14 @@ def funct(case: str = sys.platform) -> None: reader, _ = handle_all_events(events) self.assert_screen_equal(reader, code, clean=True) self.assert_screen_equal(reader, expected_sync) - self.assertEqual(reader.pos, 396) - self.assertEqual(reader.cxy, (0, 15)) + self.assertEqual(reader.pos, 419) + self.assertEqual(reader.cxy, (0, 16)) async_msg = "{k}async{z} ".format(**colors) expected_async = expected.format(a=async_msg, **colors) more_events = itertools.chain( code_to_events(code), - [Event(evt="key", data="up", raw=bytearray(b"\x1bOA"))] * 14, + [Event(evt="key", data="up", raw=bytearray(b"\x1bOA"))] * 15, code_to_events("async "), ) reader, _ = handle_all_events(more_events) diff --git a/Misc/NEWS.d/next/Library/2025-08-29-12-56-55.gh-issue-138239.uthZFI.rst b/Misc/NEWS.d/next/Library/2025-08-29-12-56-55.gh-issue-138239.uthZFI.rst new file mode 100644 index 00000000000000..9e0218e02664c6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-08-29-12-56-55.gh-issue-138239.uthZFI.rst @@ -0,0 +1,2 @@ +The REPL now highlights :keyword:`type` as a soft keyword +in :ref:`type statements `. From 46f823bb818b0e8f40b51c8fa9ef33f743915770 Mon Sep 17 00:00:00 2001 From: Ken Jin Date: Mon, 15 Sep 2025 17:24:37 +0100 Subject: [PATCH 04/13] gh-132732: Clear errors in JIT optimizer on error (GH-136048) --- Python/optimizer_analysis.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Python/optimizer_analysis.c b/Python/optimizer_analysis.c index fd395d3c6c254f..9d43f2de41df78 100644 --- a/Python/optimizer_analysis.c +++ b/Python/optimizer_analysis.c @@ -462,7 +462,7 @@ const uint16_t op_without_decref_inputs[MAX_UOP_ID + 1] = { [_BINARY_OP_SUBTRACT_FLOAT] = _BINARY_OP_SUBTRACT_FLOAT__NO_DECREF_INPUTS, }; -/* 1 for success, 0 for not ready, cannot error at the moment. */ +/* >0 (length) for success, 0 for not ready, clears all possible errors. */ static int optimize_uops( PyCodeObject *co, @@ -472,6 +472,7 @@ optimize_uops( _PyBloomFilter *dependencies ) { + assert(!PyErr_Occurred()); JitOptContext context; JitOptContext *ctx = &context; @@ -555,7 +556,11 @@ optimize_uops( OPT_ERROR_IN_OPCODE(opcode); } _Py_uop_abstractcontext_fini(ctx); - return -1; + + assert(PyErr_Occurred()); + PyErr_Clear(); + + return 0; } @@ -702,10 +707,12 @@ _Py_uop_analyze_and_optimize( _PyFrame_GetCode(frame), buffer, length, curr_stacklen, dependencies); - if (length <= 0) { + if (length == 0) { return length; } + assert(length > 0); + length = remove_unneeded_uops(buffer, length); assert(length > 0); From 8ef7735c536e0ffe4a60224e59b7587288f53e9e Mon Sep 17 00:00:00 2001 From: yihong Date: Tue, 16 Sep 2025 00:26:23 +0800 Subject: [PATCH 05/13] gh-128636: Fix crash in PyREPL when os.environ is overwritten with an invalid value for macOS (GH-138089) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: yihong0618 Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/_colorize.py | 21 +++++++++++++------ Lib/_pyrepl/unix_console.py | 8 +++++-- Lib/test/support/__init__.py | 2 +- Lib/test/test_pyrepl/test_unix_console.py | 9 ++++++++ ...-09-10-10-02-59.gh-issue-128636.ldRKGZ.rst | 2 ++ 5 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-09-10-10-02-59.gh-issue-128636.ldRKGZ.rst diff --git a/Lib/_colorize.py b/Lib/_colorize.py index f45e7b8bb300f1..d35486296f2684 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -288,21 +288,29 @@ def decolor(text: str) -> str: def can_colorize(*, file: IO[str] | IO[bytes] | None = None) -> bool: + + def _safe_getenv(k: str, fallback: str | None = None) -> str | None: + """Exception-safe environment retrieval. See gh-128636.""" + try: + return os.environ.get(k, fallback) + except Exception: + return fallback + if file is None: file = sys.stdout if not sys.flags.ignore_environment: - if os.environ.get("PYTHON_COLORS") == "0": + if _safe_getenv("PYTHON_COLORS") == "0": return False - if os.environ.get("PYTHON_COLORS") == "1": + if _safe_getenv("PYTHON_COLORS") == "1": return True - if os.environ.get("NO_COLOR"): + if _safe_getenv("NO_COLOR"): return False if not COLORIZE: return False - if os.environ.get("FORCE_COLOR"): + if _safe_getenv("FORCE_COLOR"): return True - if os.environ.get("TERM") == "dumb": + if _safe_getenv("TERM") == "dumb": return False if not hasattr(file, "fileno"): @@ -345,7 +353,8 @@ def get_theme( environment (including environment variable state and console configuration on Windows) can also change in the course of the application life cycle. """ - if force_color or (not force_no_color and can_colorize(file=tty_file)): + if force_color or (not force_no_color and + can_colorize(file=tty_file)): return _theme return theme_no_color diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index a7e49923191c07..9953051bf7c4ef 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -159,6 +159,10 @@ def __init__( self.pollob.register(self.input_fd, select.POLLIN) self.terminfo = terminfo.TermInfo(term or None) self.term = term + self.is_apple_terminal = ( + platform.system() == "Darwin" + and os.getenv("TERM_PROGRAM") == "Apple_Terminal" + ) @overload def _my_getstr(cap: str, optional: Literal[False] = False) -> bytes: ... @@ -339,7 +343,7 @@ def prepare(self): tcsetattr(self.input_fd, termios.TCSADRAIN, raw) # In macOS terminal we need to deactivate line wrap via ANSI escape code - if platform.system() == "Darwin" and os.getenv("TERM_PROGRAM") == "Apple_Terminal": + if self.is_apple_terminal: os.write(self.output_fd, b"\033[?7l") self.screen = [] @@ -370,7 +374,7 @@ def restore(self): self.flushoutput() tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate) - if platform.system() == "Darwin" and os.getenv("TERM_PROGRAM") == "Apple_Terminal": + if self.is_apple_terminal: os.write(self.output_fd, b"\033[?7h") if hasattr(self, "old_sigwinch"): diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 4bfd01ed14a0a1..8d614ab3d42b5a 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -2898,7 +2898,7 @@ def force_color(color: bool): from .os_helper import EnvironmentVarGuard with ( - swap_attr(_colorize, "can_colorize", lambda file=None: color), + swap_attr(_colorize, "can_colorize", lambda *, file=None: color), EnvironmentVarGuard() as env, ): env.unset("FORCE_COLOR", "NO_COLOR", "PYTHON_COLORS") diff --git a/Lib/test/test_pyrepl/test_unix_console.py b/Lib/test/test_pyrepl/test_unix_console.py index ab1236768cfb3e..6185c7e3c794e3 100644 --- a/Lib/test/test_pyrepl/test_unix_console.py +++ b/Lib/test/test_pyrepl/test_unix_console.py @@ -303,3 +303,12 @@ def test_getheightwidth_with_invalid_environ(self, _os_write): self.assertIsInstance(console.getheightwidth(), tuple) os.environ = [] self.assertIsInstance(console.getheightwidth(), tuple) + + @unittest.skipUnless(sys.platform == "darwin", "requires macOS") + def test_restore_with_invalid_environ_on_macos(self, _os_write): + # gh-128636 for macOS + console = UnixConsole(term="xterm") + with os_helper.EnvironmentVarGuard(): + os.environ = [] + console.prepare() # needed to call restore() + console.restore() # this should succeed diff --git a/Misc/NEWS.d/next/Library/2025-09-10-10-02-59.gh-issue-128636.ldRKGZ.rst b/Misc/NEWS.d/next/Library/2025-09-10-10-02-59.gh-issue-128636.ldRKGZ.rst new file mode 100644 index 00000000000000..54eae0a4601617 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-10-10-02-59.gh-issue-128636.ldRKGZ.rst @@ -0,0 +1,2 @@ +Fix crash in PyREPL when os.environ is overwritten with an invalid value for +mac From 811acc85d5b001e0bef6ac2e6b499e7c4f149262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 15 Sep 2025 17:27:37 +0100 Subject: [PATCH 06/13] gh-134953: Make the True/False/None check more efficient (GH-138931) --- Lib/_pyrepl/utils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py index 2ffd547f99d7c5..64708e843b685b 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -20,6 +20,7 @@ ZERO_WIDTH_BRACKET = re.compile(r"\x01.*?\x02") ZERO_WIDTH_TRANS = str.maketrans({"\x01": "", "\x02": ""}) IDENTIFIERS_AFTER = {"def", "class"} +KEYWORD_CONSTANTS = {"True", "False", "None"} BUILTINS = {str(name) for name in dir(builtins) if not name.startswith('_')} @@ -196,12 +197,12 @@ def gen_colors_from_token_stream( is_def_name = False span = Span.from_token(token, line_lengths) yield ColorSpan(span, "definition") - elif token.string in ("True", "False", "None"): - span = Span.from_token(token, line_lengths) - yield ColorSpan(span, "keyword_constant") elif keyword.iskeyword(token.string): + span_cls = "keyword" + if token.string in KEYWORD_CONSTANTS: + span_cls = "keyword_constant" span = Span.from_token(token, line_lengths) - yield ColorSpan(span, "keyword") + yield ColorSpan(span, span_cls) if token.string in IDENTIFIERS_AFTER: is_def_name = True elif ( From 29d026f93e14cc9bf5c17ac195ab0ec7708eaf57 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 15 Sep 2025 19:40:28 +0300 Subject: [PATCH 07/13] gh-37817: Allow assignment to __bases__ of direct subclasses of builtin classes (GH-137585) --- Lib/test/test_descr.py | 205 ++++++++++++++---- ...5-08-09-11-38-37.gh-issue-37817.Y5Fhde.rst | 2 + Objects/typeobject.c | 61 ++++-- 3 files changed, 202 insertions(+), 66 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-11-38-37.gh-issue-37817.Y5Fhde.rst diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 9dfeeccb81b34d..39b835b03fc599 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -4077,42 +4077,167 @@ class E(D): self.assertEqual(e.a, 2) self.assertEqual(C2.__subclasses__(), [D]) - try: + with self.assertRaisesRegex(TypeError, + "cannot delete '__bases__' attribute of immutable type"): del D.__bases__ - except (TypeError, AttributeError): - pass - else: - self.fail("shouldn't be able to delete .__bases__") - - try: + with self.assertRaisesRegex(TypeError, 'can only assign non-empty tuple'): D.__bases__ = () - except TypeError as msg: - if str(msg) == "a new-style class can't have only classic bases": - self.fail("wrong error message for .__bases__ = ()") - else: - self.fail("shouldn't be able to set .__bases__ to ()") - - try: + with self.assertRaisesRegex(TypeError, 'can only assign tuple'): + D.__bases__ = [C] + with self.assertRaisesRegex(TypeError, 'duplicate base class'): + D.__bases__ = (C, C) + with self.assertRaisesRegex(TypeError, 'inheritance cycle'): D.__bases__ = (D,) - except TypeError: - pass - else: - # actually, we'll have crashed by here... - self.fail("shouldn't be able to create inheritance cycles") + with self.assertRaisesRegex(TypeError, 'inheritance cycle'): + D.__bases__ = (E,) - try: - D.__bases__ = (C, C) - except TypeError: - pass - else: - self.fail("didn't detect repeated base classes") + class A: + __slots__ = () + def __repr__(self): + return '' + class A_with_dict: + __slots__ = ('__dict__',) + def __repr__(self): + return '' + class A_with_dict_weakref: + def __repr__(self): + return '' + class A_with_slots: + __slots__ = ('x',) + def __repr__(self): + return '' + class A_with_slots_dict: + __slots__ = ('x', '__dict__') + def __repr__(self): + return '' - try: - D.__bases__ = (E,) - except TypeError: - pass - else: - self.fail("shouldn't be able to create inheritance cycles") + class B: + __slots__ = () + b = B() + r = repr(b) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B.__bases__ = (int,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B.__bases__ = (A_with_dict_weakref,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B.__bases__ = (A_with_dict,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B.__bases__ = (A_with_slots,) + B.__bases__ = (A,) + self.assertNotHasAttr(b, '__dict__') + self.assertNotHasAttr(b, '__weakref__') + self.assertEqual(repr(b), '') + B.__bases__ = (object,) + self.assertEqual(repr(b), r) + + class B_with_dict_weakref: + pass + b = B_with_dict_weakref() + with self.assertRaisesRegex(TypeError, 'layout differs'): + B.__bases__ = (A_with_slots,) + B_with_dict_weakref.__bases__ = (A_with_dict_weakref,) + self.assertEqual(repr(b), '') + B_with_dict_weakref.__bases__ = (A_with_dict,) + self.assertEqual(repr(b), '') + B_with_dict_weakref.__bases__ = (A,) + self.assertEqual(repr(b), '') + B_with_dict_weakref.__bases__ = (object,) + + class B_with_slots: + __slots__ = ('x',) + b = B_with_slots() + with self.assertRaisesRegex(TypeError, 'layout differs'): + B_with_slots.__bases__ = (A_with_dict_weakref,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B_with_slots.__bases__ = (A_with_dict,) + B_with_slots.__bases__ = (A,) + self.assertEqual(repr(b), '') + + class B_with_slots_dict: + __slots__ = ('x', '__dict__') + b = B_with_slots_dict() + with self.assertRaisesRegex(TypeError, 'layout differs'): + B_with_slots_dict.__bases__ = (A_with_dict_weakref,) + B_with_slots_dict.__bases__ = (A_with_dict,) + self.assertEqual(repr(b), '') + B_with_slots_dict.__bases__ = (A,) + self.assertEqual(repr(b), '') + + class B_with_slots_dict_weakref: + __slots__ = ('x', '__dict__', '__weakref__') + b = B_with_slots_dict_weakref() + with self.assertRaisesRegex(TypeError, 'layout differs'): + B_with_slots_dict_weakref.__bases__ = (A_with_slots_dict,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B_with_slots_dict_weakref.__bases__ = (A_with_slots,) + B_with_slots_dict_weakref.__bases__ = (A_with_dict_weakref,) + self.assertEqual(repr(b), '') + B_with_slots_dict_weakref.__bases__ = (A_with_dict,) + self.assertEqual(repr(b), '') + B_with_slots_dict_weakref.__bases__ = (A,) + self.assertEqual(repr(b), '') + + class C_with_slots(A_with_slots): + __slots__ = () + c = C_with_slots() + with self.assertRaisesRegex(TypeError, 'layout differs'): + C_with_slots.__bases__ = (A_with_slots_dict,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + C_with_slots.__bases__ = (A_with_dict_weakref,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + C_with_slots.__bases__ = (A_with_dict,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + C_with_slots.__bases__ = (A,) + C_with_slots.__bases__ = (A_with_slots,) + self.assertEqual(repr(c), '') + + class C_with_slots_dict(A_with_slots): + pass + c = C_with_slots_dict() + with self.assertRaisesRegex(TypeError, 'layout differs'): + C_with_slots_dict.__bases__ = (A_with_dict_weakref,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + C_with_slots_dict.__bases__ = (A_with_dict,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + C_with_slots_dict.__bases__ = (A,) + C_with_slots_dict.__bases__ = (A_with_slots_dict,) + self.assertEqual(repr(c), '') + C_with_slots_dict.__bases__ = (A_with_slots,) + self.assertEqual(repr(c), '') + + class A_int(int): + __slots__ = () + def __repr__(self): + return '' + class B_int(int): + __slots__ = () + b = B_int(42) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B_int.__bases__ = (object,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B_int.__bases__ = (tuple,) + with self.assertRaisesRegex(TypeError, 'is not an acceptable base type'): + B_int.__bases__ = (bool,) + B_int.__bases__ = (A_int,) + self.assertEqual(repr(b), '') + B_int.__bases__ = (int,) + self.assertEqual(repr(b), '42') + + class A_tuple(tuple): + __slots__ = () + def __repr__(self): + return '' + class B_tuple(tuple): + __slots__ = () + b = B_tuple((1, 2)) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B_tuple.__bases__ = (object,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B_tuple.__bases__ = (int,) + B_tuple.__bases__ = (A_tuple,) + self.assertEqual(repr(b), '') + B_tuple.__bases__ = (tuple,) + self.assertEqual(repr(b), '(1, 2)') def test_assign_bases_many_subclasses(self): # This is intended to check that typeobject.c:queue_slot_update() can @@ -4165,26 +4290,14 @@ class C(object): class D(C): pass - try: + with self.assertRaisesRegex(TypeError, 'layout differs'): L.__bases__ = (dict,) - except TypeError: - pass - else: - self.fail("shouldn't turn list subclass into dict subclass") - try: + with self.assertRaisesRegex(TypeError, 'immutable type'): list.__bases__ = (dict,) - except TypeError: - pass - else: - self.fail("shouldn't be able to assign to list.__bases__") - try: + with self.assertRaisesRegex(TypeError, 'layout differs'): D.__bases__ = (C, list) - except TypeError: - pass - else: - self.fail("best_base calculation found wanting") def test_unsubclassable_types(self): with self.assertRaises(TypeError): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-11-38-37.gh-issue-37817.Y5Fhde.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-11-38-37.gh-issue-37817.Y5Fhde.rst new file mode 100644 index 00000000000000..5e73188ff2d694 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-11-38-37.gh-issue-37817.Y5Fhde.rst @@ -0,0 +1,2 @@ +Allow assignment to :attr:`~type.__bases__` of direct subclasses of builtin +classes. diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 9cead729b6fe7a..06f3ace1764a86 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -1748,7 +1748,7 @@ type_get_mro(PyObject *tp, void *Py_UNUSED(closure)) static PyTypeObject *find_best_base(PyObject *); static int mro_internal(PyTypeObject *, int, PyObject **); static int type_is_subtype_base_chain(PyTypeObject *, PyTypeObject *); -static int compatible_for_assignment(PyTypeObject *, PyTypeObject *, const char *); +static int compatible_for_assignment(PyTypeObject *, PyTypeObject *, const char *, int); static int add_subclass(PyTypeObject*, PyTypeObject*); static int add_all_subclasses(PyTypeObject *type, PyObject *bases); static void remove_subclass(PyTypeObject *, PyTypeObject *); @@ -1886,7 +1886,7 @@ type_check_new_bases(PyTypeObject *type, PyObject *new_bases, PyTypeObject **bes if (*best_base == NULL) return -1; - if (!compatible_for_assignment(type->tp_base, *best_base, "__bases__")) { + if (!compatible_for_assignment(type, *best_base, "__bases__", 0)) { return -1; } @@ -7263,10 +7263,6 @@ compatible_with_tp_base(PyTypeObject *child) return (parent != NULL && child->tp_basicsize == parent->tp_basicsize && child->tp_itemsize == parent->tp_itemsize && - child->tp_dictoffset == parent->tp_dictoffset && - child->tp_weaklistoffset == parent->tp_weaklistoffset && - ((child->tp_flags & Py_TPFLAGS_HAVE_GC) == - (parent->tp_flags & Py_TPFLAGS_HAVE_GC)) && (child->tp_dealloc == subtype_dealloc || child->tp_dealloc == parent->tp_dealloc)); } @@ -7301,11 +7297,24 @@ same_slots_added(PyTypeObject *a, PyTypeObject *b) } static int -compatible_for_assignment(PyTypeObject* oldto, PyTypeObject* newto, const char* attr) +compatible_flags(int setclass, PyTypeObject *origto, PyTypeObject *newto, unsigned long flags) +{ + /* For __class__ assignment, the flags should be the same. + For __bases__ assignment, the new base flags can only be set + if the original class flags are set. + */ + return setclass ? (origto->tp_flags & flags) == (newto->tp_flags & flags) + : !(~(origto->tp_flags & flags) & (newto->tp_flags & flags)); +} + +static int +compatible_for_assignment(PyTypeObject *origto, PyTypeObject *newto, + const char *attr, int setclass) { PyTypeObject *newbase, *oldbase; + PyTypeObject *oldto = setclass ? origto : origto->tp_base; - if (newto->tp_free != oldto->tp_free) { + if (setclass && newto->tp_free != oldto->tp_free) { PyErr_Format(PyExc_TypeError, "%s assignment: " "'%s' deallocator differs from '%s'", @@ -7314,6 +7323,28 @@ compatible_for_assignment(PyTypeObject* oldto, PyTypeObject* newto, const char* oldto->tp_name); return 0; } + if (!compatible_flags(setclass, origto, newto, + Py_TPFLAGS_HAVE_GC | + Py_TPFLAGS_INLINE_VALUES | + Py_TPFLAGS_PREHEADER)) + { + goto differs; + } + /* For __class__ assignment, tp_dictoffset and tp_weaklistoffset should + be the same for old and new types. + For __bases__ assignment, they can only be set in the new base + if they are set in the original class with the same value. + */ + if ((setclass || newto->tp_dictoffset) + && origto->tp_dictoffset != newto->tp_dictoffset) + { + goto differs; + } + if ((setclass || newto->tp_weaklistoffset) + && origto->tp_weaklistoffset != newto->tp_weaklistoffset) + { + goto differs; + } /* It's tricky to tell if two arbitrary types are sufficiently compatible as to be interchangeable; e.g., even if they have the same tp_basicsize, they @@ -7335,17 +7366,7 @@ compatible_for_assignment(PyTypeObject* oldto, PyTypeObject* newto, const char* !same_slots_added(newbase, oldbase))) { goto differs; } - if ((oldto->tp_flags & Py_TPFLAGS_INLINE_VALUES) != - ((newto->tp_flags & Py_TPFLAGS_INLINE_VALUES))) - { - goto differs; - } - /* The above does not check for the preheader */ - if ((oldto->tp_flags & Py_TPFLAGS_PREHEADER) == - ((newto->tp_flags & Py_TPFLAGS_PREHEADER))) - { - return 1; - } + return 1; differs: PyErr_Format(PyExc_TypeError, "%s assignment: " @@ -7422,7 +7443,7 @@ object_set_class_world_stopped(PyObject *self, PyTypeObject *newto) return -1; } - if (compatible_for_assignment(oldto, newto, "__class__")) { + if (compatible_for_assignment(oldto, newto, "__class__", 1)) { /* Changing the class will change the implicit dict keys, * so we must materialize the dictionary first. */ if (oldto->tp_flags & Py_TPFLAGS_INLINE_VALUES) { From a68efdf09cfd81c64973d346ed34057f20172543 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 15 Sep 2025 17:41:43 +0100 Subject: [PATCH 08/13] gh-129813, PEP 782: Use PyBytesWriter in _hashopenssl (#138922) Replace PyBytes_FromStringAndSize(NULL, size) with the new public PyBytesWriter API. --- Modules/_hashopenssl.c | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Modules/_hashopenssl.c b/Modules/_hashopenssl.c index 628e6dc11668e0..19089b009f7911 100644 --- a/Modules/_hashopenssl.c +++ b/Modules/_hashopenssl.c @@ -1629,7 +1629,6 @@ pbkdf2_hmac_impl(PyObject *module, const char *hash_name, { _hashlibstate *state = get_hashlib_state(module); PyObject *key_obj = NULL; - char *key; long dklen; int retval; @@ -1682,24 +1681,24 @@ pbkdf2_hmac_impl(PyObject *module, const char *hash_name, goto end; } - key_obj = PyBytes_FromStringAndSize(NULL, dklen); - if (key_obj == NULL) { + PyBytesWriter *writer = PyBytesWriter_Create(dklen); + if (writer == NULL) { goto end; } - key = PyBytes_AS_STRING(key_obj); Py_BEGIN_ALLOW_THREADS retval = PKCS5_PBKDF2_HMAC((const char *)password->buf, (int)password->len, (const unsigned char *)salt->buf, (int)salt->len, iterations, digest, dklen, - (unsigned char *)key); + (unsigned char *)PyBytesWriter_GetData(writer)); Py_END_ALLOW_THREADS if (!retval) { - Py_CLEAR(key_obj); + PyBytesWriter_Discard(writer); notify_ssl_error_occurred_in(Py_STRINGIFY(PKCS5_PBKDF2_HMAC)); goto end; } + key_obj = PyBytesWriter_Finish(writer); end: if (digest != NULL) { @@ -1799,7 +1798,7 @@ _hashlib_scrypt_impl(PyObject *module, Py_buffer *password, Py_buffer *salt, (const char *)password->buf, (size_t)password->len, (const unsigned char *)salt->buf, (size_t)salt->len, (uint64_t)n, (uint64_t)r, (uint64_t)p, (uint64_t)maxmem, - PyBytesWriter_GetData(writer), (size_t)dklen + (unsigned char *)PyBytesWriter_GetData(writer), (size_t)dklen ); Py_END_ALLOW_THREADS From 537133d2b63611ce1c04aac4c283c932dee9985a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Mon, 15 Sep 2025 18:51:34 +0200 Subject: [PATCH 09/13] gh-69605: Hardcode some stdlib submodules in PyREPL module completion (os.path, collections.abc...) (GH-138268) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Łukasz Langa --- Lib/_pyrepl/_module_completer.py | 31 +++++++++- Lib/test/test_pyrepl/test_pyrepl.py | 62 +++++++++++++++---- ...5-08-30-17-15-05.gh-issue-69605.KjBk99.rst | 1 + 3 files changed, 80 insertions(+), 14 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-08-30-17-15-05.gh-issue-69605.KjBk99.rst diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index 1e9462a42156d4..cf59e007f4df80 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -1,9 +1,12 @@ from __future__ import annotations +import importlib +import os import pkgutil import sys import token import tokenize +from importlib.machinery import FileFinder from io import StringIO from contextlib import contextmanager from dataclasses import dataclass @@ -16,6 +19,15 @@ from typing import Any, Iterable, Iterator, Mapping +HARDCODED_SUBMODULES = { + # Standard library submodules that are not detected by pkgutil.iter_modules + # but can be imported, so should be proposed in completion + "collections": ["abc"], + "os": ["path"], + "xml.parsers.expat": ["errors", "model"], +} + + def make_default_module_completer() -> ModuleCompleter: # Inside pyrepl, __package__ is set to None by default return ModuleCompleter(namespace={'__package__': None}) @@ -41,6 +53,7 @@ def __init__(self, namespace: Mapping[str, Any] | None = None) -> None: self.namespace = namespace or {} self._global_cache: list[pkgutil.ModuleInfo] = [] self._curr_sys_path: list[str] = sys.path[:] + self._stdlib_path = os.path.dirname(importlib.__path__[0]) def get_completions(self, line: str) -> list[str] | None: """Return the next possible import completions for 'line'.""" @@ -95,12 +108,26 @@ def _find_modules(self, path: str, prefix: str) -> list[str]: return [] modules: Iterable[pkgutil.ModuleInfo] = self.global_cache + is_stdlib_import: bool | None = None for segment in path.split('.'): modules = [mod_info for mod_info in modules if mod_info.ispkg and mod_info.name == segment] + if is_stdlib_import is None: + # Top-level import decide if we import from stdlib or not + is_stdlib_import = all( + self._is_stdlib_module(mod_info) for mod_info in modules + ) modules = self.iter_submodules(modules) - return [module.name for module in modules - if self.is_suggestion_match(module.name, prefix)] + + module_names = [module.name for module in modules] + if is_stdlib_import: + module_names.extend(HARDCODED_SUBMODULES.get(path, ())) + return [module_name for module_name in module_names + if self.is_suggestion_match(module_name, prefix)] + + def _is_stdlib_module(self, module_info: pkgutil.ModuleInfo) -> bool: + return (isinstance(module_info.module_finder, FileFinder) + and module_info.module_finder.path == self._stdlib_path) def is_suggestion_match(self, module_name: str, prefix: str) -> bool: if prefix: diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 8e4450fdf99ecd..47d384a209e9ac 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1,3 +1,4 @@ +import importlib import io import itertools import os @@ -26,9 +27,16 @@ code_to_events, ) from _pyrepl.console import Event -from _pyrepl._module_completer import ImportParser, ModuleCompleter -from _pyrepl.readline import (ReadlineAlikeReader, ReadlineConfig, - _ReadlineWrapper) +from _pyrepl._module_completer import ( + ImportParser, + ModuleCompleter, + HARDCODED_SUBMODULES, +) +from _pyrepl.readline import ( + ReadlineAlikeReader, + ReadlineConfig, + _ReadlineWrapper, +) from _pyrepl.readline import multiline_input as readline_multiline_input try: @@ -930,7 +938,6 @@ def test_func(self): class TestPyReplModuleCompleter(TestCase): def setUp(self): - import importlib # Make iter_modules() search only the standard library. # This makes the test more reliable in case there are # other user packages/scripts on PYTHONPATH which can @@ -1013,14 +1020,6 @@ def test_sub_module_private_completions(self): self.assertEqual(output, expected) def test_builtin_completion_top_level(self): - import importlib - # Make iter_modules() search only the standard library. - # This makes the test more reliable in case there are - # other user packages/scripts on PYTHONPATH which can - # intefere with the completions. - lib_path = os.path.dirname(importlib.__path__[0]) - sys.path = [lib_path] - cases = ( ("import bui\t\n", "import builtins"), ("from bui\t\n", "from builtins"), @@ -1076,6 +1075,32 @@ def test_no_fallback_on_regular_completion(self): output = reader.readline() self.assertEqual(output, expected) + def test_hardcoded_stdlib_submodules(self): + cases = ( + ("import collections.\t\n", "import collections.abc"), + ("from os import \t\n", "from os import path"), + ("import xml.parsers.expat.\t\te\t\n\n", "import xml.parsers.expat.errors"), + ("from xml.parsers.expat import \t\tm\t\n\n", "from xml.parsers.expat import model"), + ) + for code, expected in cases: + with self.subTest(code=code): + events = code_to_events(code) + reader = self.prepare_reader(events, namespace={}) + output = reader.readline() + self.assertEqual(output, expected) + + def test_hardcoded_stdlib_submodules_not_proposed_if_local_import(self): + with tempfile.TemporaryDirectory() as _dir: + dir = pathlib.Path(_dir) + (dir / "collections").mkdir() + (dir / "collections" / "__init__.py").touch() + (dir / "collections" / "foo.py").touch() + with patch.object(sys, "path", [dir, *sys.path]): + events = code_to_events("import collections.\t\n") + reader = self.prepare_reader(events, namespace={}) + output = reader.readline() + self.assertEqual(output, "import collections.foo") + def test_get_path_and_prefix(self): cases = ( ('', ('', '')), @@ -1204,6 +1229,19 @@ def test_parse_error(self): with self.subTest(code=code): self.assertEqual(actual, None) + +class TestHardcodedSubmodules(TestCase): + def test_hardcoded_stdlib_submodules_are_importable(self): + for parent_path, submodules in HARDCODED_SUBMODULES.items(): + for module_name in submodules: + path = f"{parent_path}.{module_name}" + with self.subTest(path=path): + # We can't use importlib.util.find_spec here, + # since some hardcoded submodules parents are + # not proper packages + importlib.import_module(path) + + class TestPasteEvent(TestCase): def prepare_reader(self, events): console = FakeConsole(events) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-30-17-15-05.gh-issue-69605.KjBk99.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-30-17-15-05.gh-issue-69605.KjBk99.rst new file mode 100644 index 00000000000000..d855470fc2b326 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-30-17-15-05.gh-issue-69605.KjBk99.rst @@ -0,0 +1 @@ +Fix some standard library submodules missing from the :term:`REPL` auto-completion of imports. From dfd52e7a8b5a7e5a5d64b11742ac7242ec938ede Mon Sep 17 00:00:00 2001 From: Mehdi Hassan <44486547+mehdihassan93@users.noreply.github.com> Date: Mon, 15 Sep 2025 19:50:46 +0100 Subject: [PATCH 10/13] gh-97517: Add documentation links to datetime strftime/strptime docstrings (#138559) * Add documentation links to datetime strftime/strptime docstrings - Add links to format codes documentation for all strftime methods - Add links to format codes documentation for all strptime methods - Addresses issue #97517 * Update C extension docstrings with format codes documentation * Regenerate clinic code for updated docstrings * Add clinic-generated header file for updated docstrings * Fix docstring spacing consistency in both Python and C files * Update Lib/_pydatetime.py Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> --------- Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> --- Lib/_pydatetime.py | 24 +++++++++++++++++++++--- Modules/_datetimemodule.c | 25 ++++++++++++++++++++----- Modules/clinic/_datetimemodule.c.h | 27 +++++++++++++++++++++------ 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index ab1b1722b7c254..b6d68f2372850a 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -1072,7 +1072,11 @@ def fromisocalendar(cls, year, week, day): @classmethod def strptime(cls, date_string, format): - """Parse string according to the given date format (like time.strptime()).""" + """Parse string according to the given date format (like time.strptime()). + + For a list of supported format codes, see the documentation: + https://docs.python.org/3/library/datetime.html#format-codes + """ import _strptime return _strptime._strptime_datetime_date(cls, date_string, format) @@ -1109,6 +1113,8 @@ def strftime(self, format): Format using strftime(). Example: "%d/%m/%Y, %H:%M:%S" + For a list of supported format codes, see the documentation: + https://docs.python.org/3/library/datetime.html#format-codes """ return _wrap_strftime(self, format, self.timetuple()) @@ -1456,8 +1462,13 @@ def __new__(cls, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold return self @classmethod + def strptime(cls, date_string, format): - """Parse string according to the given time format (like time.strptime()).""" + """Parse string according to the given time format (like time.strptime()). + + For a list of supported format codes, see the documentation: + https://docs.python.org/3/library/datetime.html#format-codes + """ import _strptime return _strptime._strptime_datetime_time(cls, date_string, format) @@ -1650,6 +1661,9 @@ def fromisoformat(cls, time_string): def strftime(self, format): """Format using strftime(). The date part of the timestamp passed to underlying strftime should not be used. + + For a list of supported format codes, see the documentation: + https://docs.python.org/3/library/datetime.html#format-codes """ # The year must be >= 1000 else Python's strftime implementation # can raise a bogus exception. @@ -2198,7 +2212,11 @@ def __str__(self): @classmethod def strptime(cls, date_string, format): - """Parse string according to the given date and time format (like time.strptime()).""" + """Parse string according to the given time format (like time.strptime()). + + For a list of supported format codes, see the documentation: + https://docs.python.org/3/library/datetime.html#format-codes + """ import _strptime return _strptime._strptime_datetime_datetime(cls, date_string, format) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 8f1ddf330f3940..329be68d5b8285 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -3468,12 +3468,15 @@ datetime.date.strptime / Parse string according to the given date format (like time.strptime()). + +For a list of supported format codes, see the documentation: + https://docs.python.org/3/library/datetime.html#format-codes [clinic start generated code]*/ static PyObject * datetime_date_strptime_impl(PyTypeObject *type, PyObject *string, PyObject *format) -/*[clinic end generated code: output=454d473bee2d5161 input=001904ab34f594a1]*/ +/*[clinic end generated code: output=454d473bee2d5161 input=31d57bb789433e99]*/ { PyObject *result; @@ -3608,11 +3611,14 @@ datetime.date.strftime Format using strftime(). Example: "%d/%m/%Y, %H:%M:%S". + +For a list of supported format codes, see the documentation: + https://docs.python.org/3/library/datetime.html#format-codes [clinic start generated code]*/ static PyObject * datetime_date_strftime_impl(PyObject *self, PyObject *format) -/*[clinic end generated code: output=6529b70095e16778 input=72af55077e606ed8]*/ +/*[clinic end generated code: output=6529b70095e16778 input=b6fd4a2ded27b557]*/ { /* This method can be inherited, and needs to call the * timetuple() method appropriate to self's class. @@ -4711,12 +4717,15 @@ datetime.time.strptime / Parse string according to the given time format (like time.strptime()). + +For a list of supported format codes, see the documentation: + https://docs.python.org/3/library/datetime.html#format-codes [clinic start generated code]*/ static PyObject * datetime_time_strptime_impl(PyTypeObject *type, PyObject *string, PyObject *format) -/*[clinic end generated code: output=ae05a9bc0241d3bf input=6d0f263a5f94d78d]*/ +/*[clinic end generated code: output=ae05a9bc0241d3bf input=82ba425ecacc54aa]*/ { PyObject *result; @@ -4891,11 +4900,14 @@ datetime.time.strftime Format using strftime(). The date part of the timestamp passed to underlying strftime should not be used. + +For a list of supported format codes, see the documentation: + https://docs.python.org/3/library/datetime.html#format-codes [clinic start generated code]*/ static PyObject * datetime_time_strftime_impl(PyDateTime_Time *self, PyObject *format) -/*[clinic end generated code: output=10f65af20e2a78c7 input=541934a2860f7db5]*/ +/*[clinic end generated code: output=10f65af20e2a78c7 input=c4a5bbecd798654b]*/ { PyObject *result; PyObject *tuple; @@ -5787,12 +5799,15 @@ datetime.datetime.strptime / Parse string according to the given date and time format (like time.strptime()). + +For a list of supported format codes, see the documentation: + https://docs.python.org/3/library/datetime.html#format-codes [clinic start generated code]*/ static PyObject * datetime_datetime_strptime_impl(PyTypeObject *type, PyObject *string, PyObject *format) -/*[clinic end generated code: output=af2c2d024f3203f5 input=d7597c7f5327117b]*/ +/*[clinic end generated code: output=af2c2d024f3203f5 input=ef7807589f1d50e7]*/ { PyObject *result; diff --git a/Modules/clinic/_datetimemodule.c.h b/Modules/clinic/_datetimemodule.c.h index 7c4bd5503ed56b..ee621c150c31e4 100644 --- a/Modules/clinic/_datetimemodule.c.h +++ b/Modules/clinic/_datetimemodule.c.h @@ -371,7 +371,10 @@ PyDoc_STRVAR(datetime_date_strptime__doc__, "strptime($type, string, format, /)\n" "--\n" "\n" -"Parse string according to the given date format (like time.strptime())."); +"Parse string according to the given date format (like time.strptime()).\n" +"\n" +"For a list of supported format codes, see the documentation:\n" +" https://docs.python.org/3/library/datetime.html#format-codes"); #define DATETIME_DATE_STRPTIME_METHODDEF \ {"strptime", _PyCFunction_CAST(datetime_date_strptime), METH_FASTCALL|METH_CLASS, datetime_date_strptime__doc__}, @@ -412,7 +415,10 @@ PyDoc_STRVAR(datetime_date_strftime__doc__, "\n" "Format using strftime().\n" "\n" -"Example: \"%d/%m/%Y, %H:%M:%S\"."); +"Example: \"%d/%m/%Y, %H:%M:%S\".\n" +"\n" +"For a list of supported format codes, see the documentation:\n" +" https://docs.python.org/3/library/datetime.html#format-codes"); #define DATETIME_DATE_STRFTIME_METHODDEF \ {"strftime", _PyCFunction_CAST(datetime_date_strftime), METH_FASTCALL|METH_KEYWORDS, datetime_date_strftime__doc__}, @@ -847,7 +853,10 @@ PyDoc_STRVAR(datetime_time_strptime__doc__, "strptime($type, string, format, /)\n" "--\n" "\n" -"Parse string according to the given time format (like time.strptime())."); +"Parse string according to the given time format (like time.strptime()).\n" +"\n" +"For a list of supported format codes, see the documentation:\n" +" https://docs.python.org/3/library/datetime.html#format-codes"); #define DATETIME_TIME_STRPTIME_METHODDEF \ {"strptime", _PyCFunction_CAST(datetime_time_strptime), METH_FASTCALL|METH_CLASS, datetime_time_strptime__doc__}, @@ -970,7 +979,10 @@ PyDoc_STRVAR(datetime_time_strftime__doc__, "\n" "Format using strftime().\n" "\n" -"The date part of the timestamp passed to underlying strftime should not be used."); +"The date part of the timestamp passed to underlying strftime should not be used.\n" +"\n" +"For a list of supported format codes, see the documentation:\n" +" https://docs.python.org/3/library/datetime.html#format-codes"); #define DATETIME_TIME_STRFTIME_METHODDEF \ {"strftime", _PyCFunction_CAST(datetime_time_strftime), METH_FASTCALL|METH_KEYWORDS, datetime_time_strftime__doc__}, @@ -1569,7 +1581,10 @@ PyDoc_STRVAR(datetime_datetime_strptime__doc__, "strptime($type, string, format, /)\n" "--\n" "\n" -"Parse string according to the given date and time format (like time.strptime())."); +"Parse string according to the given date and time format (like time.strptime()).\n" +"\n" +"For a list of supported format codes, see the documentation:\n" +" https://docs.python.org/3/library/datetime.html#format-codes"); #define DATETIME_DATETIME_STRPTIME_METHODDEF \ {"strptime", _PyCFunction_CAST(datetime_datetime_strptime), METH_FASTCALL|METH_CLASS, datetime_datetime_strptime__doc__}, @@ -2075,4 +2090,4 @@ datetime_datetime___reduce__(PyObject *self, PyObject *Py_UNUSED(ignored)) { return datetime_datetime___reduce___impl((PyDateTime_DateTime *)self); } -/*[clinic end generated code: output=0b8403bc58982e60 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=69658acff6a43ac4 input=a9049054013a1b77]*/ From c5fb72ea6ced51a7d20d5b203dc832558d972321 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 15 Sep 2025 21:25:47 +0100 Subject: [PATCH 11/13] gh-129813, PEP 782: Use PyBytesWriter in _winapi.PeekNamedPipe() (#138930) Replace PyBytes_FromStringAndSize(NULL, size) and _PyBytes_Resize() with the new public PyBytesWriter API. --- Modules/_winapi.c | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Modules/_winapi.c b/Modules/_winapi.c index b4cfbebcb1bb5e..2aebe44c70921d 100644 --- a/Modules/_winapi.c +++ b/Modules/_winapi.c @@ -1920,7 +1920,6 @@ static PyObject * _winapi_PeekNamedPipe_impl(PyObject *module, HANDLE handle, int size) /*[clinic end generated code: output=d0c3e29e49d323dd input=c7aa53bfbce69d70]*/ { - PyObject *buf = NULL; DWORD nread, navail, nleft; BOOL ret; @@ -1930,20 +1929,26 @@ _winapi_PeekNamedPipe_impl(PyObject *module, HANDLE handle, int size) } if (size) { - buf = PyBytes_FromStringAndSize(NULL, size); - if (!buf) + PyBytesWriter *writer = PyBytesWriter_Create(size); + if (writer == NULL) { return NULL; + } + char *buf = PyBytesWriter_GetData(writer); + Py_BEGIN_ALLOW_THREADS - ret = PeekNamedPipe(handle, PyBytes_AS_STRING(buf), size, &nread, + ret = PeekNamedPipe(handle, buf, size, &nread, &navail, &nleft); Py_END_ALLOW_THREADS if (!ret) { - Py_DECREF(buf); + PyBytesWriter_Discard(writer); return PyErr_SetExcFromWindowsErr(PyExc_OSError, 0); } - if (_PyBytes_Resize(&buf, nread)) + + PyObject *res = PyBytesWriter_FinishWithSize(writer, nread); + if (res == NULL) { return NULL; - return Py_BuildValue("NII", buf, navail, nleft); + } + return Py_BuildValue("NII", res, navail, nleft); } else { Py_BEGIN_ALLOW_THREADS From 71defb69438ce13708d8f9a73756d2073e0b2081 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Mon, 15 Sep 2025 13:38:01 -0700 Subject: [PATCH 12/13] gh-129813, PEP 782: Use PyBytesWriter in _io.FileIO.readall (#138901) Performance about the same, using the benchmark from gh-120754: ``` pyperf compare_to main.json pep782.json read_file_small: Mean +- std dev: [main] 5.71 us +- 0.05 us -> [pep782] 5.68 us +- 0.05 us: 1.01x faster read_file_large: Mean +- std dev: [main] 89.7 us +- 0.9 us -> [pep782] 86.9 us +- 0.8 us: 1.03x faster read_all_rst_bytes: Mean +- std dev: [main] 926 us +- 8 us -> [pep782] 920 us +- 12 us: 1.01x faster read_all_rst_text: Mean +- std dev: [main] 2.24 ms +- 0.02 ms -> [pep782] 2.17 ms +- 0.04 ms: 1.03x faster Benchmark hidden because not significant (1): read_all_py_bytes Geometric mean: 1.01x faster ``` --- Modules/_io/fileio.c | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c index c0b6c6425184af..ed731da32beb43 100644 --- a/Modules/_io/fileio.c +++ b/Modules/_io/fileio.c @@ -739,7 +739,7 @@ _io_FileIO_readall_impl(fileio *self) /*[clinic end generated code: output=faa0292b213b4022 input=10d8b2ec403302dc]*/ { Py_off_t pos, end; - PyObject *result; + PyBytesWriter *writer; Py_ssize_t bytes_read = 0; Py_ssize_t n; size_t bufsize; @@ -794,10 +794,10 @@ _io_FileIO_readall_impl(fileio *self) } } - - result = PyBytes_FromStringAndSize(NULL, bufsize); - if (result == NULL) + writer = PyBytesWriter_Create(bufsize); + if (writer == NULL) { return NULL; + } while (1) { if (bytes_read >= (Py_ssize_t)bufsize) { @@ -806,18 +806,18 @@ _io_FileIO_readall_impl(fileio *self) PyErr_SetString(PyExc_OverflowError, "unbounded read returned more bytes " "than a Python bytes object can hold"); - Py_DECREF(result); + PyBytesWriter_Discard(writer); return NULL; } - if (PyBytes_GET_SIZE(result) < (Py_ssize_t)bufsize) { - if (_PyBytes_Resize(&result, bufsize) < 0) + if (PyBytesWriter_GetSize(writer) < (Py_ssize_t)bufsize) { + if (PyBytesWriter_Resize(writer, bufsize) < 0) return NULL; } } n = _Py_read(self->fd, - PyBytes_AS_STRING(result) + bytes_read, + (char*)PyBytesWriter_GetData(writer) + bytes_read, bufsize - bytes_read); if (n == 0) @@ -827,20 +827,16 @@ _io_FileIO_readall_impl(fileio *self) PyErr_Clear(); if (bytes_read > 0) break; - Py_DECREF(result); + PyBytesWriter_Discard(writer); Py_RETURN_NONE; } - Py_DECREF(result); + PyBytesWriter_Discard(writer); return NULL; } bytes_read += n; } - if (PyBytes_GET_SIZE(result) > bytes_read) { - if (_PyBytes_Resize(&result, bytes_read) < 0) - return NULL; - } - return result; + return PyBytesWriter_FinishWithSize(writer, bytes_read); } /*[clinic input] From f62b495f792b579f610a7fbf93c8e4c53ccf1369 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 15 Sep 2025 21:53:03 +0100 Subject: [PATCH 13/13] gh-129813, PEP 782: Use PyBytesWriter in _sha3 (#138923) Replace PyBytes_FromStringAndSize(NULL, size) with the new public PyBytesWriter API. --- Modules/sha3module.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Modules/sha3module.c b/Modules/sha3module.c index 47fe5e57e6ddd9..14c543b86415d5 100644 --- a/Modules/sha3module.c +++ b/Modules/sha3module.c @@ -509,14 +509,19 @@ _sha3_shake_128_digest_impl(SHA3object *self, Py_ssize_t length) if (length == 0) { return Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); } - CHECK_HACL_UINT32_T_LENGTH(length); - PyObject *digest = PyBytes_FromStringAndSize(NULL, length); - uint8_t *buffer = (uint8_t *)PyBytes_AS_STRING(digest); + + PyBytesWriter *writer = PyBytesWriter_Create(length); + if (writer == NULL) { + return NULL; + } + uint8_t *buffer = (uint8_t *)PyBytesWriter_GetData(writer); + HASHLIB_ACQUIRE_LOCK(self); (void)Hacl_Hash_SHA3_squeeze(self->hash_state, buffer, (uint32_t)length); HASHLIB_RELEASE_LOCK(self); - return digest; + + return PyBytesWriter_Finish(writer); } @@ -540,8 +545,8 @@ _sha3_shake_128_hexdigest_impl(SHA3object *self, Py_ssize_t length) if (length == 0) { return Py_GetConstant(Py_CONSTANT_EMPTY_STR); } - CHECK_HACL_UINT32_T_LENGTH(length); + uint8_t *buffer = PyMem_Malloc(length); if (buffer == NULL) { return PyErr_NoMemory(); @@ -550,6 +555,7 @@ _sha3_shake_128_hexdigest_impl(SHA3object *self, Py_ssize_t length) HASHLIB_ACQUIRE_LOCK(self); (void)Hacl_Hash_SHA3_squeeze(self->hash_state, buffer, (uint32_t)length); HASHLIB_RELEASE_LOCK(self); + PyObject *digest = _Py_strhex((const char *)buffer, length); PyMem_Free(buffer); return digest;