Skip to content

Commit b991892

Browse files
committed
Merge branch 'main' into atan2
2 parents be0ed87 + 76f22f9 commit b991892

32 files changed

Lines changed: 445 additions & 205 deletions

‎.github/CODEOWNERS‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,9 +290,10 @@ Tools/jit/ @brandtbucher @savannahostrowski @diegorusso
290290
InternalDocs/jit.md @brandtbucher @savannahostrowski @diegorusso @AA-Turner
291291

292292
# Lazy imports (PEP 810)
293-
Objects/lazyimportobject.c @yhg1s @DinoV @pablogsal
294-
Include/internal/pycore_lazyimportobject.h @yhg1s @DinoV @pablogsal
295-
Lib/test/test_lazy_import @yhg1s @DinoV @pablogsal
293+
.github/workflows/reusable-test-lazy-imports-all.yml @yhg1s @DinoV @pablogsal
294+
Objects/lazyimportobject.c @yhg1s @DinoV @pablogsal
295+
Include/internal/pycore_lazyimportobject.h @yhg1s @DinoV @pablogsal
296+
Lib/test/test_lazy_import @yhg1s @DinoV @pablogsal
296297

297298
# Micro-op / μop / Tier 2 Optimiser
298299
Python/optimizer.c @markshannon @Fidget-Spinner
@@ -655,5 +656,8 @@ Objects/**/clinic/
655656
PC/**/clinic/
656657
Python/**/clinic/
657658

659+
# Exclude Lazy Imports=all CI carve out file
660+
Lib/test/lazy_imports_all_exclude.txt
661+
658662
# Exclude HTML IDs list
659663
Doc/tools/removed-ids.txt

‎.github/workflows/build.yml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,12 @@ jobs:
476476
name: hypothesis-example-db
477477
path: ${{ env.CPYTHON_BUILDDIR }}/.hypothesis/examples/
478478

