From 28fb13cb33d569720938258db68956b5f9c9eb40 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 6 Feb 2026 19:38:58 +0200 Subject: [PATCH 1/3] gh-143658: Use `str.lower` and `replace` to further improve performance of `importlib.metadata.Prepared.normalized` (#144083) Co-authored-by: Henry Schreiner --- Lib/importlib/metadata/__init__.py | 13 ++----------- .../2026-01-20-20-54-46.gh-issue-143658.v8i1jE.rst | 4 ++++ 2 files changed, 6 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-01-20-20-54-46.gh-issue-143658.v8i1jE.rst diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py index 9b723b4ec15e12..7cf4d29d330c91 100644 --- a/Lib/importlib/metadata/__init__.py +++ b/Lib/importlib/metadata/__init__.py @@ -890,14 +890,6 @@ def search(self, prepared: Prepared): return itertools.chain(infos, eggs) -# Translation table for Prepared.normalize: lowercase and -# replace "-" (hyphen) and "." (dot) with "_" (underscore). -_normalize_table = str.maketrans( - "ABCDEFGHIJKLMNOPQRSTUVWXYZ-.", - "abcdefghijklmnopqrstuvwxyz__", -) - - class Prepared: """ A prepared search query for metadata on a possibly-named package. @@ -933,9 +925,8 @@ def normalize(name): """ PEP 503 normalization plus dashes as underscores. """ - # Emulates ``re.sub(r"[-_.]+", "-", name).lower()`` from PEP 503 - # About 3x faster, safe since packages only support alphanumeric characters - value = name.translate(_normalize_table) + # Much faster than re.sub, and even faster than str.translate + value = name.lower().replace("-", "_").replace(".", "_") # Condense repeats (faster than regex) while "__" in value: value = value.replace("__", "_") diff --git a/Misc/NEWS.d/next/Library/2026-01-20-20-54-46.gh-issue-143658.v8i1jE.rst b/Misc/NEWS.d/next/Library/2026-01-20-20-54-46.gh-issue-143658.v8i1jE.rst new file mode 100644 index 00000000000000..8935b4c655023a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-20-20-54-46.gh-issue-143658.v8i1jE.rst @@ -0,0 +1,4 @@ +:mod:`importlib.metadata`: Use :meth:`str.lower` and :meth:`str.replace` to +further improve performance of +:meth:`!importlib.metadata.Prepared.normalize`. Patch by Hugo van Kemenade +and Henry Schreiner. From 895e83727d6d686ab7dd5494f3ccd52b0726502e Mon Sep 17 00:00:00 2001 From: Ken Jin Date: Sat, 7 Feb 2026 03:20:28 +0800 Subject: [PATCH 2/3] gh-144549: Fix tail calling interpreter on Windows for FT (GH-144550) --- .github/workflows/tail-call.yml | 14 +++++++++- Include/internal/pycore_ceval.h | 5 ++++ ...-02-06-17-59-47.gh-issue-144549.5BhPlY.rst | 1 + Modules/_testinternalcapi/test_cases.c.h | 23 ++------------- Python/bytecodes.c | 28 ++----------------- Python/ceval.c | 28 +++++++++++++++++++ Python/executor_cases.c.h | 26 ++++------------- Python/generated_cases.c.h | 23 ++------------- 8 files changed, 62 insertions(+), 86 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-02-06-17-59-47.gh-issue-144549.5BhPlY.rst diff --git a/.github/workflows/tail-call.yml b/.github/workflows/tail-call.yml index 853d149d20640c..a47e532d396bc0 100644 --- a/.github/workflows/tail-call.yml +++ b/.github/workflows/tail-call.yml @@ -38,6 +38,7 @@ jobs: # Un-comment as we add support for more platforms for tail-calling interpreters. # - i686-pc-windows-msvc/msvc - x86_64-pc-windows-msvc/msvc + - free-threading-msvc # - aarch64-pc-windows-msvc/msvc - x86_64-apple-darwin/clang - aarch64-apple-darwin/clang @@ -53,6 +54,9 @@ jobs: - target: x86_64-pc-windows-msvc/msvc architecture: x64 runner: windows-2025-vs2026 + - target: free-threading-msvc + architecture: x64 + runner: windows-2025-vs2026 # - target: aarch64-pc-windows-msvc/msvc # architecture: ARM64 # runner: windows-2022 @@ -80,13 +84,21 @@ jobs: python-version: '3.11' - name: Native Windows MSVC (release) - if: runner.os == 'Windows' && matrix.architecture != 'ARM64' + if: runner.os == 'Windows' && matrix.architecture != 'ARM64' && matrix.target != 'free-threading-msvc' shell: pwsh run: | $env:PlatformToolset = "v145" ./PCbuild/build.bat --tail-call-interp -c Release -p ${{ matrix.architecture }} ./PCbuild/rt.bat -p ${{ matrix.architecture }} -q --multiprocess 0 --timeout 4500 --verbose2 --verbose3 + # No tests: + - name: Native Windows MSVC with free-threading (release) + if: matrix.target == 'free-threading-msvc' + shell: pwsh + run: | + $env:PlatformToolset = "v145" + ./PCbuild/build.bat --tail-call-interp --disable-gil -c Release -p ${{ matrix.architecture }} + # No tests (yet): - name: Emulated Windows Clang (release) if: runner.os == 'Windows' && matrix.architecture == 'ARM64' diff --git a/Include/internal/pycore_ceval.h b/Include/internal/pycore_ceval.h index f6bdba3e9916c0..e9f1f65e53cec1 100644 --- a/Include/internal/pycore_ceval.h +++ b/Include/internal/pycore_ceval.h @@ -474,6 +474,11 @@ _Py_assert_within_stack_bounds( _PyInterpreterFrame *frame, _PyStackRef *stack_pointer, const char *filename, int lineno); +PyAPI_FUNC(_PyStackRef) +_Py_LoadAttr_StackRefSteal( + PyThreadState *tstate, _PyStackRef owner, + PyObject *name, _PyStackRef *self_or_null); + // Like PyMapping_GetOptionalItem, but returns the PyObject* instead of taking // it as an out parameter. This helps MSVC's escape analysis when used with // tail calling. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-06-17-59-47.gh-issue-144549.5BhPlY.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-06-17-59-47.gh-issue-144549.5BhPlY.rst new file mode 100644 index 00000000000000..9679e2cf6af426 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-06-17-59-47.gh-issue-144549.5BhPlY.rst @@ -0,0 +1 @@ +Fix building the tail calling interpreter on Visual Studio 2026 with free-threading. diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index 2a73a554eda2cc..dda3bc53dc5e0d 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -7899,28 +7899,11 @@ self_or_null = &stack_pointer[0]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 1); if (oparg & 1) { - _PyCStackRef method; _PyFrame_SetStackPointer(frame, stack_pointer); - _PyThreadState_PushCStackRef(tstate, &method); - int is_meth = _PyObject_GetMethodStackRef(tstate, PyStackRef_AsPyObjectBorrow(owner), name, &method.ref); + attr = _Py_LoadAttr_StackRefSteal(tstate, owner, name, self_or_null); stack_pointer = _PyFrame_GetStackPointer(frame); - if (is_meth) { - assert(!PyStackRef_IsNull(method.ref)); - self_or_null[0] = owner; - attr = _PyThreadState_PopCStackRefSteal(tstate, &method); - } - else { - stack_pointer += -1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - _PyFrame_SetStackPointer(frame, stack_pointer); - PyStackRef_CLOSE(owner); - stack_pointer = _PyFrame_GetStackPointer(frame); - self_or_null[0] = PyStackRef_NULL; - attr = _PyThreadState_PopCStackRefSteal(tstate, &method); - if (PyStackRef_IsNull(attr)) { - JUMP_TO_LABEL(error); - } - stack_pointer += 1; + if (PyStackRef_IsNull(attr)) { + JUMP_TO_LABEL(pop_1_error); } } else { diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 818b4fbc3801c0..bd22599aef725d 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -2364,31 +2364,9 @@ dummy_func( PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 1); if (oparg & 1) { /* Designed to work in tandem with CALL, pushes two values. */ - _PyCStackRef method; - _PyThreadState_PushCStackRef(tstate, &method); - int is_meth = _PyObject_GetMethodStackRef(tstate, PyStackRef_AsPyObjectBorrow(owner), name, &method.ref); - if (is_meth) { - /* We can bypass temporary bound method object. - meth is unbound method and obj is self. - meth | self | arg1 | ... | argN - */ - assert(!PyStackRef_IsNull(method.ref)); // No errors on this branch - self_or_null[0] = owner; // Transfer ownership - DEAD(owner); - attr = _PyThreadState_PopCStackRefSteal(tstate, &method); - } - else { - /* meth is not an unbound method (but a regular attr, or - something was returned by a descriptor protocol). Set - the second element of the stack to NULL, to signal - CALL that it's not a method call. - meth | NULL | arg1 | ... | argN - */ - PyStackRef_CLOSE(owner); - self_or_null[0] = PyStackRef_NULL; - attr = _PyThreadState_PopCStackRefSteal(tstate, &method); - ERROR_IF(PyStackRef_IsNull(attr)); - } + attr = _Py_LoadAttr_StackRefSteal(tstate, owner, name, self_or_null); + DEAD(owner); + ERROR_IF(PyStackRef_IsNull(attr)); } else { /* Classic, pushes one value. */ diff --git a/Python/ceval.c b/Python/ceval.c index 590b315ab65c2c..61644d35b5e473 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1007,6 +1007,34 @@ _Py_BuildMap_StackRefSteal( return res; } +_PyStackRef +_Py_LoadAttr_StackRefSteal( + PyThreadState *tstate, _PyStackRef owner, + PyObject *name, _PyStackRef *self_or_null) +{ + _PyCStackRef method; + _PyThreadState_PushCStackRef(tstate, &method); + int is_meth = _PyObject_GetMethodStackRef(tstate, PyStackRef_AsPyObjectBorrow(owner), name, &method.ref); + if (is_meth) { + /* We can bypass temporary bound method object. + meth is unbound method and obj is self. + meth | self | arg1 | ... | argN + */ + assert(!PyStackRef_IsNull(method.ref)); // No errors on this branch + self_or_null[0] = owner; // Transfer ownership + return _PyThreadState_PopCStackRefSteal(tstate, &method); + } + /* meth is not an unbound method (but a regular attr, or + something was returned by a descriptor protocol). Set + the second element of the stack to NULL, to signal + CALL that it's not a method call. + meth | NULL | arg1 | ... | argN + */ + PyStackRef_CLOSE(owner); + self_or_null[0] = PyStackRef_NULL; + return _PyThreadState_PopCStackRefSteal(tstate, &method); +} + #ifdef Py_DEBUG void _Py_assert_within_stack_bounds( diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index a98ec2200485d2..f8de66cbce3a9f 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -8670,32 +8670,18 @@ self_or_null = &stack_pointer[1]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 1); if (oparg & 1) { - _PyCStackRef method; stack_pointer[0] = owner; stack_pointer += 1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); - _PyThreadState_PushCStackRef(tstate, &method); - int is_meth = _PyObject_GetMethodStackRef(tstate, PyStackRef_AsPyObjectBorrow(owner), name, &method.ref); + attr = _Py_LoadAttr_StackRefSteal(tstate, owner, name, self_or_null); stack_pointer = _PyFrame_GetStackPointer(frame); - if (is_meth) { - assert(!PyStackRef_IsNull(method.ref)); - self_or_null[0] = owner; - attr = _PyThreadState_PopCStackRefSteal(tstate, &method); - } - else { - stack_pointer += -1; + if (PyStackRef_IsNull(attr)) { + stack_pointer[-1] = attr; + stack_pointer += (oparg&1); ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - _PyFrame_SetStackPointer(frame, stack_pointer); - PyStackRef_CLOSE(owner); - stack_pointer = _PyFrame_GetStackPointer(frame); - self_or_null[0] = PyStackRef_NULL; - attr = _PyThreadState_PopCStackRefSteal(tstate, &method); - if (PyStackRef_IsNull(attr)) { - SET_CURRENT_CACHED_VALUES(0); - JUMP_TO_ERROR(); - } - stack_pointer += 1; + SET_CURRENT_CACHED_VALUES(0); + JUMP_TO_ERROR(); } } else { diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index fc1144a88d70cc..4cc9d9e03a545d 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -7898,28 +7898,11 @@ self_or_null = &stack_pointer[0]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 1); if (oparg & 1) { - _PyCStackRef method; _PyFrame_SetStackPointer(frame, stack_pointer); - _PyThreadState_PushCStackRef(tstate, &method); - int is_meth = _PyObject_GetMethodStackRef(tstate, PyStackRef_AsPyObjectBorrow(owner), name, &method.ref); + attr = _Py_LoadAttr_StackRefSteal(tstate, owner, name, self_or_null); stack_pointer = _PyFrame_GetStackPointer(frame); - if (is_meth) { - assert(!PyStackRef_IsNull(method.ref)); - self_or_null[0] = owner; - attr = _PyThreadState_PopCStackRefSteal(tstate, &method); - } - else { - stack_pointer += -1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - _PyFrame_SetStackPointer(frame, stack_pointer); - PyStackRef_CLOSE(owner); - stack_pointer = _PyFrame_GetStackPointer(frame); - self_or_null[0] = PyStackRef_NULL; - attr = _PyThreadState_PopCStackRefSteal(tstate, &method); - if (PyStackRef_IsNull(attr)) { - JUMP_TO_LABEL(error); - } - stack_pointer += 1; + if (PyStackRef_IsNull(attr)) { + JUMP_TO_LABEL(pop_1_error); } } else { From 74db4404eaa9b4448b50ab7be7d50487dc0585fd Mon Sep 17 00:00:00 2001 From: Krishna-web-hub Date: Sat, 7 Feb 2026 02:56:44 +0530 Subject: [PATCH 3/3] gh-84116: Docs: Document help and aliases for argparse.add_parser() (#140574) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * gh-140281: Doc: Update free-threading how-to * gh-140281: Doc: Update free-threading how-to * Fix trailing whitespace * doc fixing of the cpython fixes#84116 * Docs: Document help and aliases for argparse.add_parser() (gh-84116) * Docs: Document help and aliases for argparse.add_parser() (gh-84116) * Fix trailing whitespace * Fix trailing whitespace * Fix trailing whitespace * Fix trailing whitespace * Fix trailing whitespace * Fix trailing whitespace * Fix trailing errors * Fix trailing errors and spaces * Fix docutils formatting, NEWS ref, and trailing whitespace * Docs: Update argparse.rst and add NEWS entry * Delete Doc/howto/free-threading-python.rst * Delete Misc/NEWS.d/next/Documentation/2025-10-25-00-49-43.gh-issue-140281.tuMQUe.rst * adding the depreceated tag * The error indexing was fixed * Fix trailing whitespace * Restore missing free-threading documentation * fixing some minor error * fixing some minor error part 2 * fixing some minor error part 3 * Fix NEWS entry format * Final cleanup of NEWS entry format 2 * changes in the argparse.rst * Remove unnecessary NEWS entry * Fixing the issue as requested * Added the Changes done before. * done the changes * done the changes#1 * Apply suggestion from @picnixz Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> * Apply suggestion from @picnixz Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> * Done the changes * Done the new changes * The versionadded is rectified * Update Doc/library/argparse.rst Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> * Docs editing * Docs fixing whitespace * Docs rectifiying * little bit rectification * Indentation rectification * Indentation rectification 1 * Apply suggestion from @picnixz Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> * deprecated rectification * Remove mistakenly added NEWS entry * Update Doc/library/argparse.rst Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> * changes #1 Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> * Update Doc/library/argparse.rst 2 Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> * removed useless thing Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> * changed according to the request Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> * Updated the test part --------- Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> --- Doc/library/argparse.rst | 73 +++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 31 deletions(-) diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index c7532ecd629b02..81e5d3470c3e57 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -1771,7 +1771,7 @@ Subcommands >>> parser.parse_args(['--foo', 'b', '--baz', 'Z']) Namespace(baz='Z', foo=True) - Note that the object returned by :meth:`parse_args` will only contain + Note that the object returned by :meth:`~ArgumentParser.parse_args` will only contain attributes for the main parser and the subparser that was selected by the command line (and not any other subparsers). So in the example above, when the ``a`` command is specified, only the ``foo`` and ``bar`` attributes are @@ -1814,7 +1814,7 @@ Subcommands -h, --help show this help message and exit --baz {X,Y,Z} baz help - The :meth:`add_subparsers` method also supports ``title`` and ``description`` + The :meth:`~ArgumentParser.add_subparsers` method also supports ``title`` and ``description`` keyword arguments. When either is present, the subparser's commands will appear in their own group in the help output. For example:: @@ -1835,34 +1835,8 @@ Subcommands {foo,bar} additional help - Furthermore, :meth:`~_SubParsersAction.add_parser` supports an additional - *aliases* argument, - which allows multiple strings to refer to the same subparser. This example, - like ``svn``, aliases ``co`` as a shorthand for ``checkout``:: - - >>> parser = argparse.ArgumentParser() - >>> subparsers = parser.add_subparsers() - >>> checkout = subparsers.add_parser('checkout', aliases=['co']) - >>> checkout.add_argument('foo') - >>> parser.parse_args(['co', 'bar']) - Namespace(foo='bar') - - :meth:`~_SubParsersAction.add_parser` supports also an additional - *deprecated* argument, which allows to deprecate the subparser. - - >>> import argparse - >>> parser = argparse.ArgumentParser(prog='chicken.py') - >>> subparsers = parser.add_subparsers() - >>> run = subparsers.add_parser('run') - >>> fly = subparsers.add_parser('fly', deprecated=True) - >>> parser.parse_args(['fly']) # doctest: +SKIP - chicken.py: warning: command 'fly' is deprecated - Namespace() - - .. versionadded:: 3.13 - One particularly effective way of handling subcommands is to combine the use - of the :meth:`add_subparsers` method with calls to :meth:`set_defaults` so + of the :meth:`~ArgumentParser.add_subparsers` method with calls to :meth:`~ArgumentParser.set_defaults` so that each subparser knows which Python function it should execute. For example:: @@ -1898,12 +1872,12 @@ Subcommands >>> args.func(args) ((XYZYX)) - This way, you can let :meth:`parse_args` do the job of calling the + This way, you can let :meth:`~ArgumentParser.parse_args` do the job of calling the appropriate function after argument parsing is complete. Associating functions with actions like this is typically the easiest way to handle the different actions for each of your subparsers. However, if it is necessary to check the name of the subparser that was invoked, the ``dest`` keyword - argument to the :meth:`add_subparsers` call will work:: + argument to the :meth:`~ArgumentParser.add_subparsers` call will work:: >>> parser = argparse.ArgumentParser() >>> subparsers = parser.add_subparsers(dest='subparser_name') @@ -1922,6 +1896,43 @@ Subcommands the main parser. +.. method:: _SubParsersAction.add_parser(name, *, help=None, aliases=None, + deprecated=False, **kwargs) + + Create and return a new :class:`ArgumentParser` object for the + subcommand *name*. + + The *name* argument is the name of the sub-command. + + The *help* argument provides a short description for this sub-command. + + The *aliases* argument allows providing alternative names for this + sub-command. For example:: + + >>> parser = argparse.ArgumentParser() + >>> subparsers = parser.add_subparsers() + >>> checkout = subparsers.add_parser('checkout', aliases=['co']) + >>> checkout.add_argument('foo') + >>> parser.parse_args(['co', 'bar']) + Namespace(foo='bar') + + The *deprecated* argument, if ``True``, marks the sub-command as + deprecated and will issue a warning when used. For example:: + + >>> parser = argparse.ArgumentParser(prog='chicken.py') + >>> subparsers = parser.add_subparsers() + >>> fly = subparsers.add_parser('fly', deprecated=True) + >>> args = parser.parse_args(['fly']) + chicken.py: warning: command 'fly' is deprecated + Namespace() + + All other keyword arguments are passed directly to the + :class:`!ArgumentParser` constructor. + + .. versionadded:: 3.13 + Added the *deprecated* parameter. + + FileType objects ^^^^^^^^^^^^^^^^