From b42dcf6d58cbb0e43c0497e6fa14407996c96848 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 17 Sep 2026 04:31:17 +0300 Subject: [PATCH 1/3] gh-105689: Parse only the current statement in the IDLE Shell (#157594) Since the prompts moved to the sidebar, sys.ps1 ends with a newline and prompt_last_line is empty, so HyperParser and newline_and_indent took the editor path in the Shell and parsed previous output. Use an explicit is_shell attribute instead. Co-authored-by: Claude Opus 5 (1M context) --- Lib/idlelib/editor.py | 4 ++-- Lib/idlelib/hyperparser.py | 2 +- Lib/idlelib/idle_test/test_autocomplete.py | 2 +- Lib/idlelib/idle_test/test_calltip.py | 2 +- Lib/idlelib/idle_test/test_hyperparser.py | 8 ++++---- Lib/idlelib/idle_test/test_parenmatch.py | 2 +- Lib/idlelib/pyshell.py | 2 +- .../IDLE/2026-09-15-21-54-04.gh-issue-105689.ASoU37.rst | 2 ++ 8 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/IDLE/2026-09-15-21-54-04.gh-issue-105689.ASoU37.rst diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 5e9f6aa86e8192..8e15319b5baab2 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -35,6 +35,7 @@ darwin = sys.platform == 'darwin' class EditorWindow: + is_shell = False # PyShell overrides. from idlelib.percolator import Percolator from idlelib.colorizer import ColorDelegator, color_config from idlelib.undo import UndoDelegator @@ -80,7 +81,6 @@ def __init__(self, flist=None, filename=None, key=None, root=None): self.recent_files_path = idleConf.userdir and os.path.join( idleConf.userdir, 'recent-files.lst') - self.prompt_last_line = '' # Override in PyShell self.text_frame = text_frame = Frame(top) self.vbar = vbar = Scrollbar(text_frame, name='vbar') width = idleConf.GetOption('main', 'EditorWindow', 'width', type='int') @@ -1434,7 +1434,7 @@ def newline_and_indent_event(self, event): # First need to find the last statement. lno = index2line(text.index('insert')) y = pyparse.Parser(self.indentwidth, self.tabwidth) - if not self.prompt_last_line: + if not self.is_shell: for context in self.num_context_lines: startat = max(lno - context, 1) startatindex = repr(startat) + ".0" diff --git a/Lib/idlelib/hyperparser.py b/Lib/idlelib/hyperparser.py index 76144ee8fb30f5..d1a28b274a499f 100644 --- a/Lib/idlelib/hyperparser.py +++ b/Lib/idlelib/hyperparser.py @@ -35,7 +35,7 @@ def index2line(index): return int(float(index)) lno = index2line(text.index(index)) - if not editwin.prompt_last_line: + if not editwin.is_shell: for context in editwin.num_context_lines: startat = max(lno - context, 1) startatindex = repr(startat) + ".0" diff --git a/Lib/idlelib/idle_test/test_autocomplete.py b/Lib/idlelib/idle_test/test_autocomplete.py index 9086c31d2733b6..cd4664d21722bb 100644 --- a/Lib/idlelib/idle_test/test_autocomplete.py +++ b/Lib/idlelib/idle_test/test_autocomplete.py @@ -19,7 +19,7 @@ def __init__(self, root, text): self.text = text self.indentwidth = 8 self.tabwidth = 8 - self.prompt_last_line = '>>>' # Currently not used by autocomplete. + self.is_shell = True class AutoCompleteTest(unittest.TestCase): diff --git a/Lib/idlelib/idle_test/test_calltip.py b/Lib/idlelib/idle_test/test_calltip.py index 28c196a42672fc..e216ae67f68b4d 100644 --- a/Lib/idlelib/idle_test/test_calltip.py +++ b/Lib/idlelib/idle_test/test_calltip.py @@ -282,7 +282,7 @@ class mock_Shell: def __init__(self, text): text.tag_prevrange = Mock(return_value=None) self.text = text - self.prompt_last_line = ">>> " + self.is_shell = True self.indentwidth = 4 self.tabwidth = 8 diff --git a/Lib/idlelib/idle_test/test_hyperparser.py b/Lib/idlelib/idle_test/test_hyperparser.py index 343843c4166e97..df61d2641d3c7f 100644 --- a/Lib/idlelib/idle_test/test_hyperparser.py +++ b/Lib/idlelib/idle_test/test_hyperparser.py @@ -11,7 +11,7 @@ def __init__(self, text): self.text = text self.indentwidth = 8 self.tabwidth = 8 - self.prompt_last_line = '>>>' + self.is_shell = True self.num_context_lines = 50, 500, 1000 _build_char_in_string_func = EditorWindow._build_char_in_string_func @@ -53,7 +53,7 @@ def setUp(self): def tearDown(self): self.text.delete('1.0', 'end') - self.editwin.prompt_last_line = '>>>' + self.editwin.is_shell = True def get_parser(self, index): """ @@ -70,8 +70,8 @@ def test_init(self): p = self.get_parser('1.5') self.assertIn('precedes', str(ve.exception)) - # test without ps1 - self.editwin.prompt_last_line = '' + # test an editor + self.editwin.is_shell = False # number of lines lesser than 50 p = self.get_parser('end') diff --git a/Lib/idlelib/idle_test/test_parenmatch.py b/Lib/idlelib/idle_test/test_parenmatch.py index 2e10d7cd36760f..4907d77fff4c7c 100644 --- a/Lib/idlelib/idle_test/test_parenmatch.py +++ b/Lib/idlelib/idle_test/test_parenmatch.py @@ -17,7 +17,7 @@ def __init__(self, text): self.text = text self.indentwidth = 8 self.tabwidth = 8 - self.prompt_last_line = '>>>' # Currently not used by parenmatch. + self.is_shell = True class ParenMatchTest(unittest.TestCase): diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index b69a5980bc5338..997b91e53327d7 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -852,6 +852,7 @@ def display_executing_dialog(self): class PyShell(OutputWindow): + is_shell = True from idlelib.squeezer import Squeezer shell_title = "IDLE Shell" @@ -909,7 +910,6 @@ def __init__(self, flist=None): self.indentwidth = 4 self.sys_ps1 = sys.ps1 if hasattr(sys, 'ps1') else '>>>\n' - self.prompt_last_line = self.sys_ps1.split('\n')[-1] self.prompt = self.sys_ps1 # Changes when debug active text = self.text diff --git a/Misc/NEWS.d/next/IDLE/2026-09-15-21-54-04.gh-issue-105689.ASoU37.rst b/Misc/NEWS.d/next/IDLE/2026-09-15-21-54-04.gh-issue-105689.ASoU37.rst new file mode 100644 index 00000000000000..7b4924fa0af228 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2026-09-15-21-54-04.gh-issue-105689.ASoU37.rst @@ -0,0 +1,2 @@ +Fix calltips, parenthesis matching and auto-indent in the IDLE Shell after +output containing unbalanced quotes or parentheses. From b215552a32666d38db5e85e1a6da91374610dd73 Mon Sep 17 00:00:00 2001 From: Samartha Date: Thu, 17 Sep 2026 08:18:25 +0530 Subject: [PATCH 2/3] gh-157621: Clear weakrefs for ParamSpec attributes (#157622) --- Lib/test/test_typing.py | 11 +++++++++++ .../2026-09-16-14-04-38.gh-issue-157621.R6v2S8.rst | 2 ++ Objects/typevarobject.c | 1 + 3 files changed, 14 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-09-16-14-04-38.gh-issue-157621.R6v2S8.rst diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index f35f864dce21e8..b0d468b34092b2 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -10299,6 +10299,17 @@ def test_args_kwargs(self): self.assertEqual(repr(P.args), "P.args") self.assertEqual(repr(P.kwargs), "P.kwargs") + def test_args_kwargs_weakrefs(self): + P = ParamSpec('P') + for attr_name in ('args', 'kwargs'): + with self.subTest(attr_name=attr_name): + callback_fired = [] + attr = getattr(P, attr_name) + ref = weakref.ref(attr, lambda _: callback_fired.append(True)) + del attr + self.assertEqual(callback_fired, [True]) + self.assertIsNone(ref()) + def test_stringized(self): P = ParamSpec('P') class C(Generic[P]): diff --git a/Misc/NEWS.d/next/Library/2026-09-16-14-04-38.gh-issue-157621.R6v2S8.rst b/Misc/NEWS.d/next/Library/2026-09-16-14-04-38.gh-issue-157621.R6v2S8.rst new file mode 100644 index 00000000000000..b8342da0faebb9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-16-14-04-38.gh-issue-157621.R6v2S8.rst @@ -0,0 +1,2 @@ +Fix a crash when a weak-reference callback is attached to a +:class:`typing.ParamSpecArgs` or :class:`typing.ParamSpecKwargs` instance. diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c index 53c35083896961..eed1457cbb426d 100644 --- a/Objects/typevarobject.c +++ b/Objects/typevarobject.c @@ -958,6 +958,7 @@ paramspecattr_dealloc(PyObject *self) _PyObject_GC_UNTRACK(self); Py_XDECREF(psa->__origin__); + PyObject_ClearWeakRefs(self); Py_TYPE(self)->tp_free(self); Py_DECREF(tp); From c39a32983728f2398bb506c20e5c162d61805da3 Mon Sep 17 00:00:00 2001 From: Shamil Date: Thu, 17 Sep 2026 05:59:34 +0300 Subject: [PATCH 3/3] gh-156402: Modernize annotation usage in libregrtest (#156403) --- Lib/test/libregrtest/main.py | 8 ++------ Lib/test/libregrtest/mypy.ini | 2 +- Lib/test/libregrtest/refleak.py | 9 +++------ Lib/test/libregrtest/results.py | 7 ++----- Lib/test/libregrtest/utils.py | 3 +-- 5 files changed, 9 insertions(+), 20 deletions(-) diff --git a/Lib/test/libregrtest/main.py b/Lib/test/libregrtest/main.py index db2e9acb850f10..2e8397a8a91d32 100644 --- a/Lib/test/libregrtest/main.py +++ b/Lib/test/libregrtest/main.py @@ -470,8 +470,7 @@ def finalize_tests(self, coverage: trace.CoverageResults | None) -> None: os.unlink(self.next_single_filename) if coverage is not None: - # uses a new-in-Python 3.13 keyword argument that mypy doesn't know about yet: - coverage.write_results(show_missing=True, summary=True, # type: ignore[call-arg] + coverage.write_results(show_missing=True, summary=True, coverdir=self.coverage_dir, ignore_missing_files=True) @@ -539,10 +538,7 @@ def _run_tests(self, selected: TestTuple, tests: TestList | None) -> int: if self.num_workers < 0: # Use all CPUs + 2 extra worker processes for tests # that like to sleep - # - # os.process.cpu_count() is new in Python 3.13; - # mypy doesn't know about it yet - self.num_workers = (os.process_cpu_count() or 1) + 2 # type: ignore[attr-defined] + self.num_workers = (os.process_cpu_count() or 1) + 2 # For a partial run, we do not need to clutter the output. if (self.want_header diff --git a/Lib/test/libregrtest/mypy.ini b/Lib/test/libregrtest/mypy.ini index 3fa9afcb7a4a8c..2830647635278c 100644 --- a/Lib/test/libregrtest/mypy.ini +++ b/Lib/test/libregrtest/mypy.ini @@ -5,7 +5,7 @@ [mypy] files = Lib/test/libregrtest explicit_package_bases = True -python_version = 3.12 +python_version = 3.15 platform = linux pretty = True diff --git a/Lib/test/libregrtest/refleak.py b/Lib/test/libregrtest/refleak.py index 69a9c9d6e5f1d7..45064115463c2f 100644 --- a/Lib/test/libregrtest/refleak.py +++ b/Lib/test/libregrtest/refleak.py @@ -96,9 +96,8 @@ def runtest_refleak(test_name, test_func, # `ByteString` is not included in `collections.abc.__all__` with warnings.catch_warnings(action='ignore', category=DeprecationWarning): - ByteString = collections.abc.ByteString - # Mypy doesn't even think `ByteString` is a class, hence the `type: ignore` - for obj in ByteString.__subclasses__() + [ByteString]: # type: ignore[attr-defined] + ByteString = collections.abc.ByteString # type: ignore[attr-defined] + for obj in ByteString.__subclasses__() + [ByteString]: abcs[obj] = _get_dump(obj)[0] warmups = hunt_refleak.warmups @@ -151,9 +150,7 @@ def runtest_refleak(test_name, test_func, # Also, readjust the reference counts and alloc blocks by ignoring # any strings that might have been interned during test_func. These # strings will be deallocated at runtime shutdown - interned_immortal_after = getunicodeinternedsize( - # Use an internal-only keyword argument that mypy doesn't know yet - _only_immortal=True) # type: ignore[call-arg] + interned_immortal_after = getunicodeinternedsize(_only_immortal=True) alloc_after = getallocatedblocks() - interned_immortal_after rc_after = gettotalrefcount() fd_after = fd_count() diff --git a/Lib/test/libregrtest/results.py b/Lib/test/libregrtest/results.py index ea5fee33421541..3475f645729182 100644 --- a/Lib/test/libregrtest/results.py +++ b/Lib/test/libregrtest/results.py @@ -1,7 +1,7 @@ import sys import trace from _colorize import get_colors # type: ignore[import-not-found] -from typing import TYPE_CHECKING +lazy from xml.etree.ElementTree import Element from .runtests import RunTests from .result import State, TestResult, TestStats, Location @@ -9,9 +9,6 @@ StrPath, TestName, TestTuple, TestList, FilterDict, printlist, count, format_duration) -if TYPE_CHECKING: - from xml.etree.ElementTree import Element - # Python uses exit code 1 when an exception is not caught # argparse.ArgumentParser.error() uses exit code 2 @@ -41,7 +38,7 @@ def __init__(self) -> None: self.test_times: list[tuple[float, TestName]] = [] self.stats = TestStats() # used by --junit-xml - self.testsuite_xml: list['Element'] = [] + self.testsuite_xml: list[Element] = [] # used by -T with -j self.covered_lines: set[Location] = set() diff --git a/Lib/test/libregrtest/utils.py b/Lib/test/libregrtest/utils.py index 6de2d07bcbb3b3..83e0575619c99a 100644 --- a/Lib/test/libregrtest/utils.py +++ b/Lib/test/libregrtest/utils.py @@ -652,8 +652,7 @@ def display_header(use_resources: dict[str, str | None], cpu_count: object = os.cpu_count() if cpu_count: - # The function is new in Python 3.13; mypy doesn't know about it yet: - process_cpu_count = os.process_cpu_count() # type: ignore[attr-defined] + process_cpu_count = os.process_cpu_count() if process_cpu_count and process_cpu_count != cpu_count: cpu_count = f"{process_cpu_count} (process) / {cpu_count} (system)" print("== CPU count:", cpu_count)