Kaydet (Commit) 42bc8bea authored tarafından csabella's avatar csabella Kaydeden (comit) terryjreedy

bpo-30495: IDLE: improve textview with docstrings, PEP8 names, more tests. (#2283)

Split TextViewer class into ViewWindow, ViewFrame, and TextFrame classes so that instances
of the latter two can be placed with other widgets within a multiframe window.
Patch by Cheryl Sabella.
üst 18974c35
...@@ -8,7 +8,7 @@ callable in the module named in the spec. Close the window to skip or ...@@ -8,7 +8,7 @@ callable in the module named in the spec. Close the window to skip or
end the test. end the test.
In a tested module, let X be a global name bound to a callable (class In a tested module, let X be a global name bound to a callable (class
or function) whose .__name__ attrubute is also X (the usual situation). or function) whose .__name__ attribute is also X (the usual situation).
The first parameter of X must be 'parent'. When called, the parent The first parameter of X must be 'parent'. When called, the parent
argument will be the root window. X must create a child Toplevel argument will be the root window. X must create a child Toplevel
window (or subclass thereof). The Toplevel may be a test widget or window (or subclass thereof). The Toplevel may be a test widget or
...@@ -306,15 +306,6 @@ _tabbed_pages_spec = { ...@@ -306,15 +306,6 @@ _tabbed_pages_spec = {
"<nothing> is an invalid add page and remove page name.\n" "<nothing> is an invalid add page and remove page name.\n"
} }
TextViewer_spec = {
'file': 'textview',
'kwds': {'title': 'Test textview',
'text':'The quick brown fox jumps over the lazy dog.\n'*35,
'_htest': True},
'msg': "Test for read-only property of text.\n"
"Text is selectable. Window is scrollable.",
}
_tooltip_spec = { _tooltip_spec = {
'file': 'tooltip', 'file': 'tooltip',
'kwds': {}, 'kwds': {},
...@@ -338,6 +329,15 @@ _undo_delegator_spec = { ...@@ -338,6 +329,15 @@ _undo_delegator_spec = {
"by printing to the console or the IDLE shell.\n" "by printing to the console or the IDLE shell.\n"
} }
ViewWindow_spec = {
'file': 'textview',
'kwds': {'title': 'Test textview',
'text': 'The quick brown fox jumps over the lazy dog.\n'*35,
'_htest': True},
'msg': "Test for read-only property of text.\n"
"Select text, scroll window, close"
}
_widget_redirector_spec = { _widget_redirector_spec = {
'file': 'redirector', 'file': 'redirector',
'kwds': {}, 'kwds': {},
......
...@@ -50,19 +50,17 @@ class LiveDialogTest(unittest.TestCase): ...@@ -50,19 +50,17 @@ class LiveDialogTest(unittest.TestCase):
def test_printer_buttons(self): def test_printer_buttons(self):
"""Test buttons whose commands use printer function.""" """Test buttons whose commands use printer function."""
dialog = self.dialog dialog = self.dialog
button_sources = [(self.dialog.py_license, license), button_sources = [(dialog.py_license, license),
(self.dialog.py_copyright, copyright), (dialog.py_copyright, copyright),
(self.dialog.py_credits, credits)] (dialog.py_credits, credits)]
for button, printer in button_sources: for button, printer in button_sources:
printer._Printer__setup() printer._Printer__setup()
button.invoke() button.invoke()
get = dialog._current_textview.viewframe.textframe.text.get
self.assertEqual(printer._Printer__lines[0], get('1.0', '1.end'))
self.assertEqual( self.assertEqual(
printer._Printer__lines[0], printer._Printer__lines[1], get('2.0', '2.end'))
dialog._current_textview.text.get('1.0', '1.end'))
self.assertEqual(
printer._Printer__lines[1],
dialog._current_textview.text.get('2.0', '2.end'))
dialog._current_textview.destroy() dialog._current_textview.destroy()
def test_file_buttons(self): def test_file_buttons(self):
...@@ -75,14 +73,11 @@ class LiveDialogTest(unittest.TestCase): ...@@ -75,14 +73,11 @@ class LiveDialogTest(unittest.TestCase):
for button, filename in button_sources: for button, filename in button_sources:
button.invoke() button.invoke()
fn = findfile(filename, subdir='idlelib') fn = findfile(filename, subdir='idlelib')
get = dialog._current_textview.viewframe.textframe.text.get
with open(fn) as f: with open(fn) as f:
self.assertEqual( self.assertEqual(f.readline().strip(), get('1.0', '1.end'))
f.readline().strip(),
dialog._current_textview.text.get('1.0', '1.end'))
f.readline() f.readline()
self.assertEqual( self.assertEqual(f.readline().strip(), get('3.0', '3.end'))
f.readline().strip(),
dialog._current_textview.text.get('3.0', '3.end'))
dialog._current_textview.destroy() dialog._current_textview.destroy()
......
'''Test idlelib.textview. '''Test idlelib.textview.
Since all methods and functions create (or destroy) a TextViewer, which Since all methods and functions create (or destroy) a ViewWindow, which
is a widget containing multiple widgets, all tests must be gui tests. is a widget containing a widget, etcetera, all tests must be gui tests.
Using mock Text would not change this. Other mocks are used to retrieve Using mock Text would not change this. Other mocks are used to retrieve
information about calls. information about calls.
...@@ -13,7 +13,8 @@ requires('gui') ...@@ -13,7 +13,8 @@ requires('gui')
import unittest import unittest
import os import os
from tkinter import Tk, Button from tkinter import Tk
from tkinter.ttk import Button
from idlelib.idle_test.mock_idle import Func from idlelib.idle_test.mock_idle import Func
from idlelib.idle_test.mock_tk import Mbox_func from idlelib.idle_test.mock_tk import Mbox_func
...@@ -25,44 +26,44 @@ def setUpModule(): ...@@ -25,44 +26,44 @@ def setUpModule():
def tearDownModule(): def tearDownModule():
global root global root
root.update_idletasks() root.update_idletasks()
root.destroy() # Pyflakes falsely sees root as undefined. root.destroy()
del root del root
# If we call TextViewer or wrapper functions with defaults # If we call ViewWindow or wrapper functions with defaults
# modal=True, _utest=False, test hangs on call to wait_window. # modal=True, _utest=False, test hangs on call to wait_window.
# Have also gotten tk error 'can't invoke "event" command'. # Have also gotten tk error 'can't invoke "event" command'.
class TV(tv.TextViewer): # Used in TextViewTest. class VW(tv.ViewWindow): # Used in ViewWindowTest.
transient = Func() transient = Func()
grab_set = Func() grab_set = Func()
wait_window = Func() wait_window = Func()
# Call wrapper class with mock wait_window. # Call wrapper class VW with mock wait_window.
class TextViewTest(unittest.TestCase): class ViewWindowTest(unittest.TestCase):
def setUp(self): def setUp(self):
TV.transient.__init__() VW.transient.__init__()
TV.grab_set.__init__() VW.grab_set.__init__()
TV.wait_window.__init__() VW.wait_window.__init__()
def test_init_modal(self): def test_init_modal(self):
view = TV(root, 'Title', 'test text') view = VW(root, 'Title', 'test text')
self.assertTrue(TV.transient.called) self.assertTrue(VW.transient.called)
self.assertTrue(TV.grab_set.called) self.assertTrue(VW.grab_set.called)
self.assertTrue(TV.wait_window.called) self.assertTrue(VW.wait_window.called)
view.ok() view.ok()
def test_init_nonmodal(self): def test_init_nonmodal(self):
view = TV(root, 'Title', 'test text', modal=False) view = VW(root, 'Title', 'test text', modal=False)
self.assertFalse(TV.transient.called) self.assertFalse(VW.transient.called)
self.assertFalse(TV.grab_set.called) self.assertFalse(VW.grab_set.called)
self.assertFalse(TV.wait_window.called) self.assertFalse(VW.wait_window.called)
view.ok() view.ok()
def test_ok(self): def test_ok(self):
view = TV(root, 'Title', 'test text', modal=False) view = VW(root, 'Title', 'test text', modal=False)
view.destroy = Func() view.destroy = Func()
view.ok() view.ok()
self.assertTrue(view.destroy.called) self.assertTrue(view.destroy.called)
...@@ -70,7 +71,28 @@ class TextViewTest(unittest.TestCase): ...@@ -70,7 +71,28 @@ class TextViewTest(unittest.TestCase):
view.destroy() view.destroy()
# Call TextViewer with modal=False. class TextFrameTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
"By itself, this tests that file parsed without exception."
cls.root = root = Tk()
root.withdraw()
cls.frame = tv.TextFrame(root, 'test text')
@classmethod
def tearDownClass(cls):
del cls.frame
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def test_line1(self):
get = self.frame.text.get
self.assertEqual(get('1.0', '1.end'), 'test text')
# Call ViewWindow with modal=False.
class ViewFunctionTest(unittest.TestCase): class ViewFunctionTest(unittest.TestCase):
@classmethod @classmethod
...@@ -85,13 +107,16 @@ class ViewFunctionTest(unittest.TestCase): ...@@ -85,13 +107,16 @@ class ViewFunctionTest(unittest.TestCase):
def test_view_text(self): def test_view_text(self):
view = tv.view_text(root, 'Title', 'test text', modal=False) view = tv.view_text(root, 'Title', 'test text', modal=False)
self.assertIsInstance(view, tv.TextViewer) self.assertIsInstance(view, tv.ViewWindow)
self.assertIsInstance(view.viewframe, tv.ViewFrame)
view.ok() view.ok()
def test_view_file(self): def test_view_file(self):
view = tv.view_file(root, 'Title', __file__, modal=False) view = tv.view_file(root, 'Title', __file__, modal=False)
self.assertIsInstance(view, tv.TextViewer) self.assertIsInstance(view, tv.ViewWindow)
self.assertIn('Test', view.text.get('1.0', '1.end')) self.assertIsInstance(view.viewframe, tv.ViewFrame)
get = view.viewframe.textframe.text.get
self.assertIn('Test', get('1.0', '1.end'))
view.ok() view.ok()
def test_bad_file(self): def test_bad_file(self):
...@@ -109,8 +134,7 @@ class ViewFunctionTest(unittest.TestCase): ...@@ -109,8 +134,7 @@ class ViewFunctionTest(unittest.TestCase):
self.assertEqual(tv.showerror.title, 'Unicode Decode Error') self.assertEqual(tv.showerror.title, 'Unicode Decode Error')
# Call ViewWindow with _utest=True.
# Call TextViewer with _utest=True.
class ButtonClickTest(unittest.TestCase): class ButtonClickTest(unittest.TestCase):
def setUp(self): def setUp(self):
...@@ -131,7 +155,8 @@ class ButtonClickTest(unittest.TestCase): ...@@ -131,7 +155,8 @@ class ButtonClickTest(unittest.TestCase):
self.assertEqual(self.called, True) self.assertEqual(self.called, True)
self.assertEqual(self.view.title(), 'TITLE_TEXT') self.assertEqual(self.view.title(), 'TITLE_TEXT')
self.assertEqual(self.view.text.get('1.0', '1.end'), 'COMMAND') self.assertEqual(self.view.viewframe.textframe.text.get('1.0', '1.end'),
'COMMAND')
def test_view_file_bind_with_button(self): def test_view_file_bind_with_button(self):
def _command(): def _command():
...@@ -143,12 +168,11 @@ class ButtonClickTest(unittest.TestCase): ...@@ -143,12 +168,11 @@ class ButtonClickTest(unittest.TestCase):
self.assertEqual(self.called, True) self.assertEqual(self.called, True)
self.assertEqual(self.view.title(), 'TITLE_FILE') self.assertEqual(self.view.title(), 'TITLE_FILE')
get = self.view.viewframe.textframe.text.get
with open(__file__) as f: with open(__file__) as f:
self.assertEqual(self.view.text.get('1.0', '1.end'), self.assertEqual(get('1.0', '1.end'), f.readline().strip())
f.readline().strip())
f.readline() f.readline()
self.assertEqual(self.view.text.get('3.0', '3.end'), self.assertEqual(get('3.0', '3.end'), f.readline().strip())
f.readline().strip())
if __name__ == '__main__': if __name__ == '__main__':
......
...@@ -892,7 +892,7 @@ class PyShell(OutputWindow): ...@@ -892,7 +892,7 @@ class PyShell(OutputWindow):
try: try:
# page help() text to shell. # page help() text to shell.
import pydoc # import must be done here to capture i/o rebinding. import pydoc # import must be done here to capture i/o rebinding.
# XXX KBK 27Dec07 use TextViewer someday, but must work w/o subproc # XXX KBK 27Dec07 use text viewer someday, but must work w/o subproc
pydoc.pager = pydoc.plainpager pydoc.pager = pydoc.plainpager
except: except:
sys.stderr = sys.__stderr__ sys.stderr = sys.__stderr__
......
"""Simple text browser for IDLE """Simple text browser for IDLE
""" """
from tkinter import Toplevel, Frame, Button, Text from tkinter import Toplevel, Text
from tkinter import DISABLED, SUNKEN, VERTICAL, WORD from tkinter.ttk import Frame, Scrollbar, Button
from tkinter import RIGHT, LEFT, TOP, BOTTOM, BOTH, X, Y
from tkinter.ttk import Scrollbar
from tkinter.messagebox import showerror from tkinter.messagebox import showerror
class TextViewer(Toplevel): class TextFrame(Frame):
"Display text with scrollbar."
def __init__(self, parent, rawtext):
"""Create a frame for Textview.
parent - parent widget for this frame
rawtext - text to display
"""
super().__init__(parent)
self['relief'] = 'sunken'
self['height'] = 700
# TODO: get fg/bg from theme.
self.bg = '#ffffff'
self.fg = '#000000'
self.text = text = Text(self, wrap='word', highlightthickness=0,
fg=self.fg, bg=self.bg)
self.scroll = scroll = Scrollbar(self, orient='vertical',
takefocus=False, command=text.yview)
text['yscrollcommand'] = scroll.set
text.insert(0.0, rawtext)
text['state'] = 'disabled'
text.focus_set()
scroll.pack(side='right', fill='y')
text.pack(side='left', expand=True, fill='both')
class ViewFrame(Frame):
"Display TextFrame and Close button."
def __init__(self, parent, text):
super().__init__(parent)
self.parent = parent
self.bind('<Return>', self.ok)
self.bind('<Escape>', self.ok)
self.textframe = TextFrame(self, text)
self.button_ok = button_ok = Button(
self, text='Close', command=self.ok, takefocus=False)
self.textframe.pack(side='top', expand=True, fill='both')
button_ok.pack(side='bottom')
def ok(self, event=None):
"""Dismiss text viewer dialog."""
self.parent.destroy()
class ViewWindow(Toplevel):
"A simple text viewer dialog for IDLE." "A simple text viewer dialog for IDLE."
def __init__(self, parent, title, text, modal=True, def __init__(self, parent, title, text, modal=True,
...@@ -24,26 +69,19 @@ class TextViewer(Toplevel): ...@@ -24,26 +69,19 @@ class TextViewer(Toplevel):
_htest - bool; change box location when running htest. _htest - bool; change box location when running htest.
_utest - bool; don't wait_window when running unittest. _utest - bool; don't wait_window when running unittest.
""" """
Toplevel.__init__(self, parent) super().__init__(parent)
self.configure(borderwidth=5) self['borderwidth'] = 5
# Place dialog below parent if running htest. # Place dialog below parent if running htest.
self.geometry("=%dx%d+%d+%d" % (750, 500, x = parent.winfo_rootx() + 10
parent.winfo_rootx() + 10, y = parent.winfo_rooty() + (10 if not _htest else 100)
parent.winfo_rooty() + (10 if not _htest else 100))) self.geometry(f'=750x500+{x}+{y}')
# TODO: get fg/bg from theme.
self.bg = '#ffffff'
self.fg = '#000000'
self.create_widgets()
self.title(title) self.title(title)
self.viewframe = ViewFrame(self, text)
self.protocol("WM_DELETE_WINDOW", self.ok) self.protocol("WM_DELETE_WINDOW", self.ok)
self.parent = parent self.button_ok = button_ok = Button(self, text='Close',
self.text.focus_set() command=self.ok, takefocus=False)
# Bind keys for closing this dialog. self.viewframe.pack(side='top', expand=True, fill='both')
self.bind('<Return>', self.ok)
self.bind('<Escape>', self.ok)
self.text.insert(0.0, text)
self.text.config(state=DISABLED)
if modal: if modal:
self.transient(parent) self.transient(parent)
...@@ -51,31 +89,13 @@ class TextViewer(Toplevel): ...@@ -51,31 +89,13 @@ class TextViewer(Toplevel):
if not _utest: if not _utest:
self.wait_window() self.wait_window()
def create_widgets(self):
"Create Frame with Text (with vertical Scrollbar) and Button."
frame = Frame(self, relief=SUNKEN, height=700)
frame_buttons = Frame(self)
self.button_ok = Button(frame_buttons, text='Close',
command=self.ok, takefocus=False)
self.scrollbar = Scrollbar(frame, orient=VERTICAL, takefocus=False)
self.text = Text(frame, wrap=WORD, highlightthickness=0,
fg=self.fg, bg=self.bg)
self.scrollbar.config(command=self.text.yview)
self.text.config(yscrollcommand=self.scrollbar.set)
self.button_ok.pack()
self.scrollbar.pack(side=RIGHT, fill=Y)
self.text.pack(side=LEFT, expand=True, fill=BOTH)
frame_buttons.pack(side=BOTTOM, fill=X)
frame.pack(side=TOP, expand=True, fill=BOTH)
def ok(self, event=None): def ok(self, event=None):
"""Dismiss text viewer dialog.""" """Dismiss text viewer dialog."""
self.destroy() self.destroy()
def view_text(parent, title, text, modal=True, _utest=False): def view_text(parent, title, text, modal=True, _utest=False):
"""Create TextViewer for given text. """Create text viewer for given text.
parent - parent of this dialog parent - parent of this dialog
title - string which is the title of popup dialog title - string which is the title of popup dialog
...@@ -84,11 +104,11 @@ def view_text(parent, title, text, modal=True, _utest=False): ...@@ -84,11 +104,11 @@ def view_text(parent, title, text, modal=True, _utest=False):
dialog is displayed dialog is displayed
_utest - bool; controls wait_window on unittest _utest - bool; controls wait_window on unittest
""" """
return TextViewer(parent, title, text, modal, _utest=_utest) return ViewWindow(parent, title, text, modal, _utest=_utest)
def view_file(parent, title, filename, encoding=None, modal=True, _utest=False): def view_file(parent, title, filename, encoding=None, modal=True, _utest=False):
"""Create TextViewer for text in filename. """Create text viewer for text in filename.
Return error message if file cannot be read. Otherwise calls view_text Return error message if file cannot be read. Otherwise calls view_text
with contents of the file. with contents of the file.
...@@ -98,7 +118,7 @@ def view_file(parent, title, filename, encoding=None, modal=True, _utest=False): ...@@ -98,7 +118,7 @@ def view_file(parent, title, filename, encoding=None, modal=True, _utest=False):
contents = file.read() contents = file.read()
except OSError: except OSError:
showerror(title='File Load Error', showerror(title='File Load Error',
message='Unable to load file %r .' % filename, message=f'Unable to load file {filename!r} .',
parent=parent) parent=parent)
except UnicodeDecodeError as err: except UnicodeDecodeError as err:
showerror(title='Unicode Decode Error', showerror(title='Unicode Decode Error',
...@@ -113,4 +133,4 @@ if __name__ == '__main__': ...@@ -113,4 +133,4 @@ if __name__ == '__main__':
import unittest import unittest
unittest.main('idlelib.idle_test.test_textview', verbosity=2, exit=False) unittest.main('idlelib.idle_test.test_textview', verbosity=2, exit=False)
from idlelib.idle_test.htest import run from idlelib.idle_test.htest import run
run(TextViewer) run(ViewWindow)
IDLE: Improve textview with docstrings, PEP8 names, and more tests. 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