From aa8668a14e4a931aaf968a85992665fb941f2af6 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 17 Sep 2026 04:31:17 +0300 Subject: [PATCH] gh-105689: Parse only the current statement in the IDLE Shell (GH-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. (cherry picked from commit b42dcf6d58cbb0e43c0497e6fa14407996c96848) Co-authored-by: Serhiy Storchaka 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.