From cda1a346ee2eaa6ccf3159940afa25d3289f3242 Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Fri, 9 Jun 2017 17:55:57 -0400 Subject: [PATCH 1/6] bpo-30617: IDLE: docstrings and unittest for outwin.py --- Lib/idlelib/idle_test/test_outwin.py | 38 ++++++++++++++++++++ Lib/idlelib/outwin.py | 52 +++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 Lib/idlelib/idle_test/test_outwin.py diff --git a/Lib/idlelib/idle_test/test_outwin.py b/Lib/idlelib/idle_test/test_outwin.py new file mode 100644 index 00000000000000..6cd7a3c99fec9d --- /dev/null +++ b/Lib/idlelib/idle_test/test_outwin.py @@ -0,0 +1,38 @@ +""" Test idlelib.outwin. +""" + +import unittest +import unittest.mock as mock +import re +from idlelib import outwin + + +class DummyOutputWindow: + _file_line_helper = outwin.OutputWindow._file_line_helper + goto_file_line = outwin.OutputWindow.goto_file_line + file_line_pats = outwin.OutputWindow.file_line_pats + + +class OutputWindowFunctionTest(unittest.TestCase): + + @mock.patch('builtins.open') + def test_file_line_helper(self, mock_open): + dummy = DummyOutputWindow() + + l = [] + for pat in dummy.file_line_pats: + l.append(re.compile(pat, re.IGNORECASE)) + dummy.file_line_progs = l + + text = ((r'file "filepasstest1", line 42, text', ('filepasstest1', 42)), + (r'filepasstest2(42)', ('filepasstest2', 42)), + (r'filepasstest3: 42: text text\n', ('filepasstest3', 42)), + (r'filefailtest 10 text', None)) + for line, expected_output in text: + self.assertEqual(dummy._file_line_helper(line), expected_output) + if expected_output: + mock_open.assert_called_with(expected_output[0], 'r') + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/Lib/idlelib/outwin.py b/Lib/idlelib/outwin.py index f6d2915c62443e..08988e59e9ee08 100644 --- a/Lib/idlelib/outwin.py +++ b/Lib/idlelib/outwin.py @@ -1,3 +1,6 @@ +"""Editor window that can serve as an output file. +""" + import re from tkinter import * @@ -8,13 +11,13 @@ class OutputWindow(EditorWindow): - """An editor window that can serve as an output file. Also the future base class for the Python shell window. This class has no input facilities. - """ + Adds binding to open a file at a line to the text widget. + """ def __init__(self, *args): EditorWindow.__init__(self, *args) self.text.bind("<>", self.goto_file_line) @@ -22,13 +25,16 @@ def __init__(self, *args): # Customize EditorWindow def ispythonsource(self, filename): + "Control highlighting of Python source." # No colorization needed - return 0 + return False def short_title(self): + "Customize EditorWindow title." return "Output" def maybesave(self): + "Customize EditorWindow to not display save file messagebox." # Override base class method -- don't ask any questions if self.get_saved(): return "yes" @@ -38,6 +44,21 @@ def maybesave(self): # Act as output file def write(self, s, tags=(), mark="insert"): + """Write text to text widget. + + The text is inserted at the given index with the provided + tags. The text widget is then scrolled to make it visible + and updated to display it, giving the effect of seeing each + line as it is added. + + Args: + s: Text to insert into text widget. + tags: Tuple of tag strings to apply on the insert. + mark: Index for the insert. + + Return: + Length of text inserted. + """ if isinstance(s, (bytes, bytes)): s = s.decode(iomenu.encoding, "replace") self.text.insert(mark, s, tags) @@ -46,10 +67,15 @@ def write(self, s, tags=(), mark="insert"): return len(s) def writelines(self, lines): + "Write each item in lines iterable." for line in lines: self.write(line) def flush(self): + """Flush file. + + Provide file flush functionality for the window. + """ pass # Our own right-button menu @@ -74,6 +100,14 @@ def flush(self): file_line_progs = None def goto_file_line(self, event=None): + """Handle request to open file/line. + + If the selected or previous line in the output window + contains a file name and line number, then open that file + name in a new window and position on the line number. + + Otherwise, display an error messagebox. + """ if self.file_line_progs is None: l = [] for pat in self.file_line_pats: @@ -95,12 +129,20 @@ def goto_file_line(self, event=None): "The line you point at doesn't look like " "a valid file name followed by a line number.", parent=self.text) - return + return None filename, lineno = result edit = self.flist.open(filename) edit.gotoline(lineno) + return None def _file_line_helper(self, line): + """Extract file name and line number from line of text. + + Check if line of text contains one of the file/line patterns. + If it does and if the file and line are valid, return + a tuple of the file name and line number. If it doesn't match + or if the file or line is invalid, return None. + """ for prog in self.file_line_progs: match = prog.search(line) if match: @@ -118,8 +160,8 @@ def _file_line_helper(self, line): except TypeError: return None -# These classes are currently not used but might come in handy +# These classes are currently not used but might come in handy class OnDemandOutputWindow: tagdefs = { From 997dce2614b29af87a2f2999ba46b4dfbdfb8542 Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Mon, 12 Jun 2017 09:33:42 -0400 Subject: [PATCH 2/6] bpo-30617: IDLE: docstrings and unittest for outwin.py --- Lib/idlelib/idle_test/test_outwin.py | 144 +++++++++++++++++++++++---- Lib/idlelib/outwin.py | 135 ++++++++++++------------- 2 files changed, 193 insertions(+), 86 deletions(-) diff --git a/Lib/idlelib/idle_test/test_outwin.py b/Lib/idlelib/idle_test/test_outwin.py index 6cd7a3c99fec9d..b3030e14df1924 100644 --- a/Lib/idlelib/idle_test/test_outwin.py +++ b/Lib/idlelib/idle_test/test_outwin.py @@ -3,33 +3,139 @@ import unittest import unittest.mock as mock -import re +from tkinter import Tk, Text +from idlelib.idle_test.mock_tk import Mbox_func + +from idlelib import filelist from idlelib import outwin +from test.support import requires +requires('gui') + + +class OutputWindowTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + cls.root.withdraw() + cls.window = outwin.OutputWindow(None, None, None, cls.root) + cls.text = Text(cls.root) + cls.window.text = cls.text + + @classmethod + def tearDownClass(cls): + del cls.text, cls.window + cls.root.destroy() + del cls.root + + def tearDown(self): + self.text.delete('1.0', 'end') + + def test_window_title(self): + self.assertEqual(self.window.top.title(), "Output") + + def test_ispythonsource(self): + self.assertFalse(self.window.ispythonsource("test.py")) + + def test_maybesave(self): + self.window.get_saved = mock.Mock(return_value=False) + self.assertEqual(self.window.maybesave(), "no") + self.window.get_saved.assert_called_once() + + def test_write(self): + text = self.text + write = self.window.write + test_text = "test text\nLine 2" + self.assertEqual(write(test_text), len(test_text)) + self.assertEqual(text.get('1.0', '1.end'), "test text") + self.assertEqual(text.get('insert linestart', 'insert lineend'), + 'Line 2') + + def test_writelines(self): + text = self.text + writelines = self.window.writelines + writelines(["Line 1\n", "Line 2\n", "Line 3\n"]) + self.assertEqual(text.get('1.0', '1.end'), "Line 1") + self.assertEqual(text.get('3.0', '3.end'), "Line 3") + + +class GotoFileLineTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + cls.root.withdraw() + cls.orig_error = outwin.messagebox.showerror + outwin.messagebox.showerror = Mbox_func() + cls.flist = filelist.FileList(cls.root) + cls.window = outwin.OutputWindow(cls.flist, None, None, None) + cls.text = Text(cls.root) + cls.window.text = cls.text + + @classmethod + def tearDownClass(cls): + outwin.messagebox.showerror = cls.orig_error + del cls.text, cls.window, cls.flist, cls.orig_error + cls.root.destroy() + del cls.root + + def tearDown(self): + self.window.text.delete('1.0', 'end') + + def test_curr_line(self): + text = self.text + goto_file_line = self.window.goto_file_line + self.window.flist.gotofileline = mock.Mock() + + text.insert('insert', '{}: 42: spam\n'.format(str(__file__))) + text.insert('insert', '{}: 21: spam'.format(str(__file__))) + self.assertIsNone(goto_file_line()) + self.window.flist.gotofileline.assert_called_with(str(__file__), 21) + + def test_no_line(self): + text = self.text + goto_file_line = self.window.goto_file_line + self.window.flist.gotofileline = mock.Mock() + + text.insert('insert', 'Not a file line') + self.assertIsNone(goto_file_line()) + self.window.flist.gotofileline.assert_not_called() + self.assertEqual(outwin.messagebox.showerror.title, 'No special line') + + def test_prev_line(self): + text = self.text + goto_file_line = self.window.goto_file_line + self.window.flist.gotofileline = mock.Mock() + text.insert('insert', '{}: 42: spam\n'.format(str(__file__))) + text.insert('insert', 'Not a file line') + self.assertIsNone(goto_file_line()) + self.window.flist.gotofileline.assert_called_with(str(__file__), 42) -class DummyOutputWindow: - _file_line_helper = outwin.OutputWindow._file_line_helper - goto_file_line = outwin.OutputWindow.goto_file_line - file_line_pats = outwin.OutputWindow.file_line_pats +class ModuleFunctionTest(unittest.TestCase): -class OutputWindowFunctionTest(unittest.TestCase): + def test_compile_progs(self): + outwin.compile_progs() + for pat, regex in zip(outwin.file_line_pats, outwin.file_line_progs): + self.assertEqual(regex.pattern, pat) @mock.patch('builtins.open') def test_file_line_helper(self, mock_open): - dummy = DummyOutputWindow() - - l = [] - for pat in dummy.file_line_pats: - l.append(re.compile(pat, re.IGNORECASE)) - dummy.file_line_progs = l - - text = ((r'file "filepasstest1", line 42, text', ('filepasstest1', 42)), - (r'filepasstest2(42)', ('filepasstest2', 42)), - (r'filepasstest3: 42: text text\n', ('filepasstest3', 42)), - (r'filefailtest 10 text', None)) - for line, expected_output in text: - self.assertEqual(dummy._file_line_helper(line), expected_output) + file_line_helper = outwin.file_line_helper + test_lines = ( + (r'foo file "testfile1", line 42, bar', ('testfile1', 42)), + (r'foo testfile2(21) bar', ('testfile2', 21)), + (r' testfile3 : 42: foo bar\n', (' testfile3 ', 42)), + (r'foo testfile4.py :1: ', ('foo testfile4.py ', 1)), + ('testfile5: \u19D4\u19D2: ', ('testfile5', 42)), + (r'testfile6: 42', None), # only one : + (r'testfile7 42 text', None) # no separators + ) + for line, expected_output in test_lines: + self.assertEqual(file_line_helper(line), expected_output) if expected_output: mock_open.assert_called_with(expected_output[0], 'r') diff --git a/Lib/idlelib/outwin.py b/Lib/idlelib/outwin.py index 08988e59e9ee08..f733da6aaeaf8e 100644 --- a/Lib/idlelib/outwin.py +++ b/Lib/idlelib/outwin.py @@ -3,13 +3,59 @@ import re -from tkinter import * -import tkinter.messagebox as tkMessageBox +from tkinter import messagebox from idlelib.editor import EditorWindow from idlelib import iomenu +file_line_pats = [ + # order of patterns matters + r'file "([^"]*)", line (\d+)', + r'([^\s]+)\((\d+)\)', + r'^(\s*\S.*?):\s*(\d+):', # Win filename, maybe starting with spaces + r'([^\s]+):\s*(\d+):', # filename or path, ltrim + r'^\s*(\S.*?):\s*(\d+):', # Win abs path with embedded spaces, ltrim +] + +file_line_progs = None + + +def compile_progs(): + "Compile the patterns for matching to file name and line number." + global file_line_progs + file_line_progs = [re.compile(pat, re.IGNORECASE) + for pat in file_line_pats] + + +def file_line_helper(line): + """Extract file name and line number from line of text. + + Check if line of text contains one of the file/line patterns. + If it does and if the file and line are valid, return + a tuple of the file name and line number. If it doesn't match + or if the file or line is invalid, return None. + """ + if not file_line_progs: + compile_progs() + for prog in file_line_progs: + match = prog.search(line) + if match: + filename, lineno = match.group(1, 2) + try: + f = open(filename, "r") + f.close() + break + except OSError: + continue + else: + return None + try: + return filename, int(lineno) + except TypeError: + return None + + class OutputWindow(EditorWindow): """An editor window that can serve as an output file. @@ -18,14 +64,23 @@ class OutputWindow(EditorWindow): Adds binding to open a file at a line to the text widget. """ + + # Our own right-button menu + rmenu_specs = [ + ("Cut", "<>", "rmenu_check_cut"), + ("Copy", "<>", "rmenu_check_copy"), + ("Paste", "<>", "rmenu_check_paste"), + (None, None, None), + ("Go to file/line", "<>", None), + ] + def __init__(self, *args): EditorWindow.__init__(self, *args) self.text.bind("<>", self.goto_file_line) # Customize EditorWindow - def ispythonsource(self, filename): - "Control highlighting of Python source." + "Control colorization of Python source." # No colorization needed return False @@ -36,13 +91,9 @@ def short_title(self): def maybesave(self): "Customize EditorWindow to not display save file messagebox." # Override base class method -- don't ask any questions - if self.get_saved(): - return "yes" - else: - return "no" + return 'yes' if self.get_saved() else 'no' # Act as output file - def write(self, s, tags=(), mark="insert"): """Write text to text widget. @@ -78,27 +129,6 @@ def flush(self): """ pass - # Our own right-button menu - - rmenu_specs = [ - ("Cut", "<>", "rmenu_check_cut"), - ("Copy", "<>", "rmenu_check_copy"), - ("Paste", "<>", "rmenu_check_paste"), - (None, None, None), - ("Go to file/line", "<>", None), - ] - - file_line_pats = [ - # order of patterns matters - r'file "([^"]*)", line (\d+)', - r'([^\s]+)\((\d+)\)', - r'^(\s*\S.*?):\s*(\d+):', # Win filename, maybe starting with spaces - r'([^\s]+):\s*(\d+):', # filename or path, ltrim - r'^\s*(\S.*?):\s*(\d+):', # Win abs path with embedded spaces, ltrim - ] - - file_line_progs = None - def goto_file_line(self, event=None): """Handle request to open file/line. @@ -108,58 +138,25 @@ def goto_file_line(self, event=None): Otherwise, display an error messagebox. """ - if self.file_line_progs is None: - l = [] - for pat in self.file_line_pats: - l.append(re.compile(pat, re.IGNORECASE)) - self.file_line_progs = l - # x, y = self.event.x, self.event.y - # self.text.mark_set("insert", "@%d,%d" % (x, y)) line = self.text.get("insert linestart", "insert lineend") - result = self._file_line_helper(line) + result = file_line_helper(line) if not result: # Try the previous line. This is handy e.g. in tracebacks, # where you tend to right-click on the displayed source line line = self.text.get("insert -1line linestart", "insert -1line lineend") - result = self._file_line_helper(line) + result = file_line_helper(line) if not result: - tkMessageBox.showerror( + messagebox.showerror( "No special line", "The line you point at doesn't look like " "a valid file name followed by a line number.", parent=self.text) return None filename, lineno = result - edit = self.flist.open(filename) - edit.gotoline(lineno) + self.flist.gotofileline(filename, lineno) return None - def _file_line_helper(self, line): - """Extract file name and line number from line of text. - - Check if line of text contains one of the file/line patterns. - If it does and if the file and line are valid, return - a tuple of the file name and line number. If it doesn't match - or if the file or line is invalid, return None. - """ - for prog in self.file_line_progs: - match = prog.search(line) - if match: - filename, lineno = match.group(1, 2) - try: - f = open(filename, "r") - f.close() - break - except OSError: - continue - else: - return None - try: - return filename, int(lineno) - except TypeError: - return None - # These classes are currently not used but might come in handy class OnDemandOutputWindow: @@ -187,3 +184,7 @@ def setup(self): text.tag_configure(tag, **cnf) text.tag_raise('sel') self.write = self.owin.write + +if __name__ == '__main__': + import unittest + unittest.main('idlelib.idle_test.test_outwin', verbosity=2, exit=False) From 7094e9568104f772327339470b3a18f7909c097a Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Wed, 14 Jun 2017 11:37:23 -0400 Subject: [PATCH 3/6] bpo-30617: IDLE: docstrings and unittest for outwin.py --- Lib/idlelib/idle_test/test_outwin.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/Lib/idlelib/idle_test/test_outwin.py b/Lib/idlelib/idle_test/test_outwin.py index b3030e14df1924..4ae1ef391d2c97 100644 --- a/Lib/idlelib/idle_test/test_outwin.py +++ b/Lib/idlelib/idle_test/test_outwin.py @@ -9,7 +9,6 @@ from idlelib import filelist from idlelib import outwin from test.support import requires -requires('gui') class OutputWindowTest(unittest.TestCase): @@ -33,31 +32,34 @@ def tearDown(self): self.text.delete('1.0', 'end') def test_window_title(self): - self.assertEqual(self.window.top.title(), "Output") + self.assertEqual(self.window.top.title(), 'Output') def test_ispythonsource(self): - self.assertFalse(self.window.ispythonsource("test.py")) + "OutputWindow overrides ispythonsource to always return False." + self.assertFalse(self.window.ispythonsource('test.txt')) + self.assertFalse(self.window.ispythonsource(__file__)) def test_maybesave(self): self.window.get_saved = mock.Mock(return_value=False) - self.assertEqual(self.window.maybesave(), "no") + self.assertEqual(self.window.maybesave(), 'no') self.window.get_saved.assert_called_once() + del self.window.get_saved def test_write(self): text = self.text write = self.window.write - test_text = "test text\nLine 2" + test_text = 'test text\nLine 2' self.assertEqual(write(test_text), len(test_text)) - self.assertEqual(text.get('1.0', '1.end'), "test text") + self.assertEqual(text.get('1.0', '1.end'), 'test text') self.assertEqual(text.get('insert linestart', 'insert lineend'), 'Line 2') def test_writelines(self): text = self.text writelines = self.window.writelines - writelines(["Line 1\n", "Line 2\n", "Line 3\n"]) - self.assertEqual(text.get('1.0', '1.end'), "Line 1") - self.assertEqual(text.get('3.0', '3.end'), "Line 3") + writelines(['Line 1\n', 'Line 2\n', 'Line 3\n']) + self.assertEqual(text.get('1.0', '1.end'), 'Line 1') + self.assertEqual(text.get('3.0', '3.end'), 'Line 3') class GotoFileLineTest(unittest.TestCase): @@ -81,13 +83,16 @@ def tearDownClass(cls): cls.root.destroy() del cls.root + def setUp(self): + self.window.flist.gotofileline = mock.Mock() + def tearDown(self): self.window.text.delete('1.0', 'end') + del self.window.flist.gotofileline def test_curr_line(self): text = self.text goto_file_line = self.window.goto_file_line - self.window.flist.gotofileline = mock.Mock() text.insert('insert', '{}: 42: spam\n'.format(str(__file__))) text.insert('insert', '{}: 21: spam'.format(str(__file__))) @@ -97,7 +102,6 @@ def test_curr_line(self): def test_no_line(self): text = self.text goto_file_line = self.window.goto_file_line - self.window.flist.gotofileline = mock.Mock() text.insert('insert', 'Not a file line') self.assertIsNone(goto_file_line()) @@ -107,7 +111,6 @@ def test_no_line(self): def test_prev_line(self): text = self.text goto_file_line = self.window.goto_file_line - self.window.flist.gotofileline = mock.Mock() text.insert('insert', '{}: 42: spam\n'.format(str(__file__))) text.insert('insert', 'Not a file line') From 5ae9b9c4d8b8d4dcd7c5de24bdf1b486b296b466 Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Sat, 26 Aug 2017 12:42:42 -0400 Subject: [PATCH 4/6] Improve tests. --- Lib/idlelib/idle_test/test_outwin.py | 180 +++++++++++++++------------ 1 file changed, 102 insertions(+), 78 deletions(-) diff --git a/Lib/idlelib/idle_test/test_outwin.py b/Lib/idlelib/idle_test/test_outwin.py index 4ae1ef391d2c97..8bc2670941d8b6 100644 --- a/Lib/idlelib/idle_test/test_outwin.py +++ b/Lib/idlelib/idle_test/test_outwin.py @@ -2,13 +2,12 @@ """ import unittest -import unittest.mock as mock from tkinter import Tk, Text from idlelib.idle_test.mock_tk import Mbox_func - -from idlelib import filelist +from idlelib.idle_test.mock_idle import Func from idlelib import outwin from test.support import requires +from unittest import mock class OutputWindowTest(unittest.TestCase): @@ -16,11 +15,10 @@ class OutputWindowTest(unittest.TestCase): @classmethod def setUpClass(cls): requires('gui') - cls.root = Tk() - cls.root.withdraw() - cls.window = outwin.OutputWindow(None, None, None, cls.root) - cls.text = Text(cls.root) - cls.window.text = cls.text + root = cls.root = Tk() + root.withdraw() + w = cls.window = outwin.OutputWindow(None, None, None, root) + cls.text = w.text = Text(root) @classmethod def tearDownClass(cls): @@ -28,98 +26,124 @@ def tearDownClass(cls): cls.root.destroy() del cls.root - def tearDown(self): + def setUp(self): self.text.delete('1.0', 'end') + def test_ispythonsource(self): + # OutputWindow overrides ispythonsource to always return False. + w = self.window + self.assertFalse(w.ispythonsource('test.txt')) + self.assertFalse(w.ispythonsource(__file__)) + def test_window_title(self): self.assertEqual(self.window.top.title(), 'Output') - def test_ispythonsource(self): - "OutputWindow overrides ispythonsource to always return False." - self.assertFalse(self.window.ispythonsource('test.txt')) - self.assertFalse(self.window.ispythonsource(__file__)) - def test_maybesave(self): - self.window.get_saved = mock.Mock(return_value=False) - self.assertEqual(self.window.maybesave(), 'no') - self.window.get_saved.assert_called_once() - del self.window.get_saved + w = self.window + eq = self.assertEqual + w.get_saved = Func() + + w.get_saved.result = False + eq(w.maybesave(), 'no') + eq(w.get_saved.called, 1) + + w.get_saved.result = True + eq(w.maybesave(), 'yes') + eq(w.get_saved.called, 2) + del w.get_saved def test_write(self): - text = self.text + eq = self.assertEqual + delete = self.text.delete + get = self.text.get write = self.window.write + + # Test bytes. + b = b'Test bytes.' + eq(write(b), len(b)) + eq(get('1.0', '1.end'), b.decode()) + + # No new line - insert stays on same line. + delete('1.0', 'end') + test_text = 'test text' + eq(write(test_text), len(test_text)) + eq(get('1.0', '1.end'), 'test text') + eq(get('insert linestart', 'insert lineend'), 'test text') + + # New line - insert moves to next line. + delete('1.0', 'end') + test_text = 'test text\n' + eq(write(test_text), len(test_text)) + eq(get('1.0', '1.end'), 'test text') + eq(get('insert linestart', 'insert lineend'), '') + + # Text after new line is tagged for second line of Text widget. + delete('1.0', 'end') test_text = 'test text\nLine 2' - self.assertEqual(write(test_text), len(test_text)) - self.assertEqual(text.get('1.0', '1.end'), 'test text') - self.assertEqual(text.get('insert linestart', 'insert lineend'), - 'Line 2') + eq(write(test_text), len(test_text)) + eq(get('1.0', '1.end'), 'test text') + eq(get('2.0', '2.end'), 'Line 2') + eq(get('insert linestart', 'insert lineend'), 'Line 2') + + # Test tags. + delete('1.0', 'end') + test_text = 'test text\n' + test_text2 = 'Line 2\n' + eq(write(test_text, tags='mytag'), len(test_text)) + eq(write(test_text2, tags='secondtag'), len(test_text2)) + eq(get('mytag.first', 'mytag.last'), test_text) + eq(get('secondtag.first', 'secondtag.last'), test_text2) + eq(get('1.0', '1.end'), test_text.rstrip('\n')) + eq(get('2.0', '2.end'), test_text2.rstrip('\n')) def test_writelines(self): - text = self.text + eq = self.assertEqual + get = self.text.get writelines = self.window.writelines - writelines(['Line 1\n', 'Line 2\n', 'Line 3\n']) - self.assertEqual(text.get('1.0', '1.end'), 'Line 1') - self.assertEqual(text.get('3.0', '3.end'), 'Line 3') - - -class GotoFileLineTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - cls.orig_error = outwin.messagebox.showerror - outwin.messagebox.showerror = Mbox_func() - cls.flist = filelist.FileList(cls.root) - cls.window = outwin.OutputWindow(cls.flist, None, None, None) - cls.text = Text(cls.root) - cls.window.text = cls.text - - @classmethod - def tearDownClass(cls): - outwin.messagebox.showerror = cls.orig_error - del cls.text, cls.window, cls.flist, cls.orig_error - cls.root.destroy() - del cls.root - - def setUp(self): - self.window.flist.gotofileline = mock.Mock() - def tearDown(self): - self.window.text.delete('1.0', 'end') - del self.window.flist.gotofileline + writelines(('Line 1\n', 'Line 2\n', 'Line 3\n')) + eq(get('1.0', '1.end'), 'Line 1') + eq(get('2.0', '2.end'), 'Line 2') + eq(get('3.0', '3.end'), 'Line 3') + eq(get('insert linestart', 'insert lineend'), '') - def test_curr_line(self): + def test_goto_file_line(self): + eq = self.assertEqual + w = self.window text = self.text - goto_file_line = self.window.goto_file_line - text.insert('insert', '{}: 42: spam\n'.format(str(__file__))) - text.insert('insert', '{}: 21: spam'.format(str(__file__))) - self.assertIsNone(goto_file_line()) - self.window.flist.gotofileline.assert_called_with(str(__file__), 21) + w.flist = mock.Mock() + gfl = w.flist.gotofileline = Func() + showerror = outwin.messagebox.showerror = Mbox_func() - def test_no_line(self): - text = self.text - goto_file_line = self.window.goto_file_line + # No file/line number. + w.write('Not a file line') + self.assertIsNone(w.goto_file_line()) + eq(gfl.called, 0) + eq(showerror.title, 'No special line') - text.insert('insert', 'Not a file line') - self.assertIsNone(goto_file_line()) - self.window.flist.gotofileline.assert_not_called() - self.assertEqual(outwin.messagebox.showerror.title, 'No special line') + # Current file/line number. + w.write(f'{str(__file__)}: 42: spam\n') + w.write(f'{str(__file__)}: 21: spam') + self.assertIsNone(w.goto_file_line()) + eq(gfl.args, (str(__file__), 21)) - def test_prev_line(self): - text = self.text - goto_file_line = self.window.goto_file_line + # Previous line has file/line number. + text.delete('1.0', 'end') + w.write(f'{str(__file__)}: 42: spam\n') + w.write('Not a file line') + self.assertIsNone(w.goto_file_line()) + eq(gfl.args, (str(__file__), 42)) - text.insert('insert', '{}: 42: spam\n'.format(str(__file__))) - text.insert('insert', 'Not a file line') - self.assertIsNone(goto_file_line()) - self.window.flist.gotofileline.assert_called_with(str(__file__), 42) + del w.flist.gotofileline, outwin.messagebox.showerror class ModuleFunctionTest(unittest.TestCase): + @classmethod + def setUp(cls): + outwin.file_line_progs = None + def test_compile_progs(self): outwin.compile_progs() for pat, regex in zip(outwin.file_line_pats, outwin.file_line_progs): @@ -127,18 +151,18 @@ def test_compile_progs(self): @mock.patch('builtins.open') def test_file_line_helper(self, mock_open): - file_line_helper = outwin.file_line_helper + flh = outwin.file_line_helper test_lines = ( (r'foo file "testfile1", line 42, bar', ('testfile1', 42)), (r'foo testfile2(21) bar', ('testfile2', 21)), (r' testfile3 : 42: foo bar\n', (' testfile3 ', 42)), (r'foo testfile4.py :1: ', ('foo testfile4.py ', 1)), ('testfile5: \u19D4\u19D2: ', ('testfile5', 42)), - (r'testfile6: 42', None), # only one : + (r'testfile6: 42', None), # only one `:` (r'testfile7 42 text', None) # no separators ) for line, expected_output in test_lines: - self.assertEqual(file_line_helper(line), expected_output) + self.assertEqual(flh(line), expected_output) if expected_output: mock_open.assert_called_with(expected_output[0], 'r') From 97277c5e49a1a0f63cbd85191907cbe787497eba Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Sun, 27 Aug 2017 16:49:53 -0400 Subject: [PATCH 5/6] News blurb --- .../NEWS.d/next/IDLE/2017-08-27-16-49-36.bpo-30617.UHnswr.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/IDLE/2017-08-27-16-49-36.bpo-30617.UHnswr.rst diff --git a/Misc/NEWS.d/next/IDLE/2017-08-27-16-49-36.bpo-30617.UHnswr.rst b/Misc/NEWS.d/next/IDLE/2017-08-27-16-49-36.bpo-30617.UHnswr.rst new file mode 100644 index 00000000000000..262674c32dcf05 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2017-08-27-16-49-36.bpo-30617.UHnswr.rst @@ -0,0 +1,4 @@ +IDLE - Add docstrings and tests for outwin subclass of editor. + +Move some data and functions from the class to module level. Patch by Cheryl +Sabella. From ca95099e4d2d2eaafd329b89e583ddb88032a21c Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Sun, 27 Aug 2017 17:13:18 -0400 Subject: [PATCH 6/6] Make changes I requested. --- Lib/idlelib/idle_test/test_outwin.py | 5 +++-- Lib/idlelib/outwin.py | 17 +++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Lib/idlelib/idle_test/test_outwin.py b/Lib/idlelib/idle_test/test_outwin.py index 8bc2670941d8b6..231c7bf9cfb620 100644 --- a/Lib/idlelib/idle_test/test_outwin.py +++ b/Lib/idlelib/idle_test/test_outwin.py @@ -22,6 +22,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): + cls.window.close() del cls.text, cls.window cls.root.destroy() del cls.root @@ -114,7 +115,7 @@ def test_goto_file_line(self): w.flist = mock.Mock() gfl = w.flist.gotofileline = Func() - showerror = outwin.messagebox.showerror = Mbox_func() + showerror = w.showerror = Mbox_func() # No file/line number. w.write('Not a file line') @@ -135,7 +136,7 @@ def test_goto_file_line(self): self.assertIsNone(w.goto_file_line()) eq(gfl.args, (str(__file__), 42)) - del w.flist.gotofileline, outwin.messagebox.showerror + del w.flist.gotofileline, w.showerror class ModuleFunctionTest(unittest.TestCase): diff --git a/Lib/idlelib/outwin.py b/Lib/idlelib/outwin.py index f733da6aaeaf8e..5f7c09fb92cdad 100644 --- a/Lib/idlelib/outwin.py +++ b/Lib/idlelib/outwin.py @@ -80,8 +80,7 @@ def __init__(self, *args): # Customize EditorWindow def ispythonsource(self, filename): - "Control colorization of Python source." - # No colorization needed + "Python source is only part of output: do not colorize." return False def short_title(self): @@ -90,7 +89,6 @@ def short_title(self): def maybesave(self): "Customize EditorWindow to not display save file messagebox." - # Override base class method -- don't ask any questions return 'yes' if self.get_saved() else 'no' # Act as output file @@ -123,12 +121,12 @@ def writelines(self, lines): self.write(line) def flush(self): - """Flush file. - - Provide file flush functionality for the window. - """ + "No flushing needed as write() directly writes to widget." pass + def showerror(self, *args, **kwargs): + messagebox.showerror(*args, **kwargs) + def goto_file_line(self, event=None): """Handle request to open file/line. @@ -147,15 +145,14 @@ def goto_file_line(self, event=None): "insert -1line lineend") result = file_line_helper(line) if not result: - messagebox.showerror( + self.showerror( "No special line", "The line you point at doesn't look like " "a valid file name followed by a line number.", parent=self.text) - return None + return filename, lineno = result self.flist.gotofileline(filename, lineno) - return None # These classes are currently not used but might come in handy