diff -r 694e50a79638 Lib/idlelib/SearchEngine.py --- a/Lib/idlelib/SearchEngine.py Mon Aug 26 14:00:39 2013 +0300 +++ b/Lib/idlelib/SearchEngine.py Thu Aug 29 22:15:54 2013 -0400 @@ -1,6 +1,6 @@ '''Define SearchEngine for search dialogs.''' import re -from tkinter import * +from tkinter import StringVar, BooleanVar, TclError import tkinter.messagebox as tkMessageBox def get(root): @@ -22,14 +22,13 @@ The dialogs bind these to the UI elements present in the dialogs. ''' - self.root = root - self.patvar = StringVar(root) # search pattern - self.revar = BooleanVar(root) # regular expression? - self.casevar = BooleanVar(root) # match case? - self.wordvar = BooleanVar(root) # match whole word? - self.wrapvar = BooleanVar(root) # wrap around buffer? - self.wrapvar.set(1) # (on by default) - self.backvar = BooleanVar(root) # search backwards? + self.root = root # need for report_error() + self.patvar = StringVar(root, '') # search pattern + self.revar = BooleanVar(root, False) # regular expression? + self.casevar = BooleanVar(root, False) # match case? + self.wordvar = BooleanVar(root, False) # match whole word? + self.wrapvar = BooleanVar(root, True) # wrap around buffer? + self.backvar = BooleanVar(root, False) # search backwards? # Access methods @@ -56,9 +55,16 @@ # Higher level access methods + def setcookedpat(self, pat): + "Set pattern after escaping if re." + # called only in SearchDialog.py: 66 + if self.isre(): + pat = re.escape(pat) + self.setpat(pat) + def getcookedpat(self): pat = self.getpat() - if not self.isre(): + if not self.isre(): # if True, see setcookedpat pat = re.escape(pat) if self.isword(): pat = r"\b%s\b" % pat @@ -90,33 +96,28 @@ # Derived class could override this with something fancier msg = "Error: " + str(msg) if pat: - msg = msg + "\np\Pattern: " + str(pat) + msg = msg + "\nPattern: " + str(pat) if col >= 0: msg = msg + "\nOffset: " + str(col) tkMessageBox.showerror("Regular expression error", msg, master=self.root) - def setcookedpat(self, pat): - if self.isre(): - pat = re.escape(pat) - self.setpat(pat) + def search_text(self, text, prog=None, ok=0): + '''Return (lineno, matchobj) or None for forward/backward search. - def search_text(self, text, prog=None, ok=0): - '''Return (lineno, matchobj) for prog in text widget, or None. + This function calls the right function with the right arguments. + It directly return the result of that call. - If prog is given, it should be a precompiled pattern. - Wrap (yes/no) and direction (forward/back) settings are used. + Text is a text widget. Prog is a precompiled pattern. + The ok parameteris a bit complicated as it has two effects. - The search starts at the selection (if there is one) or at the - insert mark (otherwise). If the search is forward, it starts - at the right of the selection; for a backward search, it - starts at the left end. An empty match exactly at either end - of the selection (or at the insert mark if there is no - selection) is ignored unless the ok flag is true -- this is - done to guarantee progress. + If there is a selection, the search begin at either end, + depending on the direction setting and ok, with ok meaning that + the search starts with the selection. Otherwise, search begins + at the insert mark. - If the search is allowed to wrap around, it will return the - original selection if (and only if) it is the only match. + To aid progress, the search functions do not return an empty + match at the starting position unless ok is True. ''' if not prog: @@ -188,15 +189,18 @@ return None def search_reverse(prog, chars, col): - '''Search backwards in a string (line of text). + '''Search backwards and return an re match object or None. This is done by searching forwards until there is no match. + Prog: compiled re object with a search method returning a match. + Chars: line of text, without \n. + Col: stop index for the search; the limit for match.end(). ''' m = prog.search(chars) if not m: return None found = None - i, j = m.span() + i, j = m.span() # m.start(), m.end() == match slice indexes while i < col and j <= col: found = m if i == j: @@ -226,7 +230,7 @@ line, col = map(int, index.split(".")) # Fails on invalid index return line, col -##if __name__ == "__main__": -## from test import support; support.use_resources = ['gui'] -## import unittest -## unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False) +if __name__ == "__main__": + from test import support; support.use_resources = ['gui'] + import unittest + unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False) diff -r 694e50a79638 Lib/idlelib/idle_test/test_searchengine.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Lib/idlelib/idle_test/test_searchengine.py Thu Aug 29 22:15:54 2013 -0400 @@ -0,0 +1,341 @@ +'''Test functions and SearchEngine class in SearchEngine.py.''' + +# With mock replacements, the module does not use any gui widgets unless +# a real Text widget is passed to the search mthods. This is only +# necessary now for get_selection and search_text because of selections +# and search_backwards because of 'end-1c'. + +import re +import unittest +from test.support import requires +from tkinter import Tk, Text, BooleanVar, StringVar +import tkinter.messagebox as tkMessageBox +from idlelib import SearchEngine as se +from idlelib.idle_test.mock_tk import Var, Mbox +from idlelib.idle_test.mock_tk import Text as mockText + +def setUpModule(): + # Replace s-e module tkinter imports other than non-gui TclError. + se.BooleanVar = Var + se.StringVar = Var + se.tkMessageBox = Mbox + +def tearDownModule(): + # Restore 'just in case', though other tests should also replace. + se.BooleanVar = BooleanVar + se.StringVar = StringVar + se.tkMessageBox = tkMessageBox + + +class Mock: + def __init__(self, *args, **kwargs): pass + +class GetTest(unittest.TestCase): + # SearchEngine.get returns singleton created & saved on first call. + def test_get(self): + saved_Engine = se.SearchEngine + se.SearchEngine = Mock # monkey-patch class + try: + root = Mock() + engine = se.get(root) + self.assertIsInstance(engine, se.SearchEngine) + self.assertIs(root._searchengine, engine) + self.assertIs(se.get(root), engine) + finally: + se.SearchEngine = saved_Engine # restore class to module + +class GetLineColTest(unittest.TestCase): + # Test simple text-independent helper function + def test_get_line_col(self): + self.assertEqual(se.get_line_col('1.0'), (1, 0)) + self.assertEqual(se.get_line_col('1.11'), (1, 11)) + + self.assertRaises(ValueError, se.get_line_col, ('1.0 lineend')) + self.assertRaises(ValueError, se.get_line_col, ('end')) + +class GetSelectionTest(unittest.TestCase): + # Test text-dependent helper function. + # Need gui for text.index('sel.first/sel.last/insert'). + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + + @classmethod + def tearDownClass(cls): + cls.root.destroy() + + def test_get_selection(self): + text = Text(master=self.root) + text.insert('1.0', 'hello world') + # Test with 'Hello' selected. + text.tag_add('sel', '1.0', '1.5') + self.assertEqual(se.get_selection(text), ('1.0', '1.5')) + # Test with ' world' selected. + text.tag_remove('sel', '1.0', 'end') + text.tag_add('sel', '1.6', '1.end') + self.assertEqual(se.get_selection(text), ('1.6', '1.11')) + + def test_get_mark(self): + text = Text(master=self.root) + text.insert('1.0', 'hello world') + # Test with no selection and cursor at 1.0. + text.mark_set('insert', '1.0') + self.assertEqual(se.get_selection(text), ('1.0', '1.0')) + # Test with no selection and cursor at 1.8. + text.mark_set('insert', '1.8') + self.assertEqual(se.get_selection(text), ('1.8', '1.8')) + + +class SearchReverseTest(unittest.TestCase): + # Test helper function that searches backwards within a line. + def test_search_reverse(self): + Equal = self.assertEqual + line = "Here is an 'is' test text." + prog = re.compile('is') + Equal(se.search_reverse(prog, line, len(line)).span(), (12, 14)) + Equal(se.search_reverse(prog, line, 14).span(), (12, 14)) + Equal(se.search_reverse(prog, line, 13).span(), (5, 7)) + Equal(se.search_reverse(prog, line, 7).span(), (5, 7)) + Equal(se.search_reverse(prog, line, 6), None) + + +class SearchEngineTest(unittest.TestCase): + # Test class methods that do not use Text widget. + + def setUp(self): + self.engine = se.SearchEngine(root=None) + # Engine.root is only used to create error message boxes. + # The mock replacement ignores the root argument. + + def test_is_get(self): + engine = self.engine + Equal = self.assertEqual + + Equal(engine.getpat(), '') + engine.setpat('hello') + Equal(engine.getpat(), 'hello') + + Equal(engine.isre(), False) + engine.revar.set(1) + Equal(engine.isre(), True) + + Equal(engine.iscase(), False) + engine.casevar.set(1) + Equal(engine.iscase(), True) + + Equal(engine.isword(), False) + engine.wordvar.set(1) + Equal(engine.isword(), True) + + Equal(engine.iswrap(), True) + engine.wrapvar.set(0) + Equal(engine.iswrap(), False) + + Equal(engine.isback(), False) + engine.backvar.set(1) + Equal(engine.isback(), True) + + def test_setcookedpat(self): + engine = self.engine + engine.setcookedpat('\s') + self.assertEqual(engine.getpat(), '\s') + engine.revar.set(1) + engine.setcookedpat('\s') + self.assertEqual(engine.getpat(), r'\\s') + + def test_getcookedpat(self): + engine = self.engine + Equal = self.assertEqual + + Equal(engine.getcookedpat(), '') + engine.setpat('hello') + Equal(engine.getcookedpat(), 'hello') + engine.wordvar.set(True) + Equal(engine.getcookedpat(), r'\bhello\b') + engine.wordvar.set(False) + + engine.setpat('\s') + Equal(engine.getcookedpat(), r'\\s') + engine.revar.set(True) + Equal(engine.getcookedpat(), '\s') + + def test_getprog(self): + engine = self.engine + Equal = self.assertEqual + + engine.setpat('Hello') + temppat = engine.getprog() + Equal(temppat.pattern, re.compile('Hello', re.IGNORECASE).pattern) + engine.casevar.set(1) + temppat = engine.getprog() + Equal(temppat.pattern, re.compile('Hello').pattern, 0) + + engine.setpat('') + Equal(engine.getprog(), None) + engine.setpat('+') + engine.revar.set(1) + Equal(engine.getprog(), None) + self.assertEqual(Mbox.showerror.message, + 'Error: nothing to repeat\nPattern: +') + + def test_report_error(self): + showerror = Mbox.showerror + Equal = self.assertEqual + pat = '[a-z' + msg = 'unexpected end of regular expression' + + Equal(self.engine.report_error(pat, msg), None) + Equal(showerror.title, 'Regular expression error') + expected_message = ("Error: " + msg + "\nPattern: [a-z") + Equal(showerror.message, expected_message) + + Equal(self.engine.report_error(pat, msg, 5), None) + Equal(showerror.title, 'Regular expression error') + expected_message += "\nOffset: 5" + Equal(showerror.message, expected_message) + + +class SearchTest(unittest.TestCase): + # Test that search_text makes right call to right method. + # Currently needed gui for get_selection() call. + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + cls.text = Text(master=cls.root) + test_text = ( + 'First line\n' + 'Line with target\n' + 'Last line\n') + cls.text.insert('1.0', test_text) + cls.pat = re.compile('target') + + cls.engine = se.SearchEngine(None) + cls.engine.search_forward = lambda *args: ('f', args) + cls.engine.search_backward = lambda *args: ('b', args) + + @classmethod + def tearDownClass(cls): + cls.root.destroy() + + def test_search(self): + Equal = self.assertEqual + engine = self.engine + search = engine.search_text + text = self.text + pat = self.pat + + text.mark_set('insert', '1.5') + Equal(search(text, pat), ('f', (text, pat, 1, 5, True, False))) + engine.wrapvar.set(False) + Equal(search(text, pat), ('f', (text, pat, 1, 5, False, False))) + engine.wrapvar.set(True) + engine.backvar.set(True) + Equal(search(text, pat), ('b', (text, pat, 1, 5, True, False))) + engine.backvar.set(False) + + self.text.tag_add('sel', '2.10', '2.16') + Equal(search(text, pat), ('f', (text, pat, 2, 16, True, False))) + Equal(search(text, pat, True), ('f', (text, pat, 2, 10, True, True))) + engine.backvar.set(True) + Equal(search(text, pat), ('b', (text, pat, 2, 10, True, False))) + Equal(search(text, pat, True), ('b', (text, pat, 2, 16, True, True))) + + +class ForwardTest(unittest.TestCase): + # Test that search_forward method finds the target. + + @classmethod + def setUpClass(cls): + cls.engine = se.SearchEngine(None) +## requires('gui') +## cls.root = Tk() +## cls.text = Text(master=cls.root) + cls.text = mockText() + test_text = ( + 'First line\n' + 'Line with target\n' + 'Last line\n') + cls.text.insert('1.0', test_text) + cls.pat = re.compile('target') + cls.res = (2, (10, 16)) # line, slice indexes of 'target' + cls.failpat = re.compile('xyz') # not in text + cls.emptypat = re.compile('\w*') # empty match possible + + def make_search(self, func): + def search(pat, line, col, wrap, ok=0): + res = func(self.text, pat, line, col, wrap, ok) + # res is (line, matchobject) or None + return (res[0], res[1].span()) if res else res + return search + + def test_search_forward(self): + # search for non-empty match + Equal = self.assertEqual + forward = self.make_search(self.engine.search_forward) + pat = self.pat + Equal(forward(pat, 1, 0, True), self.res) + Equal(forward(pat, 3, 0, True), self.res) # wrap + Equal(forward(pat, 3, 0, False), None) # no wrap + Equal(forward(pat, 2, 10, False), self.res) + Equal(forward(self.failpat, 1, 0, True), None) + Equal(forward(self.emptypat, 2, 9, True, ok=True), (2, (9, 9))) + #Equal(forward(self.emptypat, 2, 9, True), self.res) + # While the initial empty match is correctly ignored, skipping + # the rest of the line and returning (3, (0,4)) seems buggy - tjr. + Equal(forward(self.emptypat, 2, 10, True), self.res) + +class BackwardTest(unittest.TestCase): + # Test that search_backward method finds the target. + # Currently need gui for text.index("end-1c") in tested method. + # Will combine with ForwardTest when do not need gui. + + @classmethod + def setUpClass(cls): + cls.engine = se.SearchEngine(None) + requires('gui') + cls.root = Tk() + cls.text = Text(master=cls.root) +## cls.text = mockText() + test_text = ( + 'First line\n' + 'Line with target\n' + 'Last line\n') + cls.text.insert('1.0', test_text) + cls.pat = re.compile('target') + cls.res = (2, (10, 16)) # line, slice indexes of 'target' + cls.failpat = re.compile('xyz') # not in text + cls.emptypat = re.compile('\w*') # empty match possible + + @classmethod + def tearDownClass(cls): + cls.root.destroy() + + def make_search(self, func): + # written to work in combined ForwardBackwardTest + def search(pat, line, col, wrap, ok=0): + res = func(self.text, pat, line, col, wrap, ok) + # res is (line, matchobject) or None + return (res[0], res[1].span()) if res else res + return search + + def test_search_backward(self): + # search for non-empty match + Equal = self.assertEqual + backward = self.make_search(self.engine.search_backward) + pat = self.pat + Equal(backward(pat, 3, 5, True), self.res) + Equal(backward(pat, 2, 0, True), self.res) # wrap + Equal(backward(pat, 2, 0, False), None) # no wrap + Equal(backward(pat, 2, 16, False), self.res) + Equal(backward(self.failpat, 3, 9, True), None) + Equal(backward(self.emptypat, 2, 10, True, ok=True), (2, (9,9))) + # Accepted because 9 < 10, not because ok=True. + # It is not clear that ok=True is useful going back - tjr + Equal(backward(self.emptypat, 2, 9, True), (2, (5, 9))) + + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=2)