Commit 998f4966 authored by Cheryl Sabella's avatar Cheryl Sabella Committed by Terry Jan Reedy

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.
parent 3457f428
""" 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)
"""Editor window that can serve as an output file.
"""
import re import re
from tkinter import * from tkinter import messagebox
import tkinter.messagebox as tkMessageBox
from idlelib.editor import EditorWindow from idlelib.editor import EditorWindow
from idlelib import iomenu 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. """An editor window that can serve as an output file.
Also the future base class for the Python shell window. Also the future base class for the Python shell window.
This class has no input facilities. 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", "<<cut>>", "rmenu_check_cut"),
("Copy", "<<copy>>", "rmenu_check_copy"),
("Paste", "<<paste>>", "rmenu_check_paste"),
(None, None, None),
("Go to file/line", "<<goto-file-line>>", None),
]
def __init__(self, *args): def __init__(self, *args):
EditorWindow.__init__(self, *args) EditorWindow.__init__(self, *args)
self.text.bind("<<goto-file-line>>", self.goto_file_line) self.text.bind("<<goto-file-line>>", self.goto_file_line)
# Customize EditorWindow # Customize EditorWindow
def ispythonsource(self, filename): def ispythonsource(self, filename):
# No colorization needed "Python source is only part of output: do not colorize."
return 0 return False
def short_title(self): def short_title(self):
"Customize EditorWindow title."
return "Output" return "Output"
def maybesave(self): def maybesave(self):
# Override base class method -- don't ask any questions "Customize EditorWindow to not display save file messagebox."
if self.get_saved(): return 'yes' if self.get_saved() else 'no'
return "yes"
else:
return "no"
# Act as output file # Act as output file
def write(self, s, tags=(), mark="insert"): 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)): if isinstance(s, (bytes, bytes)):
s = s.decode(iomenu.encoding, "replace") s = s.decode(iomenu.encoding, "replace")
self.text.insert(mark, s, tags) self.text.insert(mark, s, tags)
...@@ -46,80 +116,46 @@ class OutputWindow(EditorWindow): ...@@ -46,80 +116,46 @@ class OutputWindow(EditorWindow):
return len(s) return len(s)
def writelines(self, lines): def writelines(self, lines):
"Write each item in lines iterable."
for line in lines: for line in lines:
self.write(line) self.write(line)
def flush(self): def flush(self):
"No flushing needed as write() directly writes to widget."
pass pass
# Our own right-button menu def showerror(self, *args, **kwargs):
messagebox.showerror(*args, **kwargs)
rmenu_specs = [
("Cut", "<<cut>>", "rmenu_check_cut"),
("Copy", "<<copy>>", "rmenu_check_copy"),
("Paste", "<<paste>>", "rmenu_check_paste"),
(None, None, None),
("Go to file/line", "<<goto-file-line>>", None),
]
file_line_pats = [ def goto_file_line(self, event=None):
# order of patterns matters """Handle request to open file/line.
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 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): 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") line = self.text.get("insert linestart", "insert lineend")
result = self._file_line_helper(line) result = file_line_helper(line)
if not result: if not result:
# Try the previous line. This is handy e.g. in tracebacks, # Try the previous line. This is handy e.g. in tracebacks,
# where you tend to right-click on the displayed source line # where you tend to right-click on the displayed source line
line = self.text.get("insert -1line linestart", line = self.text.get("insert -1line linestart",
"insert -1line lineend") "insert -1line lineend")
result = self._file_line_helper(line) result = file_line_helper(line)
if not result: if not result:
tkMessageBox.showerror( self.showerror(
"No special line", "No special line",
"The line you point at doesn't look like " "The line you point at doesn't look like "
"a valid file name followed by a line number.", "a valid file name followed by a line number.",
parent=self.text) parent=self.text)
return return
filename, lineno = result filename, lineno = result
edit = self.flist.open(filename) self.flist.gotofileline(filename, lineno)
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
# These classes are currently not used but might come in handy
# These classes are currently not used but might come in handy
class OnDemandOutputWindow: class OnDemandOutputWindow:
tagdefs = { tagdefs = {
...@@ -145,3 +181,7 @@ class OnDemandOutputWindow: ...@@ -145,3 +181,7 @@ class OnDemandOutputWindow:
text.tag_configure(tag, **cnf) text.tag_configure(tag, **cnf)
text.tag_raise('sel') text.tag_raise('sel')
self.write = self.owin.write self.write = self.owin.write
if __name__ == '__main__':
import unittest
unittest.main('idlelib.idle_test.test_outwin', verbosity=2, exit=False)
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.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment