Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Lib/idlelib/idle_test/test_searchbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
31 changes: 30 additions & 1 deletion Lib/idlelib/idle_test/test_searchengine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
24 changes: 24 additions & 0 deletions Lib/idlelib/searchbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand All @@ -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).

Expand Down
28 changes: 22 additions & 6 deletions Lib/idlelib/searchengine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The IDLE search dialogs now show a regular expression error below the entry
instead of in a message box.
Loading