Skip to content

Commit 418ad55

Browse files
authored
Merge branch '3.15' into backport-8b31d08-3.15
2 parents 202971f + de401ef commit 418ad55

9 files changed

Lines changed: 158 additions & 179 deletions

File tree

.github/workflows/reusable-windows.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ permissions:
2222

2323
env:
2424
FORCE_COLOR: 1
25-
IncludeUwp: >-
26-
true
2725

2826
jobs:
2927
build:

Include/internal/pycore_import.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ extern PyObject * _PyImport_GetAbsName(
3939
// Symbol is exported for the JIT on Windows builds.
4040
PyAPI_FUNC(PyObject *) _PyImport_LoadLazyImportTstate(
4141
PyThreadState *tstate, PyObject *lazy_import);
42+
extern PyObject * _PyImport_TryLoadLazySubmodule(
43+
PyObject *mod_name, PyObject *attr_name);
4244
extern PyObject * _PyImport_LazyImportModuleLevelObject(
4345
PyThreadState *tstate, PyObject *name, PyObject *builtins,
4446
PyObject *globals, PyObject *locals, PyObject *fromlist, int level);

Lib/gzip.py

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -484,14 +484,22 @@ def _read_exact(fp, n):
484484
return data
485485

486486

487-
def _read_until_null(fp, append_to):
487+
def _read_until_null(fp, crc=None):
488488
'''Read until the first encountered null byte in fp.
489-
Append to given byte array object'''
490-
while True:
491-
s = fp.read(1)
492-
append_to += s
493-
if not s or s == b'\000':
494-
break
489+
If crc is not None, update and return the CRC.
490+
'''
491+
if crc is None:
492+
while True:
493+
s = fp.read(1)
494+
if not s or s == b'\000':
495+
break
496+
else:
497+
while True:
498+
s = fp.read(1)
499+
crc = zlib.crc32(s, crc)
500+
if not s or s == b'\000':
501+
break
502+
return crc
495503

496504

497505
def _read_gzip_header(fp):
@@ -517,30 +525,32 @@ def _read_gzip_header(fp):
517525
return last_mtime
518526
if flag == FNAME:
519527
# Read and discard a null-terminated string containing the filename
520-
while True:
521-
s = fp.read(1)
522-
if not s or s==b'\000':
523-
break
528+
_read_until_null(fp)
524529
return last_mtime
525530

526531
# Processing for more complex flags. Save header parts for FHCRC checking.
527-
header = bytearray(magic + base_header)
532+
if flag & FHCRC:
533+
crc = zlib.crc32(magic + base_header)
534+
else:
535+
crc = None
528536
if flag & FEXTRA:
529537
extra_len_bytes = _read_exact(fp, 2)
530538
extra_len, = struct.unpack("<H", extra_len_bytes)
531-
header += extra_len_bytes
532-
header += _read_exact(fp, extra_len)
539+
extra = _read_exact(fp, extra_len)
540+
if crc is not None:
541+
crc = zlib.crc32(extra_len_bytes, crc)
542+
crc = zlib.crc32(extra, crc)
533543
if flag & FNAME:
534-
_read_until_null(fp, append_to=header)
544+
crc = _read_until_null(fp, crc)
535545
if flag & FCOMMENT:
536-
_read_until_null(fp, append_to=header)
537-
if flag & FHCRC:
546+
crc = _read_until_null(fp, crc)
547+
if crc is not None:
538548
# Header CRC is the last 16 bits of a crc32.
539549
header_crc, = struct.unpack("<H", _read_exact(fp, 2))
540-
true_crc = zlib.crc32(header) & 0xFFFF
541-
if header_crc != true_crc:
550+
crc = crc & 0xFFFF
551+
if header_crc != crc:
542552
raise BadGzipFile(f"Corrupted gzip header. Checksums do not "
543-
f"match: {true_crc:04x} != {header_crc:04x}")
553+
f"match: {crc:04x} != {header_crc:04x}")
544554
return last_mtime
545555

546556

Lib/test/test_free_threading/test_iteration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
NUMITEMS = 1000
1313
NUMTHREADS = 2
1414
else:
15-
NUMITEMS = 100000
15+
NUMITEMS = 5000
1616
NUMTHREADS = 5
1717
NUMMUTATORS = 2
1818

Lib/test/test_lazy_import/__init__.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,14 @@ def test_lazy_import_pkg(self):
450450
self.assertIn("test.test_lazy_import.data.pkg.bar", sys.modules)
451451
self.assertIn("BAR_MODULE_LOADED", out.getvalue())
452452

453+
def test_lazy_submodule_stored_in_parent_dict(self):
454+
"""Accessing a lazy submodule should store it in the parent's __dict__."""
455+
import test.test_lazy_import.data.lazy_import_pkg
456+
457+
pkg = sys.modules["test.test_lazy_import.data.pkg"]
458+
self.assertIn("bar", pkg.__dict__)
459+
self.assertIs(pkg.__dict__["bar"], sys.modules["test.test_lazy_import.data.pkg.bar"])
460+
453461
def test_lazy_import_pkg_cross_import(self):
454462
"""Cross-imports within package should preserve lazy imports."""
455463
import test.test_lazy_import.data.pkg.c
@@ -462,6 +470,18 @@ def test_lazy_import_pkg_cross_import(self):
462470
self.assertEqual(type(g["x"]), int)
463471
self.assertEqual(type(g["b"]), types.LazyImportType)
464472

473+
@support.requires_subprocess()
474+
def test_lazy_from_import_does_not_pollute_parent(self):
475+
"""Lazy from import should not add the name to the parent module's dict."""
476+
code = textwrap.dedent("""
477+
lazy from json import nonexistent_attr
478+
import json
479+
assert "nonexistent_attr" not in json.__dict__, (
480+
"lazy from import should not publish attributes on the parent module"
481+
)
482+
""")
483+
assert_python_ok("-c", code)
484+
465485
@support.requires_subprocess()
466486
def test_package_from_import_with_module_getattr(self):
467487
"""Lazy from import should respect a package's __getattr__."""
@@ -613,19 +633,14 @@ def tearDown(self):
613633
sys.set_lazy_imports("normal")
614634

615635
def test_import_error_shows_chained_traceback(self):
616-
"""ImportError during reification should chain to show both definition and access."""
617-
# Errors at reification must show where the lazy import was defined
618-
# AND where the access happened, per PEP 810 "Reification" section
636+
"""Accessing a nonexistent lazy submodule via parent attr raises AttributeError."""
619637
code = textwrap.dedent("""
620638
import sys
621639
lazy import test.test_lazy_import.data.nonexistent_module
622640
623641
try:
624642
x = test.test_lazy_import.data.nonexistent_module
625-
except ImportError as e:
626-
# Should have __cause__ showing the original error
627-
# The exception chain shows both where import was defined and where access happened
628-
assert e.__cause__ is not None, "Expected chained exception"
643+
except AttributeError as e:
629644
print("OK")
630645
""")
631646
result = subprocess.run(
@@ -673,7 +688,7 @@ def test_reification_retries_on_failure(self):
673688
# First access - should fail
674689
try:
675690
x = test.test_lazy_import.data.broken_module
676-
except ValueError:
691+
except AttributeError:
677692
pass
678693
679694
# The lazy object should still be a lazy proxy (not reified)
@@ -683,7 +698,7 @@ def test_reification_retries_on_failure(self):
683698
# Second access - should also fail (retry the import)
684699
try:
685700
x = test.test_lazy_import.data.broken_module
686-
except ValueError:
701+
except AttributeError:
687702
print("OK - retry worked")
688703
""")
689704
result = subprocess.run(
@@ -696,20 +711,15 @@ def test_reification_retries_on_failure(self):
696711

697712
def test_error_during_module_execution_propagates(self):
698713
"""Errors in module code during reification should propagate correctly."""
699-
# Module that raises during import should propagate with chaining
700714
code = textwrap.dedent("""
701715
import sys
702716
lazy import test.test_lazy_import.data.broken_module
703717
704718
try:
705719
_ = test.test_lazy_import.data.broken_module
706720
print("FAIL - should have raised")
707-
except ValueError as e:
708-
# The ValueError from the module should be the cause
709-
if "always fails" in str(e) or (e.__cause__ and "always fails" in str(e.__cause__)):
710-
print("OK")
711-
else:
712-
print(f"FAIL - wrong error: {e}")
721+
except AttributeError:
722+
print("OK")
713723
""")
714724
result = subprocess.run(
715725
[sys.executable, "-c", code],

Lib/test/test_tcl.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def test_eval_null_in_result(self):
5555
def test_eval_surrogates_in_result(self):
5656
tcl = self.interp
5757
result = tcl.eval(r'set a "<\ud83d\udcbb>"')
58-
if sys.platform == 'win32':
58+
if sys.platform == 'win32' and tcl_version >= (9, 0):
5959
self.assertEqual('<\ud83d\udcbb>', result)
6060
else:
6161
self.assertEqual('<\U0001f4bb>', result)
@@ -294,7 +294,7 @@ def test_evalfile_surrogates_in_result(self):
294294
""")
295295
tcl.evalfile(filename)
296296
result = tcl.eval('set b')
297-
if sys.platform == 'win32':
297+
if sys.platform == 'win32' and tcl_version >= (9, 0):
298298
self.assertEqual('<\ud83d\udcbb>', result)
299299
else:
300300
self.assertEqual('<\U0001f4bb>', result)

Modules/_io/winconsoleio.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -673,12 +673,13 @@ read_console_w(HANDLE handle, DWORD maxlen, DWORD *readlen) {
673673
maxlen += 1;
674674
Py_BLOCK_THREADS
675675
newbuf = (wchar_t*)PyMem_Realloc(buf, maxlen * sizeof(wchar_t));
676-
Py_UNBLOCK_THREADS
677676
if (!newbuf) {
678677
sig = -1;
679678
PyErr_NoMemory();
679+
Py_UNBLOCK_THREADS
680680
break;
681681
}
682+
Py_UNBLOCK_THREADS
682683
buf = newbuf;
683684
/* Only advance by n and not BUFSIZ in this case */
684685
off += n;

Objects/moduleobject.c

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,6 +1299,33 @@ _PyModule_IsPossiblyShadowing(PyObject *origin)
12991299
return result;
13001300
}
13011301

1302+
// Check if `name` is a lazily pending submodule of module `m`.
1303+
// Returns a new reference on success, or NULL with no error set.
1304+
static PyObject *
1305+
try_load_lazy_submodule(PyModuleObject *m, PyObject *name)
1306+
{
1307+
PyObject *mod_name;
1308+
int rc = PyDict_GetItemRef(m->md_dict, &_Py_ID(__name__), &mod_name);
1309+
if (rc <= 0) {
1310+
return NULL;
1311+
}
1312+
if (!PyUnicode_Check(mod_name)) {
1313+
Py_DECREF(mod_name);
1314+
return NULL;
1315+
}
1316+
PyObject *result = _PyImport_TryLoadLazySubmodule(mod_name, name);
1317+
Py_DECREF(mod_name);
1318+
if (result == NULL) {
1319+
PyErr_Clear();
1320+
return NULL;
1321+
}
1322+
if (PyDict_SetItem(m->md_dict, name, result) < 0) {
1323+
Py_DECREF(result);
1324+
return NULL;
1325+
}
1326+
return result;
1327+
}
1328+
13021329
PyObject*
13031330
_Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
13041331
{
@@ -1363,6 +1390,13 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
13631390
PyErr_Clear();
13641391
}
13651392
assert(m->md_dict != NULL);
1393+
attr = try_load_lazy_submodule(m, name);
1394+
if (attr != NULL) {
1395+
return attr;
1396+
}
1397+
if (PyErr_Occurred()) {
1398+
return NULL;
1399+
}
13661400
if (PyDict_GetItemRef(m->md_dict, &_Py_ID(__getattr__), &getattr) < 0) {
13671401
return NULL;
13681402
}

0 commit comments

Comments
 (0)