https://github.com/python/cpython/commit/9ab004d41ec18e71fd8afe992a831e6156b84b03 commit: 9ab004d41ec18e71fd8afe992a831e6156b84b03 branch: main author: Serhiy Storchaka <[email protected]> committer: serhiy-storchaka <[email protected]> date: 2026-09-17T12:02:18+03:00 summary:
gh-69365: Show search pattern errors in the IDLE search dialogs (GH-157598) Show the error in red below the entry and move the cursor to the offending character in the pattern, instead of a message box. Find Again without a dialog still uses the message box. Check the pattern as typed, so that the position is right. This also fixes the "Whole word" option with a regular expression: the pattern is now grouped, so that "a|b" is anchored on both sides, and a pattern ending with a backslash is an error instead of matching "\b". Co-authored-by: Claude Opus 5 (1M context) <[email protected]> files: A Misc/NEWS.d/next/IDLE/2026-09-15-22-34-33.gh-issue-69365.KehjZN.rst M Lib/idlelib/idle_test/test_searchbase.py M Lib/idlelib/idle_test/test_searchengine.py M Lib/idlelib/searchbase.py M Lib/idlelib/searchengine.py diff --git a/Lib/idlelib/idle_test/test_searchbase.py b/Lib/idlelib/idle_test/test_searchbase.py index 1780cab6527dd94..aeb85dc3ebf226e 100644 --- a/Lib/idlelib/idle_test/test_searchbase.py +++ b/Lib/idlelib/idle_test/test_searchbase.py @@ -56,6 +56,16 @@ def test_open_and_close(self): self.dialog.open(text, searchphrase="hello") self.assertEqual(self.dialog.ent.get(), 'hello') + + # While open, the dialog shows pattern errors (gh-69365). + self.assertEqual(self.engine.error_handler, self.dialog.show_error) + self.dialog.show_error('Error: spam', 1) + self.assertEqual(self.dialog.error_label['text'], 'Error: spam') + self.assertEqual(self.dialog.ent.index('insert'), 1) + self.engine.patvar.set('eggs') # Editing the pattern clears it. + self.assertEqual(self.dialog.error_label['text'], '') + self.dialog.close() + self.assertIsNone(self.engine.error_handler) toplevel.update_idletasks() toplevel.destroy() diff --git a/Lib/idlelib/idle_test/test_searchengine.py b/Lib/idlelib/idle_test/test_searchengine.py index 9d9798394195860..5207d2a6b4a9369 100644 --- a/Lib/idlelib/idle_test/test_searchengine.py +++ b/Lib/idlelib/idle_test/test_searchengine.py @@ -154,7 +154,7 @@ def test_getcookedpat(self): engine.setpat('hello') Equal(engine.getcookedpat(), 'hello') engine.wordvar.set(True) - Equal(engine.getcookedpat(), r'\bhello\b') + Equal(engine.getcookedpat(), r'\b(?:hello)\b') engine.wordvar.set(False) engine.setpat(r'\s') @@ -182,6 +182,24 @@ def test_getprog(self): Equal(engine.getprog(), None) Equal(Mbox.showerror.message, 'Error: nothing to repeat\nPattern: +\nOffset: 0') + # Errors are reported for the pattern as typed, not as cooked. + engine.wordvar.set(True) + engine.setpat('a\\') + Equal(engine.getprog(), None) + Equal(Mbox.showerror.message, + 'Error: bad escape (end of pattern)\nPattern: a\\\nOffset: 1') + engine.setpat('a|b') + Equal(engine.getprog().pattern, r'\b(?:a|b)\b') + engine.setpat(')(') + Equal(engine.getprog(), None) + Equal(Mbox.showerror.message, + 'Error: unbalanced parenthesis\nPattern: )(\nOffset: 0') + engine.setpat('(?i)x') + Equal(engine.getprog(), None) + Equal(Mbox.showerror.message, + 'Error: global flags like (?i) cannot be used with the ' + '"Whole word" option\nPattern: (?i)x') + engine.wordvar.set(False) def test_report_error(self): showerror = Mbox.showerror @@ -199,6 +217,17 @@ def test_report_error(self): expected_message += "\nOffset: 5" Equal(showerror.message, expected_message) + # An open dialog shows the message itself (gh-69365). + messages = [] + self.engine.error_handler = lambda msg, pos: messages.append((msg, pos)) + self.addCleanup(setattr, self.engine, 'error_handler', None) + showerror.message = None + Equal(self.engine.report_error(pat, msg, 3), None) + Equal(messages, [("Error: " + msg + " at position 3", 3)]) + Equal(showerror.message, None) + Equal(self.engine.report_error(pat, "Empty"), None) + Equal(messages[-1], ("Error: Empty", None)) + class SearchTest(unittest.TestCase): # Test that search_text makes right call to right method. diff --git a/Lib/idlelib/searchbase.py b/Lib/idlelib/searchbase.py index c68a6ca339af045..da96a047348094e 100644 --- a/Lib/idlelib/searchbase.py +++ b/Lib/idlelib/searchbase.py @@ -64,14 +64,30 @@ def open(self, text, searchphrase=None): self.ent.selection_range(0, "end") self.ent.icursor(0) self.top.grab_set() + self.show_error('') + self.engine.error_handler = self.show_error def close(self, event=None): "Put dialog away for later use." if self.top: + self.engine.error_handler = None self.top.grab_release() self.top.transient('') self.top.withdraw() + def show_error(self, message, pos=None): + """Show message (or nothing) in red below the entries. + + If pos is given, move the cursor to that position of the pattern. + """ + self.error_label['text'] = message + if message: + self.bell() + if pos is not None: + self.ent.focus_set() + self.ent.selection_clear() + self.ent.icursor(pos) + def create_widgets(self): '''Create basic 3 row x 3 col search (find) dialog. @@ -96,6 +112,7 @@ def create_widgets(self): self.frame.grid_columnconfigure(1, pad=2, minsize=100, weight=100) self.create_entries() # row 0 (and maybe 1), cols 0, 1 + self.create_error_label() # next row, col 1 self.create_option_buttons() # next row, cols 0, 1 self.create_other_buttons() # next row, cols 0, 1 self.create_command_buttons() # col 2, all rows @@ -117,6 +134,13 @@ def create_entries(self): "Create one or more entry lines with make_entry." self.ent = self.make_entry("Find:", self.engine.patvar)[0] + def create_error_label(self): + "Create the label for a pattern error, cleared when the pattern changes." + self.error_label = Label(self.frame, text=' ', foreground='red') + self.error_label.grid(row=self.row, column=1, sticky="nw") + self.row = self.row + 1 + self.engine.patvar.trace_add('write', lambda *args: self.show_error('')) + def make_frame(self,labeltext=None): '''Return (frame, label). diff --git a/Lib/idlelib/searchengine.py b/Lib/idlelib/searchengine.py index ceb38cfaef900ba..54c5399541900a1 100644 --- a/Lib/idlelib/searchengine.py +++ b/Lib/idlelib/searchengine.py @@ -31,6 +31,7 @@ def __init__(self, root): self.wordvar = BooleanVar(root, False) # match whole word? self.wrapvar = BooleanVar(root, True) # wrap around buffer? self.backvar = BooleanVar(root, False) # search backwards? + self.error_handler = None # Set by an open dialog, see report_error. # Access methods @@ -69,7 +70,7 @@ def getcookedpat(self): if not self.isre(): # if True, see setcookedpat pat = re.escape(pat) if self.isword(): - pat = r"\b%s\b" % pat + pat = r"\b(?:%s)\b" % pat return pat def getprog(self): @@ -78,19 +79,34 @@ def getprog(self): if not pat: self.report_error(pat, "Empty regular expression") return None - pat = self.getcookedpat() flags = 0 if not self.iscase(): flags = flags | re.IGNORECASE + if self.isre(): + # Check the pattern as typed, so that an error is reported + # at the right position. + try: + re.compile(pat, flags) + except re.PatternError as e: + self.report_error(pat, e.msg, e.pos) + return None try: - prog = re.compile(pat, flags) + return re.compile(self.getcookedpat(), flags) except re.PatternError as e: - self.report_error(pat, e.msg, e.pos) + msg = e.msg + if msg.startswith('global flags not at the start'): + msg = ('global flags like (?i) cannot be used ' + 'with the "Whole word" option') + self.report_error(pat, msg) return None - return prog def report_error(self, pat, msg, col=None): - # Derived class could override this with something fancier + "Show msg in the open dialog, if any, else in a message box." + if self.error_handler is not None: + if col is not None: + msg = f"{msg} at position {col}" + self.error_handler("Error: " + str(msg), col) + return msg = "Error: " + str(msg) if pat: msg = msg + "\nPattern: " + str(pat) diff --git a/Misc/NEWS.d/next/IDLE/2026-09-15-22-34-33.gh-issue-69365.KehjZN.rst b/Misc/NEWS.d/next/IDLE/2026-09-15-22-34-33.gh-issue-69365.KehjZN.rst new file mode 100644 index 000000000000000..d29c87aed78e1a0 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2026-09-15-22-34-33.gh-issue-69365.KehjZN.rst @@ -0,0 +1,2 @@ +The IDLE search dialogs now show a regular expression error below the entry +instead of in a message box. _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]
