From 998f4966bf0c591f3e8b3d07eccad7501f60f524 Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Sun, 27 Aug 2017 18:06:00 -0400 Subject: bpo-30617: IDLE: docstrings and unittest for outwin.py (#2046) Move some data and functions from the class to module level. Patch by Cheryl Sabella. --- Lib/idlelib/idle_test/test_outwin.py | 172 +++++++++++++++++++++ Lib/idlelib/outwin.py | 164 ++++++++++++-------- .../IDLE/2017-08-27-16-49-36.bpo-30617.UHnswr.rst | 4 + 3 files changed, 278 insertions(+), 62 deletions(-) create mode 100644 Lib/idlelib/idle_test/test_outwin.py create mode 100644 Misc/NEWS.d/next/IDLE/2017-08-27-16-49-36.bpo-30617.UHnswr.rst diff --git a/Lib/idlelib/idle_test/test_outwin.py b/Lib/idlelib/idle_test/test_outwin.py new file mode 100644 index 0000000..231c7bf --- /dev/null +++ b/Lib/idlelib/idle_test/test_outwin.py @@ -0,0 +1,172 @@ +""" Test idlelib.outwin. +""" + +import unittest +from tkinter import Tk, Text +from idlelib.idle_test.mock_tk import Mbox_func +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): + + @classmethod + def setUpClass(cls): + requires('gui') + 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): + cls.window.close() + del cls.text, cls.window + cls.root.destroy() + del cls.root + + 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_maybesave(self): + 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): + 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' + 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): + eq = self.assertEqual + get = self.text.get + writelines = self.window.writelines + + 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_goto_file_line(self): + eq = self.assertEqual + w = self.window + text = self.text + + w.flist = mock.Mock() + gfl = w.flist.gotofileline = Func() + showerror = w.showerror = Mbox_func() + + # 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') + + # 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)) + + # 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)) + + del w.flist.gotofileline, w.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): + self.assertEqual(regex.pattern, pat) + + @mock.patch('builtins.open') + def test_file_line_helper(self, mock_open): + 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'testfile7 42 text', None) # no separators + ) + for line, expected_output in test_lines: + self.assertEqual(flh(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 f6d2915..5f7c09f 100644 --- a/Lib/idlelib/outwin.py +++ b/Lib/idlelib/outwin.py @@ -1,43 +1,113 @@ +"""Editor window that can serve as an output file. +""" + import re -from tkinter import * -import tkinter.messagebox as tkMessageBox +from tkinter import messagebox from idlelib.editor import EditorWindow from idlelib import iomenu -class OutputWindow(EditorWindow): +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. 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. """ + # 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): - # No colorization needed - return 0 + "Python source is only part of output: do not colorize." + return False def short_title(self): + "Customize EditorWindow title." return "Output" def maybesave(self): - # Override base class method -- don't ask any questions - if self.get_saved(): - return "yes" - else: - return "no" + "Customize EditorWindow to not display save file messagebox." + return 'yes' if self.get_saved() else 'no' # 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,80 +116,46 @@ class OutputWindow(EditorWindow): return len(s) def writelines(self, lines): + "Write each item in lines iterable." for line in lines: self.write(line) def flush(self): + "No flushing needed as write() directly writes to widget." 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), - ] + def showerror(self, *args, **kwargs): + messagebox.showerror(*args, **kwargs) - 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 - ] + def goto_file_line(self, event=None): + """Handle request to open file/line. - file_line_progs = None + 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. - def goto_file_line(self, event=None): - 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)) + Otherwise, display an error messagebox. + """ 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( + 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 filename, lineno = result - edit = self.flist.open(filename) - edit.gotoline(lineno) - - def _file_line_helper(self, line): - 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 + self.flist.gotofileline(filename, lineno) -# 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 = { @@ -145,3 +181,7 @@ class OnDemandOutputWindow: 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) 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 0000000..262674c --- /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. -- cgit v0.12