479+
test-lazy-imports-all:
480+
name: 'Lazy imports enabled'
481+
needs: build-context
482+
if: fromJSON(needs.build-context.outputs.run-tests)
483+
uses: ./.github/workflows/reusable-test-lazy-imports-all.yml
484+
479485
build-asan:
480486
name: 'Address sanitizer'
481487
runs-on: ${{ matrix.os }}
@@ -648,6 +654,7 @@ jobs:
648654
- build-emscripten
649655
- build-wasi
650656
- test-hypothesis
657+
- test-lazy-imports-all
651658
- build-asan
652659
- build-san
653660
- cross-build-linux
@@ -705,4 +712,5 @@ jobs:
705712
${{ !fromJSON(needs.build-context.outputs.run-ios) && 'build-ios,' || '' }}
706713
${{ !fromJSON(needs.build-context.outputs.run-emscripten) && 'build-emscripten,' || '' }}
707714
${{ !fromJSON(needs.build-context.outputs.run-wasi) && 'build-wasi,' || '' }}
715+
${{ !fromJSON(needs.build-context.outputs.run-tests) && 'test-lazy-imports-all,' || '' }}
708716
jobs: ${{ toJSON(needs) }}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: Reusable Lazy Imports Tests
2+
3+
# Run the CPython test suite with global lazy imports forced on
4+
# (``-X lazy_imports=all``).
5+
#
6+
# Modules that are known to fail under lazy imports are listed in
7+
# Lib/test/lazy_imports_all_exclude.txt and skipped here. Remove entries from
8+
# that file as the modules are fixed so this workflow starts guarding them
9+
# against regressions. Excluded modules are also checked separately so the
10+
# workflow fails when one starts passing and its exclusion should be removed.
11+
12+
on:
13+
workflow_call:
14+
15+
permissions:
16+
contents: read
17+
18+
env:
19+
FORCE_COLOR: 1
20+
21+
jobs:
22+
test-lazy-imports-all:
23+
name: 'Run Tests with lazy_imports=all'
24+
runs-on: ubuntu-26.04
25+
timeout-minutes: 60
26+
env:
27+
EXCLUDE_FILE: Lib/test/lazy_imports_all_exclude.txt
28+
steps:
29+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
30+
with:
31+
persist-credentials: false
32+
- name: Register gcc problem matcher
33+
run: echo "::add-matcher::.github/problem-matchers/gcc.json"
34+
- name: Install dependencies
35+
run: sudo ./.github/workflows/posix-deps-apt.sh
36+
- name: Configure CPython
37+
run: ./configure --config-cache --with-pydebug
38+
- name: Build CPython
39+
run: make -j4
40+
- name: Display build info
41+
run: make pythoninfo
42+
- name: Verify lazy imports are fully enabled
43+
run: ./python -X lazy_imports=all -c "import sys; assert sys.flags.lazy_imports == 1, sys.flags.lazy_imports; print('lazy imports all enabled')"
44+
- name: Build test list (all tests minus the known-failing exclusions)
45+
run: |
46+
set -euo pipefail
47+
./python -m test --list-tests > all_tests.txt
48+
# Strip comments/blank lines from the exclusion file, then drop those
49+
# exact test names (whole-line, fixed-string match) from the run list.
50+
grep -vE '^\s*(#.*)?$' "$EXCLUDE_FILE" > exclude_tests.txt || true
51+
grep -vxF -f exclude_tests.txt all_tests.txt > run_tests.txt
52+
# Fail loudly if any exclusion entry matched nothing: a stale or
53+
# mistyped name (or a change in `--list-tests` output) would otherwise
54+
# silently stop excluding a module and let it fail the run.
55+
stale=$(comm -23 <(sort -u exclude_tests.txt) <(sort -u all_tests.txt))
56+
if [ -n "$stale" ]; then
57+
echo "::error::Stale entries in $EXCLUDE_FILE (no longer match 'python -m test --list-tests'); remove or fix them:"
58+
echo "$stale"
59+
exit 1
60+
fi
61+
echo "Excluding $(wc -l < exclude_tests.txt) module(s); running $(wc -l < run_tests.txt) of $(wc -l < all_tests.txt)."
62+
- name: Run tests with lazy imports
63+
run: xvfb-run xargs -a run_tests.txt ./python -X lazy_imports=all -m test --fast-ci --timeout=900 < /dev/null
64+
- name: Verify excluded tests still need exclusion
65+
run: |
66+
set -euo pipefail
67+
unexpected_passes=()
68+
while IFS= read -r test_name; do
69+
[ -n "$test_name" ] || continue
70+
echo "Checking excluded test: $test_name"
71+
if xvfb-run ./python -X lazy_imports=all -m test --fast-ci --timeout=900 "$test_name"; then
72+
unexpected_passes+=("$test_name")
73+
fi
74+
done < exclude_tests.txt
75+
if [ "${#unexpected_passes[@]}" -ne 0 ]; then
76+
echo "::error::These tests still appear in $EXCLUDE_FILE but now pass with -X lazy_imports=all. Remove them from the exclude file:"
77+
printf '%s\n' "${unexpected_passes[@]}"
78+
exit 1
79+
fi

‎Doc/license.rst‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -960,10 +960,11 @@ libmpdec
960960
--------
961961

962962
The :mod:`!_decimal` C extension underlying the :mod:`decimal` module
963-
is built using an included copy of the libmpdec
964-
library unless the build is configured ``--with-system-libmpdec``::
963+
uses the libmpdec library if made available by the operating system.
964+
Additionally, the Windows and macOS installers for Python include a copy
965+
of the libmpdec library, so we include a copy of the libmpdec license here::
965966

966-
Copyright (c) 2008-2020 Stefan Krah. All rights reserved.
967+
Copyright (c) 2008-2024 Stefan Krah. All rights reserved.
967968

968969
Redistribution and use in source and binary forms, with or without
969970
modification, are permitted provided that the following conditions

‎Lib/idlelib/editor.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
TK_TABWIDTH_DEFAULT = 8
3535
darwin = sys.platform == 'darwin'
3636

