From 58d9c78d6aa45cf64237b571adbd0b3289a34559 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 27 Jul 2026 14:31:02 +0300 Subject: [PATCH 1/4] Update mypy to 2.3 (#154764) --- Tools/requirements-dev.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tools/requirements-dev.txt b/Tools/requirements-dev.txt index 46381ea58a12382..900c5028b0af604 100644 --- a/Tools/requirements-dev.txt +++ b/Tools/requirements-dev.txt @@ -1,7 +1,7 @@ # Requirements file for external linters and checks we run on # Tools/clinic, Tools/cases_generator/, and Tools/peg_generator/ in CI -mypy==2.1.0 +mypy==2.3.0 # needed for peg_generator: -types-psutil==7.2.2.20260508 -types-setuptools==82.0.0.20260508 +types-psutil==7.2.2.20260518 +types-setuptools==83.0.0.20260724 From 7961bbf2bbbd96679eba89419c34128481b9b0ff Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Mon, 27 Jul 2026 06:05:40 -0700 Subject: [PATCH 2/4] gh-122931: Allow stable abi3 API extensions to include a multiarch tuple in the filename (GH-152461) This permits stable ABI extensions for multiple architectures to be co-installed into the same directory, without clashing with each other, the same way (non-stable ABI) regular extensions can. It is listed before the current platform-less suffixes since it's more specific. The platform is stored in a new pyconfig.h define & sysconfig variable, SOABI_PLATFORM. On some known architectures (FreeBSD, Windows), this will be undefined/zero; these won't have the new tag (yet). Add SOABI_PLATFORM & ALT_SOABI the info to `make pythoninfo` as well. Co-authored-by: Petr Viktorin Co-authored-by: Victor Stinner --- Doc/whatsnew/3.15.rst | 8 ++++++++ Lib/test/pythoninfo.py | 9 +++++++++ Lib/test/test_importlib/extension/test_finder.py | 9 +++++++++ Lib/test/test_sysconfig.py | 9 +++++++++ .../C_API/2024-08-12-09-48-04.gh-issue-122931.x435Mb.rst | 1 + Python/dynload_shlib.c | 6 ++++++ configure | 6 ++++++ configure.ac | 4 ++++ pyconfig.h.in | 3 +++ 9 files changed, 55 insertions(+) create mode 100644 Misc/NEWS.d/next/C_API/2024-08-12-09-48-04.gh-issue-122931.x435Mb.rst diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index df2ca138db32b50..250558dd1341a49 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -873,6 +873,14 @@ Other language changes imports ``pkg.sub.mod``. (Contributed by Gregory P. Smith in :gh:`83065`.) +* File names of Stable ABI extensions that use the ``.so`` suffix may now + include a multiarch tuple, for example, ``foo.abi3-x86-64-linux-gnu.so``. + This permits stable ABI extensions for multiple architectures to be + co-installed into the same directory, without clashing with each + other, as regular dynamic extensions do. + (Contributed by Stefano Rivera in :gh:`122931`.) + + Default interactive shell ========================= diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index 13a3199b1f1267b..ea7edb798051567 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -570,6 +570,7 @@ def collect_sysconfig(info_add): for name in ( 'ABIFLAGS', + 'ALT_SOABI', 'ANDROID_API_LEVEL', 'CC', 'CCSHARED', @@ -595,6 +596,7 @@ def collect_sysconfig(info_add): 'Py_REMOTE_DEBUG', 'SHELL', 'SOABI', + 'SOABI_PLATFORM', 'TEST_MODULES', 'VAPTH', 'abs_builddir', @@ -1315,6 +1317,12 @@ def collect_system(info_add): info_add('system.hardware', hardware) +def collect_importlib(info_add): + import importlib.machinery + info_add('importlib.extension_suffixes', + importlib.machinery.EXTENSION_SUFFIXES) + + def collect_info(info): error = False info_add = info.add @@ -1357,6 +1365,7 @@ def collect_info(info): collect_zstd, collect_libregrtest_utils, collect_system, + collect_importlib, # Collecting from tests should be last as they have side effects. collect_test_socket, diff --git a/Lib/test/test_importlib/extension/test_finder.py b/Lib/test/test_importlib/extension/test_finder.py index dc77fa78a203fdb..e33c0465654607f 100644 --- a/Lib/test/test_importlib/extension/test_finder.py +++ b/Lib/test/test_importlib/extension/test_finder.py @@ -5,6 +5,7 @@ import unittest import sys +import sysconfig class FinderTests(abc.FinderTests): @@ -61,6 +62,7 @@ def test_failure(self): def test_abi3_extension_suffixes(self): suffixes = self.machinery.EXTENSION_SUFFIXES + platform = sysconfig.get_config_var("SOABI_PLATFORM") if 'win32' in sys.platform: # Either "_d.pyd" or ".pyd" must be in suffixes self.assertTrue({"_d.pyd", ".pyd"}.intersection(suffixes)) @@ -73,6 +75,13 @@ def test_abi3_extension_suffixes(self): self.assertIn(".abi3.so", suffixes) self.assertIn(".abi3t.so", suffixes) + if platform: + if Py_GIL_DISABLED: + self.assertNotIn(f".abi3-{platform}.so", suffixes) + else: + self.assertIn(f".abi3-{platform}.so", suffixes) + self.assertIn(f".abi3t-{platform}.so", suffixes) + (Frozen_FinderTests, Source_FinderTests diff --git a/Lib/test/test_sysconfig.py b/Lib/test/test_sysconfig.py index e6f99581f0b7a66..9bb3e326ff3e952 100644 --- a/Lib/test/test_sysconfig.py +++ b/Lib/test/test_sysconfig.py @@ -457,6 +457,15 @@ def test_soabi(self): soabi = sysconfig.get_config_var('SOABI') self.assertIn(soabi, _imp.extension_suffixes()[0]) + @unittest.skipIf(not _imp.extension_suffixes(), "stub loader has no suffixes") + @unittest.skipIf(sys.platform == "win32", "Does not apply to Windows") + @unittest.skipIf(sysconfig.get_config_var('SOABI_PLATFORM') == 0, + "SOABI_PLATFORM is undefined") + def test_soabi_platform(self): + soabi_platform = sysconfig.get_config_var('SOABI_PLATFORM') + soabi = sysconfig.get_config_var('SOABI') + self.assertIn(soabi_platform, soabi) + def test_library(self): library = sysconfig.get_config_var('LIBRARY') ldlibrary = sysconfig.get_config_var('LDLIBRARY') diff --git a/Misc/NEWS.d/next/C_API/2024-08-12-09-48-04.gh-issue-122931.x435Mb.rst b/Misc/NEWS.d/next/C_API/2024-08-12-09-48-04.gh-issue-122931.x435Mb.rst new file mode 100644 index 000000000000000..ff972aaefc74268 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2024-08-12-09-48-04.gh-issue-122931.x435Mb.rst @@ -0,0 +1 @@ +Allow importing stable ABI C extensions that include a multiarch tuple in their filename, e.g. ``foo.abi3-x86-64-linux-gnu.so``. diff --git a/Python/dynload_shlib.c b/Python/dynload_shlib.c index 0ff88ad330fd09c..f7f7c9229200857 100644 --- a/Python/dynload_shlib.c +++ b/Python/dynload_shlib.c @@ -46,8 +46,14 @@ const char *_PyImport_DynLoadFiletab[] = { "." ALT_SOABI ".so", #endif #ifndef Py_GIL_DISABLED +#ifdef SOABI_PLATFORM + ".abi" PYTHON_ABI_STRING "-" SOABI_PLATFORM ".so", +#endif /* SOABI_PLATFORM */ ".abi" PYTHON_ABI_STRING ".so", #endif /* Py_GIL_DISABLED */ +#ifdef SOABI_PLATFORM + ".abi" PYTHON_ABI_STRING "t-" SOABI_PLATFORM ".so", +#endif /* SOABI_PLATFORM */ ".abi" PYTHON_ABI_STRING "t.so", ".so", #endif /* __CYGWIN__ */ diff --git a/configure b/configure index dc23b9f7c20da2f..69d76f7b9f2dba2 100755 --- a/configure +++ b/configure @@ -7395,6 +7395,12 @@ case $ac_sys_system in #( ;; esac +if test x$SOABI_PLATFORM != x; then + +printf "%s\n" "#define SOABI_PLATFORM \"${SOABI_PLATFORM}\"" >>confdefs.h + +fi + if test x$MULTIARCH != x; then MULTIARCH_CPPFLAGS="-DMULTIARCH=\\\"$MULTIARCH\\\"" fi diff --git a/configure.ac b/configure.ac index 5a59e70d64bab31..93b211eef749880 100644 --- a/configure.ac +++ b/configure.ac @@ -1221,6 +1221,10 @@ AS_CASE([$ac_sys_system], [SOABI_PLATFORM=$PLATFORM_TRIPLET] ) +if test x$SOABI_PLATFORM != x; then + AC_DEFINE_UNQUOTED([SOABI_PLATFORM], ["${SOABI_PLATFORM}"], [Platform tag, used in binary module extension filenames.]) +fi + if test x$MULTIARCH != x; then MULTIARCH_CPPFLAGS="-DMULTIARCH=\\\"$MULTIARCH\\\"" fi diff --git a/pyconfig.h.in b/pyconfig.h.in index 25c4842d25e8a67..2658fe8116781db 100644 --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -1968,6 +1968,9 @@ /* The size of '_Bool', as computed by sizeof. */ #undef SIZEOF__BOOL +/* Platform tag, used in binary module extension filenames. */ +#undef SOABI_PLATFORM + /* Define to 1 if you have the ANSI C header files. */ #undef STDC_HEADERS From c7b9a13a7528342fdac47a2121248f0b989131e5 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Mon, 27 Jul 2026 16:00:20 +0200 Subject: [PATCH 3/4] gh-154431: Revert "gh-154431: Fix data race in `sys.audithook` (#154462)" (GH-154769) This commit caused a refleak. This reverts commit 596cd5c5d7b6ada3e50c6499447eb90a065769ad. --- Include/internal/pycore_interp_structs.h | 1 - Lib/test/test_free_threading/test_sys.py | 14 ---------- ...-07-22-12-42-53.gh-issue-154431.U2kXXZ.rst | 1 - Python/pystate.c | 1 - Python/sysmodule.c | 26 ++++++------------- 5 files changed, 8 insertions(+), 35 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2026-07-22-12-42-53.gh-issue-154431.U2kXXZ.rst diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index 3d577ff717cc6e1..0623adce693d465 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -977,7 +977,6 @@ struct _is { struct _obmalloc_state *obmalloc; PyObject *audit_hooks; - PyMutex audit_hooks_mutex; PyType_WatchCallback type_watchers[TYPE_MAX_WATCHERS]; PyCode_WatchCallback code_watchers[CODE_MAX_WATCHERS]; PyContext_WatchCallback context_watchers[CONTEXT_MAX_WATCHERS]; diff --git a/Lib/test/test_free_threading/test_sys.py b/Lib/test/test_free_threading/test_sys.py index b8ba933cb01adc0..271fdd13c62b668 100644 --- a/Lib/test/test_free_threading/test_sys.py +++ b/Lib/test/test_free_threading/test_sys.py @@ -44,20 +44,6 @@ def worker(worker_id): workers = [lambda: worker(i) for i in range(5)] threading_helper.run_concurrently(workers) - def test_sys_audit_hooks(self): - def _hook(*args): - return None - - def adder(): - for _ in range(100): - sys.addaudithook(_hook) - - def auditor(): - for _ in range(2000): - sys.audit("fusil.tsan.test") - - threading_helper.run_concurrently([adder, auditor]) - if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-07-22-12-42-53.gh-issue-154431.U2kXXZ.rst b/Misc/NEWS.d/next/Library/2026-07-22-12-42-53.gh-issue-154431.U2kXXZ.rst deleted file mode 100644 index a56c3b3a6f21f3a..000000000000000 --- a/Misc/NEWS.d/next/Library/2026-07-22-12-42-53.gh-issue-154431.U2kXXZ.rst +++ /dev/null @@ -1 +0,0 @@ -Fixes a data race in free-threading build in :func:`sys.addaudithook`. diff --git a/Python/pystate.c b/Python/pystate.c index b6c34552e94337b..d10b38def32911d 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -578,7 +578,6 @@ init_interpreter(PyInterpreterState *interp, llist_init(&interp->mem_free_queue.head); llist_init(&interp->asyncio_tasks_head); interp->asyncio_tasks_lock = (PyMutex){0}; - interp->audit_hooks_mutex = (PyMutex){0}; for (int i = 0; i < _PY_MONITORING_UNGROUPED_EVENTS; i++) { interp->monitors.tools[i] = 0; } diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 207ccbce41d71db..9442472b53abbe1 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -236,7 +236,7 @@ should_audit(PyInterpreterState *interp) return 0; } return (interp->runtime->audit_hooks.head - || FT_ATOMIC_LOAD_PTR_ACQUIRE(interp->audit_hooks) + || interp->audit_hooks || PyDTrace_AUDIT_ENABLED()); } @@ -306,14 +306,13 @@ sys_audit_tstate(PyThreadState *ts, const char *event, } /* Call interpreter hooks */ - PyObject *audit_hooks = FT_ATOMIC_LOAD_PTR_ACQUIRE(is->audit_hooks); - if (audit_hooks) { + if (is->audit_hooks) { eventName = PyUnicode_FromString(event); if (!eventName) { goto exit; } - hooks = PyObject_GetIter(audit_hooks); + hooks = PyObject_GetIter(is->audit_hooks); if (!hooks) { goto exit; } @@ -537,29 +536,20 @@ sys_addaudithook_impl(PyObject *module, PyObject *hook) } PyInterpreterState *interp = tstate->interp; - PyMutex mutex = interp->audit_hooks_mutex; - PyMutex_Lock(&mutex); - if (interp->audit_hooks == NULL) { - PyObject *new_list = PyList_New(0); - if (new_list == NULL) { - goto error; + interp->audit_hooks = PyList_New(0); + if (interp->audit_hooks == NULL) { + return NULL; } /* Avoid having our list of hooks show up in the GC module */ - PyObject_GC_UnTrack(new_list); - FT_ATOMIC_STORE_PTR_RELEASE(interp->audit_hooks, new_list); + PyObject_GC_UnTrack(interp->audit_hooks); } if (PyList_Append(interp->audit_hooks, hook) < 0) { - goto error; + return NULL; } - PyMutex_Unlock(&mutex); Py_RETURN_NONE; - -error: - PyMutex_Unlock(&mutex); - return NULL; } /*[clinic input] From b8862aeac3d7cc4ed661c232130cd51c8f691a95 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 27 Jul 2026 18:03:17 +0300 Subject: [PATCH 4/4] gh-154744: Improve detection of skipinitialspace in csv.Sniffer (GH-154745) Detect the padding by parsing the sample both with and without skipinitialspace and comparing the two readings, instead of testing whether every field following a delimiter starts with a space. --- Lib/csv.py | 48 +++++++++++------ Lib/test/test_csv.py | 54 +++++++++++++++++++ ...-07-26-13-40-00.gh-issue-154744.Sk1Psp.rst | 1 + 3 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-26-13-40-00.gh-issue-154744.Sk1Psp.rst diff --git a/Lib/csv.py b/Lib/csv.py index a84234f110dd70c..c66717dc1ee59e7 100644 --- a/Lib/csv.py +++ b/Lib/csv.py @@ -563,22 +563,40 @@ def _detect_doublequote(self, lines, delimiter, quotechar, escapechar): def _detect_skipinitialspace(self, lines, delimiter, quotechar, escapechar, doublequote): """ - True only if every field following a delimiter starts with - a space. + Detect whether the spaces following a delimiter are a part of + the format or of the data. """ - skipinitialspace = False - try: - for row in self._make_reader(lines, delimiter, quotechar, - escapechar, - doublequote=doublequote, - skipinitialspace=False): - for field in row[1:]: - if not field.startswith(' '): - return False - skipinitialspace = True - except Error: - pass - return skipinitialspace + results = [] + for skipinitialspace in False, True: + rows = [] + try: + rows.extend(self._make_reader( + lines, delimiter, quotechar, escapechar, + doublequote=doublequote, + skipinitialspace=skipinitialspace)) + except Error: + # Keep the rows parsed before the error. + pass + results.append([row for row in rows if row]) + if results[0] == results[1]: + return False # No evidence. + counts = [[len(row) for row in rows] for rows in results] + if counts[0] != counts[1]: + # Prefer the more consistent row widths. + return len(set(counts[1])) <= len(set(counts[0])) + # Only some spaces are stripped. A field differs only if + # a space was skipped at its start, which tells the padding + # apart from the spaces inside quoted or escaped fields. + if not all(kept_field != skipped_field + for kept_row, skipped_row in zip(*results) + for kept_field, skipped_field in zip(kept_row[1:], + skipped_row[1:])): + return False + # The first field of a row is commonly not padded ('a, b, c'), + # so the first fields need only agree with each other. + first = [kept_row[0] != skipped_row[0] + for kept_row, skipped_row in zip(*results)] + return all(first) or not any(first) def has_header(self, sample): # Creates a dictionary of types of data in each column. If any diff --git a/Lib/test/test_csv.py b/Lib/test/test_csv.py index ded53e936d19632..91170cc16b3ac95 100644 --- a/Lib/test/test_csv.py +++ b/Lib/test/test_csv.py @@ -1688,6 +1688,60 @@ def test_sniff_skipinitialspace_quoted(self): self.assertEqual(dialect.quotechar, "'") self.assertIs(dialect.skipinitialspace, True) + def test_sniff_skipinitialspace_quoted_fields(self): + # A quote is only a quote at the very start of a field, so not + # skipping the space splits the quoted field. + sniffer = csv.Sniffer() + sample = 'a, "b,c"\nd,e\nf,g\n' + dialect = sniffer.sniff(sample) + self.assertEqual(dialect.delimiter, ',') + self.assertEqual(dialect.quotechar, '"') + self.assertIs(dialect.skipinitialspace, True) + self.assertEqual(next(csv.reader(StringIO(sample), dialect)), + ['a', 'b,c']) + + # But without a delimiter inside the quotes nothing is split, + # so only the padding counts. + sample = 'a, "b"\nd,e\nf,g\n' + dialect = sniffer.sniff(sample) + self.assertEqual(dialect.delimiter, ',') + self.assertEqual(dialect.quotechar, '"') + self.assertIs(dialect.skipinitialspace, False) + self.assertEqual(next(csv.reader(StringIO(sample), dialect)), + ['a', ' "b"']) + + def test_sniff_skipinitialspace_data(self): + # The spaces are a part of the data if only some fields + # are padded. + sniffer = csv.Sniffer() + sample = 'a, b\nc,d\ne, f\n' + dialect = sniffer.sniff(sample) + self.assertEqual(dialect.delimiter, ',') + self.assertIs(dialect.skipinitialspace, False) + self.assertEqual(next(csv.reader(StringIO(sample), dialect)), + ['a', ' b']) + + def test_sniff_skipinitialspace_not_skipped(self): + # A quoted or escaped space is not skipped, so it is not + # an evidence of the padding. + sniffer = csv.Sniffer() + sample = 'a," b"\nc, d\n' + dialect = sniffer.sniff(sample) + self.assertEqual(dialect.delimiter, ',') + self.assertEqual(dialect.quotechar, '"') + self.assertIs(dialect.skipinitialspace, False) + self.assertEqual(list(csv.reader(StringIO(sample), dialect)), + [['a', ' b'], ['c', ' d']]) + + # The escaped delimiter forces the escapechar detection. + sample = 'a,\\ b\\,c\nd, e\n' + dialect = sniffer.sniff(sample) + self.assertEqual(dialect.delimiter, ',') + self.assertEqual(dialect.escapechar, '\\') + self.assertIs(dialect.skipinitialspace, False) + self.assertEqual(list(csv.reader(StringIO(sample), dialect)), + [['a', ' b,c'], ['d', ' e']]) + def test_sniff_regex_backtracking(self): # gh-109638: this artificial sample used to take minutes. sniffer = csv.Sniffer() diff --git a/Misc/NEWS.d/next/Library/2026-07-26-13-40-00.gh-issue-154744.Sk1Psp.rst b/Misc/NEWS.d/next/Library/2026-07-26-13-40-00.gh-issue-154744.Sk1Psp.rst new file mode 100644 index 000000000000000..500826c195346f7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-26-13-40-00.gh-issue-154744.Sk1Psp.rst @@ -0,0 +1 @@ +Improve detection of ``skipinitialspace`` in :meth:`csv.Sniffer.sniff`.