Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
=========================
Expand Down
1 change: 0 additions & 1 deletion Include/internal/pycore_interp_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
48 changes: 33 additions & 15 deletions Lib/csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions Lib/test/pythoninfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,7 @@ def collect_sysconfig(info_add):

for name in (
'ABIFLAGS',
'ALT_SOABI',
'ANDROID_API_LEVEL',
'CC',
'CCSHARED',
Expand All @@ -595,6 +596,7 @@ def collect_sysconfig(info_add):
'Py_REMOTE_DEBUG',
'SHELL',
'SOABI',
'SOABI_PLATFORM',
'TEST_MODULES',
'VAPTH',
'abs_builddir',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
54 changes: 54 additions & 0 deletions Lib/test/test_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
14 changes: 0 additions & 14 deletions Lib/test/test_free_threading/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
9 changes: 9 additions & 0 deletions Lib/test/test_importlib/extension/test_finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import unittest
import sys
import sysconfig


class FinderTests(abc.FinderTests):
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions Lib/test/test_sysconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
@@ -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``.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improve detection of ``skipinitialspace`` in :meth:`csv.Sniffer.sniff`.
6 changes: 6 additions & 0 deletions Python/dynload_shlib.c
Original file line number Diff line number Diff line change
Expand Up @@ -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__ */
Expand Down
1 change: 0 additions & 1 deletion Python/pystate.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
26 changes: 8 additions & 18 deletions Python/sysmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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]
Expand Down
6 changes: 3 additions & 3 deletions Tools/requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions configure

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions pyconfig.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading