Skip to content

Commit 204795f

Browse files
generatedunixname1734921407115435meta-codesync[bot]
authored andcommitted
Sync pre-release CPython 3.15 branch from GitHub (2026-05-19)
Summary: Imported python/cpython `3.15.0b1+` from upstream rev [`d36e080`](https://www.github.com/python/cpython/commit/d36e08099d56a54028174429a946c9816f284374) (committed 2026-05-19 04:42:25+00:00). # Commit Info Base: (`3.15.0b1+`) - [`42ff9b4`](https://www.github.com/python/cpython/commit/42ff9b4959667cf31bde13a53fca01b1ec381168) (commit date: 2026-05-17 11:01:15+00:00) Imported: (`3.15.0b1+`) - [`d36e080`](https://www.github.com/python/cpython/commit/d36e08099d56a54028174429a946c9816f284374) (commit date: 2026-05-19 04:42:25+00:00) # Noteworthy file changes - Test files (1 added) - Low-signal files (4 added) (NEWS.d, docs, .github) Complete list of added/removed files: https://www.internalfb.com/intern/everpaste/?color=0&handle=GFKP2SJMYBn_qPUFAJ4XJOBDCEgAbr0LAAAz Differential Revision: D105663012 fbshipit-source-id: 9912e924ef712cba761c927db3ad6879c15e3d1b
1 parent 1051697 commit 204795f

18 files changed

Lines changed: 193 additions & 90 deletions

File tree

Doc/library/ssl.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2076,7 +2076,7 @@ to speed up repeated connections from the same clients.
20762076
:attr:`~SSLContext.minimum_version` and
20772077
:attr:`SSLContext.options` all affect the supported SSL
20782078
and TLS versions of the context. The implementation does not prevent
2079-
invalid combination. For example a context with
2079+
invalid combinations. For example a context with
20802080
:attr:`OP_NO_TLSv1_2` in :attr:`~SSLContext.options` and
20812081
:attr:`~SSLContext.maximum_version` set to :attr:`TLSVersion.TLSv1_2`
20822082
will not be able to establish a TLS 1.2 connection.
@@ -2891,11 +2891,11 @@ disabled by default.
28912891
::
28922892

28932893
>>> client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
2894-
>>> client_context.minimum_version = ssl.TLSVersion.TLSv1_3
2894+
>>> client_context.minimum_version = ssl.TLSVersion.TLSv1_2
28952895
>>> client_context.maximum_version = ssl.TLSVersion.TLSv1_3
28962896

28972897

2898-
The SSL context created above will only allow TLSv1.3 and later (if
2898+
The SSL client context created above will only allow TLSv1.2 and TLSv1.3 (if
28992899
supported by your system) connections to a server. :const:`PROTOCOL_TLS_CLIENT`
29002900
implies certificate validation and hostname checks by default. You have to
29012901
load certificates into the context.

Include/internal/pycore_interp_structs.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,15 @@ struct _import_state {
349349
int lazy_imports_mode;
350350
PyObject *lazy_imports_filter;
351351
PyObject *lazy_importing_modules;
352+
// The set stored in sys.lazy_modules if values that have been
353+
// lazily imported. This value is only for debugging/introspection
354+
// purposes and is not used by the runtime.
352355
PyObject *lazy_modules;
356+
// A dict mapping package names to a set of submodule names that
357+
// have been imported lazily from packages which have been imported
358+
// lazily. When the package is reified we need to add a
359+
// LazyImportObject which refers to the submodule on the module.
360+
PyObject *lazy_pending_submodules;
353361
#ifdef Py_GIL_DISABLED
354362
PyMutex lazy_mutex;
355363
#endif

Lib/test/test_free_threading/test_dict.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,34 @@ def watcher():
268268
finally:
269269
_testcapi.clear_dict_watcher(wid)
270270

271+
def test_racing_split_dict_clear_and_lookup(self):
272+
class C:
273+
pass
274+
275+
keys = [f"a{i}" for i in range(16)]
276+
277+
def make_split_nonembedded():
278+
inst = C()
279+
for key in keys:
280+
setattr(inst, key, keys.index(key))
281+
# dict.copy() of a split instance dict yields a split table
282+
# with non-embedded values
283+
return inst.__dict__.copy()
284+
285+
d = make_split_nonembedded()
286+
287+
def clearer():
288+
for _ in range(1000):
289+
d.clear()
290+
d.update(make_split_nonembedded())
291+
292+
def reader():
293+
for _ in range(1000):
294+
for k in keys:
295+
d.get(k)
296+
297+
threading_helper.run_concurrently([clearer, reader, reader])
298+
271299
def test_racing_dict_update_and_method_lookup(self):
272300
# gh-144295: test race between dict modifications and method lookups.
273301
# Uses BytesIO because the race requires a type without Py_TPFLAGS_INLINE_VALUES

Lib/test/test_lazy_import/__init__.py

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import unittest
1111
import tempfile
1212
import os
13+
import contextlib
1314

1415
from test import support
1516
from test.support.script_helper import assert_python_ok
@@ -37,8 +38,7 @@ def test_basic_unused(self):
3738
"""Lazy imported module should not be loaded if never accessed."""
3839
import test.test_lazy_import.data.basic_unused
3940
self.assertNotIn("test.test_lazy_import.data.basic2", sys.modules)
40-
self.assertIn("test.test_lazy_import.data", sys.lazy_modules)
41-
self.assertEqual(sys.lazy_modules["test.test_lazy_import.data"], {"basic2"})
41+
self.assertIn("test.test_lazy_import.data.basic2", sys.lazy_modules)
4242

4343
def test_sys_lazy_modules(self):
4444
try:
@@ -48,7 +48,7 @@ def test_sys_lazy_modules(self):
4848

4949
self.assertFalse("test.test_lazy_import.data.basic2" in sys.modules)
5050
self.assertIn("test.test_lazy_import.data", sys.lazy_modules)
51-
self.assertEqual(sys.lazy_modules["test.test_lazy_import.data"], {"basic2"})
51+
self.assertIn("test.test_lazy_import.data.basic2", sys.lazy_modules)
5252
test.test_lazy_import.data.basic_from_unused.basic2
5353
self.assertNotIn("test.test_import.data", sys.lazy_modules)
5454

@@ -441,10 +441,14 @@ def tearDown(self):
441441

442442
def test_lazy_import_pkg(self):
443443
"""lazy import of package submodule should load the package."""
444-
import test.test_lazy_import.data.lazy_import_pkg
444+
out = io.StringIO()
445+
446+
with contextlib.redirect_stdout(out):
447+
import test.test_lazy_import.data.lazy_import_pkg
445448

446449
self.assertIn("test.test_lazy_import.data.pkg", sys.modules)
447450
self.assertIn("test.test_lazy_import.data.pkg.bar", sys.modules)
451+
self.assertIn("BAR_MODULE_LOADED", out.getvalue())
448452

449453
def test_lazy_import_pkg_cross_import(self):
450454
"""Cross-imports within package should preserve lazy imports."""
@@ -569,8 +573,8 @@ def my_filter(name):
569573
self.assertIs(sys.get_lazy_imports_filter(), my_filter)
570574

571575
def test_lazy_modules_attribute_is_dict(self):
572-
"""sys.lazy_modules should be a dict per PEP 810."""
573-
self.assertIsInstance(sys.lazy_modules, dict)
576+
"""sys.lazy_modules should be a set per PEP 810."""
577+
self.assertIsInstance(sys.lazy_modules, set)
574578

575579
@support.requires_subprocess()
576580
def test_lazy_modules_tracks_lazy_imports(self):
@@ -579,8 +583,7 @@ def test_lazy_modules_tracks_lazy_imports(self):
579583
import sys
580584
initial_count = len(sys.lazy_modules)
581585
import test.test_lazy_import.data.basic_unused
582-
assert "test.test_lazy_import.data" in sys.lazy_modules
583-
assert sys.lazy_modules["test.test_lazy_import.data"] == {"basic2"}
586+
assert "test.test_lazy_import.data.basic2" in sys.lazy_modules
584587
assert len(sys.lazy_modules) > initial_count
585588
print("OK")
586589
""")
@@ -1029,15 +1032,14 @@ def test_module_added_to_lazy_modules_on_lazy_import(self):
10291032
lazy import test.test_lazy_import.data.basic2
10301033
10311034
# Should be in lazy_modules after lazy import
1032-
assert "test.test_lazy_import.data" in sys.lazy_modules
1033-
assert sys.lazy_modules["test.test_lazy_import.data"] == {"basic2"}
1035+
assert "test.test_lazy_import.data.basic2" in sys.lazy_modules
10341036
assert len(sys.lazy_modules) > initial_count
10351037
10361038
# Trigger reification
10371039
_ = test.test_lazy_import.data.basic2.x
10381040
10391041
# Module should still be tracked (for diagnostics per PEP 810)
1040-
assert "test.test_lazy_import.data" not in sys.lazy_modules
1042+
assert "test.test_lazy_import.data.basic2" not in sys.lazy_modules
10411043
print("OK")
10421044
""")
10431045
result = subprocess.run(
@@ -1050,8 +1052,8 @@ def test_module_added_to_lazy_modules_on_lazy_import(self):
10501052

10511053
def test_lazy_modules_is_per_interpreter(self):
10521054
"""Each interpreter should have independent sys.lazy_modules."""
1053-
# Basic test that sys.lazy_modules exists and is a dict
1054-
self.assertIsInstance(sys.lazy_modules, dict)
1055+
# Basic test that sys.lazy_modules exists and is a set
1056+
self.assertIsInstance(sys.lazy_modules, set)
10551057

10561058
def test_lazy_module_without_children_is_tracked(self):
10571059
code = textwrap.dedent("""
@@ -1060,10 +1062,6 @@ def test_lazy_module_without_children_is_tracked(self):
10601062
assert "json" in sys.lazy_modules, (
10611063
f"expected 'json' in sys.lazy_modules, got {set(sys.lazy_modules)}"
10621064
)
1063-
assert sys.lazy_modules["json"] == set(), (
1064-
f"expected empty set for sys.lazy_modules['json'], "
1065-
f"got {sys.lazy_modules['json']!r}"
1066-
)
10671065
print("OK")
10681066
""")
10691067
assert_python_ok("-c", code)
@@ -1932,7 +1930,7 @@ def create_lazy_imports(idx):
19321930
t.join()
19331931
19341932
assert not errors, f"Errors: {errors}"
1935-
assert isinstance(sys.lazy_modules, dict), "sys.lazy_modules is not a dict"
1933+
assert isinstance(sys.lazy_modules, set), "sys.lazy_modules is not a dict"
19361934
print("OK")
19371935
""")
19381936

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import unittest
2+
3+
unittest.main('test.test_lazy_import')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix crash when faulthandler is imported more than once.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
``sys.lazy_modules`` is now a set instead of a dict as initially spelled out in PEP 810.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a race condition in ``_random.Random.__init__`` method in free-threading
2+
mode.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix race condition when pickling dictionaries in free threaded builds. Also
2+
reduce critical section cover.

Modules/_pickle.c

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3450,6 +3450,9 @@ batch_dict(PickleState *state, PicklerObject *self, PyObject *iter, PyObject *or
34503450
* Returns 0 on success, -1 on error.
34513451
*
34523452
* Note that this currently doesn't work for protocol 0.
3453+
3454+
* gh-146452: Wrap the dict iteration in a critical sections to prevent
3455+
* concurrent mutation from invalidating PyDict_Next() iteration state.
34533456
*/
34543457
static int
34553458
batch_dict_exact(PickleState *state, PicklerObject *self, PyObject *obj)
@@ -3466,15 +3469,24 @@ batch_dict_exact(PickleState *state, PicklerObject *self, PyObject *obj)
34663469
assert(self->proto > 0);
34673470

34683471
dict_size = PyDict_GET_SIZE(obj);
3469-
assert(dict_size);
34703472

34713473
/* Write in batches of BATCHSIZE. */
34723474
Py_ssize_t total = 0;
34733475
do {
34743476
if (dict_size - total == 1) {
3475-
PyDict_Next(obj, &ppos, &key, &value);
3476-
Py_INCREF(key);
3477-
Py_INCREF(value);
3477+
int next;
3478+
Py_BEGIN_CRITICAL_SECTION(obj);
3479+
next = PyDict_Next(obj, &ppos, &key, &value);
3480+
if (next) {
3481+
Py_INCREF(key);
3482+
Py_INCREF(value);
3483+
}
3484+
Py_END_CRITICAL_SECTION();
3485+
if (!next) {
3486+
PyErr_SetString(PyExc_RuntimeError,
3487+
"dictionary changed size during iteration");
3488+
goto error;
3489+
}
34783490
if (save(state, self, key, 0) < 0) {
34793491
goto error;
34803492
}
@@ -3492,9 +3504,18 @@ batch_dict_exact(PickleState *state, PicklerObject *self, PyObject *obj)
34923504
i = 0;
34933505
if (_Pickler_Write(self, &mark_op, 1) < 0)
34943506
return -1;
3495-
while (PyDict_Next(obj, &ppos, &key, &value)) {
3496-
Py_INCREF(key);
3497-
Py_INCREF(value);
3507+
int next;
3508+
while (1) {
3509+
Py_BEGIN_CRITICAL_SECTION(obj);
3510+
next = PyDict_Next(obj, &ppos, &key, &value);
3511+
if (next) {
3512+
Py_INCREF(key);
3513+
Py_INCREF(value);
3514+
}
3515+
Py_END_CRITICAL_SECTION();
3516+
if (!next) {
3517+
break;
3518+
}
34983519
if (save(state, self, key, 0) < 0) {
34993520
goto error;
35003521
}

0 commit comments

Comments
 (0)