37+
# A letter keysym in a key sequence, such as "s" in "<Control-Key-s>".
38+
_letter_key_re = re.compile(r'(?<=-Key-)[a-zA-Z](?=>)')
39+
40+
3741
class EditorWindow:
3842
is_shell = False # PyShell overrides.
3943
from idlelib.percolator import Percolator
@@ -1186,6 +1190,12 @@ def apply_bindings(self, keydefs=None):
11861190
for event, keylist in keydefs.items():
11871191
if keylist:
11881192
text.event_add(event, *keylist)
1193+
# Caps Lock changes the case of letter keysyms, so bind
1194+
# the sequences with the other case too (gh-56596).
1195+
for keys in keylist:
1196+
other = _letter_key_re.sub(lambda m: m[0].swapcase(), keys)
1197+
if other not in keylist:
1198+
text.event_add(event, other)
11891199

11901200
def fill_menus(self, menudefs=None, keydefs=None):
11911201
"""Fill in dropdown menus used by this window.

‎Lib/idlelib/idle_test/test_editor.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,26 @@ def test_init(self):
3131
self.assertEqual(e.root, self.root)
3232
e._close()
3333

34+
def test_apply_bindings_caps_lock(self):
35+
# gh-56596: Caps Lock changes the case of letter keysyms, so the
36+
# sequences are bound with both cases.
37+
e = Editor(root=self.root)
38+
try:
39+
e.apply_bindings({'<<spam>>': ('<Control-Key-s>', '<Key-F1>'),
40+
'<<eggs>>': ('<Control-Key-x><Alt-Shift-Key-S>',),
41+
'<<ham>>': ('<Control-Key-h>', '<Control-Key-H>')})
42+
self.assertEqual(set(e.text.event_info('<<spam>>')),
43+
{'<Control-KeyPress-s>', '<Control-KeyPress-S>',
44+
'<KeyPress-F1>'})
45+
# Existing variants are not added again.
46+
self.assertEqual(e.text.event_info('<<ham>>'),
47+
('<Control-KeyPress-h>', '<Control-KeyPress-H>'))
48+
self.assertEqual(set(e.text.event_info('<<eggs>>')),
49+
{'<Control-Key-x><Shift-Alt-Key-S>',
50+
'<Control-Key-X><Shift-Alt-Key-s>'})
51+
finally:
52+
e._close()
53+
3454
def test_set_width_zero_char_width(self):
3555
# A zero-width '0' must not raise ZeroDivisionError (gh-90304).
3656
e = Editor(root=self.root)

‎Lib/plistlib.py‎

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,25 @@ def _date_to_string(d, aware_datetime):
159159
d.hour, d.minute, d.second
160160
)
161161

162+
def _dict_items(d, sort_keys, skipkeys):
163+
"""Return the (key, value) pairs of a dict, sorted if needed.
164+
165+
Sorting fails for keys of different types, so non-string keys are
166+
removed or reported before sorting.
167+
"""
168+
items = d.items()
169+
if sort_keys:
170+
if skipkeys:
171+
items = [item for item in items if isinstance(item[0], str)]
172+
items.sort()
173+
else:
174+
for key in d:
175+
if not isinstance(key, str):
176+
raise TypeError("keys must be strings")
177+
items = sorted(items)
178+
return items
179+
180+
162181
def _escape(text):
163182
m = _controlCharPat.search(text)
164183
if m is not None:
@@ -388,11 +407,7 @@ def write_bytes(self, data):
388407
def write_dict(self, d):
389408
if d:
390409
self.begin_element("dict")
391-
if self._sort_keys:
392-
items = sorted(d.items())
393-
else:
394-
items = d.items()
395-
410+
items = _dict_items(d, self._sort_keys, self._skipkeys)
396411
for key, value in items:
397412
if not isinstance(key, str):
398413
if self._skipkeys:
@@ -718,10 +733,7 @@ def _flatten(self, value):
718733
if isinstance(value, (dict, frozendict)):
719734
keys = []
720735
values = []
721-
items = value.items()
722-
if self._sort_keys:
723-
items = sorted(items)
724-
736+
items = _dict_items(value, self._sort_keys, self._skipkeys)
725737
for k, v in items:
726738
if not isinstance(k, str):
727739
if self._skipkeys:
@@ -839,11 +851,7 @@ def _write_object(self, value):
839851
elif isinstance(value, (dict, frozendict)):
840852
keyRefs, valRefs = [], []
841853

842-
if self._sort_keys:
843-
rootItems = sorted(value.items())
844-
else:
845-
rootItems = value.items()
846-
854+
rootItems = _dict_items(value, self._sort_keys, self._skipkeys)
847855
for k, v in rootItems:
848856
if not isinstance(k, str):
849857
if self._skipkeys:
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Test modules that currently FAIL under global lazy imports
2+
# (``-X lazy_imports=all`` / ``PYTHON_LAZY_IMPORTS=all``).
3+
#
4+
# The "Lazy Imports All" CI workflow
5+
# (.github/workflows/reusable-test-lazy-imports-all.yml) runs the whole test
6+
# suite with lazy_imports=all, skipping every module listed here. Exclusion is
7+
# whole-module: a listed module is skipped entirely, so any passing tests it
8+
# contains are not covered until its line is removed. As each module is fixed,
9+
# delete its line so the workflow starts guarding it against regressions. The
10+
# workflow also checks listed modules separately and fails if one now passes,
11+
# so accidental fixes prompt cleanup of this file.
12+
#
13+
# Format: one test name per line, exactly as printed by
14+
# ``python -m test --list-tests``. Lines starting with ``#`` and blank lines
15+
# are ignored. Note that split test packages use a dotted path
16+
# (e.g. test.test_future_stmt.test_future) while ordinary modules use the bare
17+
# name (e.g. test_builtin).
18+
19+
test.test_inspect.test_inspect
20+
test___all__
21+
test__interpreters
22+
test_builtin
23+
test_clinic
24+
test_crossinterp
25+
test_datetime
26+
test_generated_cases
27+
test_heapq
28+
test_import
29+
test_importlib
30+
test_json
31+
test_pkg
32+
test_profile
33+
test_profiling
34+
test_pyrepl
35+
test_subprocess
36+
test_symtable
37+
test_tools
38+
test_trace
39+
test_type_annotations
40+
test_unittest

‎Lib/test/test_array.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1607,10 +1607,19 @@ class HalfFloatTest(FPTest, unittest.TestCase):
16071607
typecode = 'e'
16081608
minitemsize = 2
16091609

1610+
def test_overflows(self):
1611+
# Overflows half-float type:
1612+
self.assertRaises(OverflowError, array.array, self.typecode, [123456])
1613+
# Overflows also float type:
1614+
self.assertRaises(OverflowError, array.array, self.typecode, [1e300])
1615+
16101616
class FloatTest(FPTest, unittest.TestCase):
16111617
typecode = 'f'
16121618
minitemsize = 4
16131619

1620+
def test_overflows(self):
1621+
self.assertRaises(OverflowError, array.array, self.typecode, [1e300])
1622+
16141623
class DoubleTest(FPTest, unittest.TestCase):
16151624
typecode = 'd'
16161625
minitemsize = 8
@@ -1637,6 +1646,10 @@ class ComplexFloatTest(CFPTest, unittest.TestCase):
16371646
typecode = 'Zf'
16381647
minitemsize = 8
16391648

1649+
def test_overflows(self):
1650+
self.assertRaises(OverflowError, array.array, self.typecode, [1e300])
1651+
self.assertRaises(OverflowError, array.array, self.typecode, [1e300j])
1652+
16401653
class ComplexDoubleTest(CFPTest, unittest.TestCase):
16411654
typecode = 'Zd'
16421655
minitemsize = 16

‎Lib/test/test_capi/test_marshal.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def test_write_object_to_file(self):
125125
with self.assertRaises(SystemError):
126126
write_object_to_file(NULL, filename, version)
127127

128-
with self.assertRaisesRegex(ValueError, 'unmarshallable object'):
128+
with self.assertRaisesRegex(ValueError, 'cannot marshal object objects'):
129129
write_object_to_file(UNMARSHALLABLE, filename, version)
130130

131131
def test_read_short_from_file(self):
@@ -225,7 +225,7 @@ def test_pymarshal_writeobjecttostring(self):
225225
obj2 = marshal.loads(data)
226226
self.check_object(obj2, obj)
227227

228-
with self.assertRaisesRegex(ValueError, 'unmarshallable object'):
228+
with self.assertRaisesRegex(ValueError, 'cannot marshal object objects'):
229229
writeobjecttostring(UNMARSHALLABLE, version)
230230

231231
with self.assertRaises(SystemError):

0 commit comments

Comments
 (0)