diff options
author | Tal Einat <532281+taleinat@users.noreply.github.com> | 2021-04-28 22:27:55 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-28 22:27:55 (GMT) |
commit | 15d386185659683fc044ccaa300aa8cd7d49cc1a (patch) | |
tree | 6fac7df4ac125b39648d8f0d7fbb008212dc6ba8 /Lib/idlelib/idle_test | |
parent | 103d5e420dd90489933ad9da8bb1d6008773384d (diff) | |
download | cpython-15d386185659683fc044ccaa300aa8cd7d49cc1a.zip cpython-15d386185659683fc044ccaa300aa8cd7d49cc1a.tar.gz cpython-15d386185659683fc044ccaa300aa8cd7d49cc1a.tar.bz2 |
bpo-37903: IDLE: Shell sidebar with prompts (GH-22682)
The first followup will change shell indents to spaces.
More are expected.
Co-authored-by: Terry Jan Reedy <tjreedy@udel.edu>
Diffstat (limited to 'Lib/idlelib/idle_test')
-rw-r--r-- | Lib/idlelib/idle_test/test_editor.py | 8 | ||||
-rw-r--r-- | Lib/idlelib/idle_test/test_pyshell.py | 84 | ||||
-rw-r--r-- | Lib/idlelib/idle_test/test_sidebar.py | 350 | ||||
-rw-r--r-- | Lib/idlelib/idle_test/test_squeezer.py | 32 | ||||
-rw-r--r-- | Lib/idlelib/idle_test/tkinter_testing_utils.py | 56 |
5 files changed, 494 insertions, 36 deletions
diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index 443dcf0..8665d68 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -167,7 +167,6 @@ class IndentAndNewlineTest(unittest.TestCase): '2.end'), ) - w.prompt_last_line = '' for test in tests: with self.subTest(label=test.label): insert(text, test.text) @@ -182,13 +181,6 @@ class IndentAndNewlineTest(unittest.TestCase): # Deletes selected text before adding new line. eq(get('1.0', 'end'), ' def f1(self, a,\n \n return a + b\n') - # Preserves the whitespace in shell prompt. - w.prompt_last_line = '>>> ' - insert(text, '>>> \t\ta =') - text.mark_set('insert', '1.5') - nl(None) - eq(get('1.0', 'end'), '>>> \na =\n') - class RMenuTest(unittest.TestCase): diff --git a/Lib/idlelib/idle_test/test_pyshell.py b/Lib/idlelib/idle_test/test_pyshell.py index 4a09667..7067039 100644 --- a/Lib/idlelib/idle_test/test_pyshell.py +++ b/Lib/idlelib/idle_test/test_pyshell.py @@ -60,5 +60,89 @@ class PyShellFileListTest(unittest.TestCase): ## self.assertIsInstance(ps, pyshell.PyShell) +class PyShellRemoveLastNewlineAndSurroundingWhitespaceTest(unittest.TestCase): + regexp = pyshell.PyShell._last_newline_re + + def all_removed(self, text): + self.assertEqual('', self.regexp.sub('', text)) + + def none_removed(self, text): + self.assertEqual(text, self.regexp.sub('', text)) + + def check_result(self, text, expected): + self.assertEqual(expected, self.regexp.sub('', text)) + + def test_empty(self): + self.all_removed('') + + def test_newline(self): + self.all_removed('\n') + + def test_whitespace_no_newline(self): + self.all_removed(' ') + self.all_removed(' ') + self.all_removed(' ') + self.all_removed(' ' * 20) + self.all_removed('\t') + self.all_removed('\t\t') + self.all_removed('\t\t\t') + self.all_removed('\t' * 20) + self.all_removed('\t ') + self.all_removed(' \t') + self.all_removed(' \t \t ') + self.all_removed('\t \t \t') + + def test_newline_with_whitespace(self): + self.all_removed(' \n') + self.all_removed('\t\n') + self.all_removed(' \t\n') + self.all_removed('\t \n') + self.all_removed('\n ') + self.all_removed('\n\t') + self.all_removed('\n \t') + self.all_removed('\n\t ') + self.all_removed(' \n ') + self.all_removed('\t\n ') + self.all_removed(' \n\t') + self.all_removed('\t\n\t') + self.all_removed('\t \t \t\n') + self.all_removed(' \t \t \n') + self.all_removed('\n\t \t \t') + self.all_removed('\n \t \t ') + + def test_multiple_newlines(self): + self.check_result('\n\n', '\n') + self.check_result('\n' * 5, '\n' * 4) + self.check_result('\n' * 5 + '\t', '\n' * 4) + self.check_result('\n' * 20, '\n' * 19) + self.check_result('\n' * 20 + ' ', '\n' * 19) + self.check_result(' \n \n ', ' \n') + self.check_result(' \n\n ', ' \n') + self.check_result(' \n\n', ' \n') + self.check_result('\t\n\n', '\t\n') + self.check_result('\n\n ', '\n') + self.check_result('\n\n\t', '\n') + self.check_result(' \n \n ', ' \n') + self.check_result('\t\n\t\n\t', '\t\n') + + def test_non_whitespace(self): + self.none_removed('a') + self.check_result('a\n', 'a') + self.check_result('a\n ', 'a') + self.check_result('a \n ', 'a') + self.check_result('a \n\t', 'a') + self.none_removed('-') + self.check_result('-\n', '-') + self.none_removed('.') + self.check_result('.\n', '.') + + def test_unsupported_whitespace(self): + self.none_removed('\v') + self.none_removed('\n\v') + self.check_result('\v\n', '\v') + self.none_removed(' \n\v') + self.check_result('\v\n ', '\v') + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index 2974a9a..7228d0e 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -1,13 +1,23 @@ -"""Test sidebar, coverage 93%""" -import idlelib.sidebar +"""Test sidebar, coverage 85%""" +from textwrap import dedent +import sys + from itertools import chain import unittest import unittest.mock -from test.support import requires +from test.support import requires, swap_attr import tkinter as tk +from .tkinter_testing_utils import run_in_tk_mainloop from idlelib.delegator import Delegator +from idlelib.editor import fixwordbreaks +from idlelib import macosx from idlelib.percolator import Percolator +import idlelib.pyshell +from idlelib.pyshell import fix_x11_paste, PyShell, PyShellFileList +from idlelib.run import fix_scaling +import idlelib.sidebar +from idlelib.sidebar import get_end_linenumber, get_lineno class Dummy_editwin: @@ -31,6 +41,7 @@ class LineNumbersTest(unittest.TestCase): def setUpClass(cls): requires('gui') cls.root = tk.Tk() + cls.root.withdraw() cls.text_frame = tk.Frame(cls.root) cls.text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) @@ -154,7 +165,7 @@ class LineNumbersTest(unittest.TestCase): self.assert_sidebar_n_lines(3) self.assert_state_disabled() - # Note: deleting up to "2.end" doesn't delete the final newline. + # Deleting up to "2.end" doesn't delete the final newline. self.text.delete('2.0', '2.end') self.assert_text_equals('fbarfoo\n\n\n') self.assert_sidebar_n_lines(3) @@ -165,7 +176,7 @@ class LineNumbersTest(unittest.TestCase): self.assert_sidebar_n_lines(1) self.assert_state_disabled() - # Note: Text widgets always keep a single '\n' character at the end. + # Text widgets always keep a single '\n' character at the end. self.text.delete('1.0', 'end') self.assert_text_equals('\n') self.assert_sidebar_n_lines(1) @@ -234,11 +245,19 @@ class LineNumbersTest(unittest.TestCase): self.assert_sidebar_n_lines(4) self.assertEqual(get_width(), 1) - # Note: Text widgets always keep a single '\n' character at the end. + # Text widgets always keep a single '\n' character at the end. self.text.delete('1.0', 'end -1c') self.assert_sidebar_n_lines(1) self.assertEqual(get_width(), 1) + # The following tests are temporarily disabled due to relying on + # simulated user input and inspecting which text is selected, which + # are fragile and can fail when several GUI tests are run in parallel + # or when the windows created by the test lose focus. + # + # TODO: Re-work these tests or remove them from the test suite. + + @unittest.skip('test disabled') def test_click_selection(self): self.linenumber.show_sidebar() self.text.insert('1.0', 'one\ntwo\nthree\nfour\n') @@ -252,6 +271,7 @@ class LineNumbersTest(unittest.TestCase): self.assertEqual(self.get_selection(), ('2.0', '3.0')) + @unittest.skip('test disabled') def simulate_drag(self, start_line, end_line): start_x, start_y = self.get_line_screen_position(start_line) end_x, end_y = self.get_line_screen_position(end_line) @@ -277,6 +297,7 @@ class LineNumbersTest(unittest.TestCase): x=end_x, y=end_y) self.root.update() + @unittest.skip('test disabled') def test_drag_selection_down(self): self.linenumber.show_sidebar() self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') @@ -286,6 +307,7 @@ class LineNumbersTest(unittest.TestCase): self.simulate_drag(2, 4) self.assertEqual(self.get_selection(), ('2.0', '5.0')) + @unittest.skip('test disabled') def test_drag_selection_up(self): self.linenumber.show_sidebar() self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') @@ -353,7 +375,7 @@ class LineNumbersTest(unittest.TestCase): ln.hide_sidebar() self.highlight_cfg = test_colors - # Nothing breaks with inactive code context. + # Nothing breaks with inactive line numbers. ln.update_colors() # Show line numbers, previous colors change is immediately effective. @@ -370,5 +392,319 @@ class LineNumbersTest(unittest.TestCase): assert_colors_are_equal(orig_colors) +class ShellSidebarTest(unittest.TestCase): + root: tk.Tk = None + shell: PyShell = None + + @classmethod + def setUpClass(cls): + requires('gui') + + cls.root = root = tk.Tk() + root.withdraw() + + fix_scaling(root) + fixwordbreaks(root) + fix_x11_paste(root) + + cls.flist = flist = PyShellFileList(root) + macosx.setupApp(root, flist) + root.update_idletasks() + + cls.init_shell() + + @classmethod + def tearDownClass(cls): + if cls.shell is not None: + cls.shell.executing = False + cls.shell.close() + cls.shell = None + cls.flist = None + cls.root.update_idletasks() + cls.root.destroy() + cls.root = None + + @classmethod + def init_shell(cls): + cls.shell = cls.flist.open_shell() + cls.shell.pollinterval = 10 + cls.root.update() + cls.n_preface_lines = get_lineno(cls.shell.text, 'end-1c') - 1 + + @classmethod + def reset_shell(cls): + cls.shell.per.bottom.delete(f'{cls.n_preface_lines+1}.0', 'end-1c') + cls.shell.shell_sidebar.update_sidebar() + cls.root.update() + + def setUp(self): + # In some test environments, e.g. Azure Pipelines (as of + # Apr. 2021), sys.stdout is changed between tests. However, + # PyShell relies on overriding sys.stdout when run without a + # sub-process (as done here; see setUpClass). + self._saved_stdout = None + if sys.stdout != self.shell.stdout: + self._saved_stdout = sys.stdout + sys.stdout = self.shell.stdout + + self.reset_shell() + + def tearDown(self): + if self._saved_stdout is not None: + sys.stdout = self._saved_stdout + + def get_sidebar_lines(self): + canvas = self.shell.shell_sidebar.canvas + texts = list(canvas.find(tk.ALL)) + texts_by_y_coords = { + canvas.bbox(text)[1]: canvas.itemcget(text, 'text') + for text in texts + } + line_y_coords = self.get_shell_line_y_coords() + return [texts_by_y_coords.get(y, None) for y in line_y_coords] + + def assert_sidebar_lines_end_with(self, expected_lines): + self.shell.shell_sidebar.update_sidebar() + self.assertEqual( + self.get_sidebar_lines()[-len(expected_lines):], + expected_lines, + ) + + def get_shell_line_y_coords(self): + text = self.shell.text + y_coords = [] + index = text.index("@0,0") + if index.split('.', 1)[1] != '0': + index = text.index(f"{index} +1line linestart") + while True: + lineinfo = text.dlineinfo(index) + if lineinfo is None: + break + y_coords.append(lineinfo[1]) + index = text.index(f"{index} +1line") + return y_coords + + def get_sidebar_line_y_coords(self): + canvas = self.shell.shell_sidebar.canvas + texts = list(canvas.find(tk.ALL)) + texts.sort(key=lambda text: canvas.bbox(text)[1]) + return [canvas.bbox(text)[1] for text in texts] + + def assert_sidebar_lines_synced(self): + self.assertLessEqual( + set(self.get_sidebar_line_y_coords()), + set(self.get_shell_line_y_coords()), + ) + + def do_input(self, input): + shell = self.shell + text = shell.text + for line_index, line in enumerate(input.split('\n')): + if line_index > 0: + text.event_generate('<<newline-and-indent>>') + text.insert('insert', line, 'stdin') + + def test_initial_state(self): + sidebar_lines = self.get_sidebar_lines() + self.assertEqual( + sidebar_lines, + [None] * (len(sidebar_lines) - 1) + ['>>>'], + ) + self.assert_sidebar_lines_synced() + + @run_in_tk_mainloop + def test_single_empty_input(self): + self.do_input('\n') + yield + self.assert_sidebar_lines_end_with(['>>>', '>>>']) + + @run_in_tk_mainloop + def test_single_line_statement(self): + self.do_input('1\n') + yield + self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) + + @run_in_tk_mainloop + def test_multi_line_statement(self): + # Block statements are not indented because IDLE auto-indents. + self.do_input(dedent('''\ + if True: + print(1) + + ''')) + yield + self.assert_sidebar_lines_end_with([ + '>>>', + '...', + '...', + '...', + None, + '>>>', + ]) + + @run_in_tk_mainloop + def test_single_long_line_wraps(self): + self.do_input('1' * 200 + '\n') + yield + self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) + self.assert_sidebar_lines_synced() + + @run_in_tk_mainloop + def test_squeeze_multi_line_output(self): + shell = self.shell + text = shell.text + + self.do_input('print("a\\nb\\nc")\n') + yield + self.assert_sidebar_lines_end_with(['>>>', None, None, None, '>>>']) + + text.mark_set('insert', f'insert -1line linestart') + text.event_generate('<<squeeze-current-text>>') + yield + self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) + self.assert_sidebar_lines_synced() + + shell.squeezer.expandingbuttons[0].expand() + yield + self.assert_sidebar_lines_end_with(['>>>', None, None, None, '>>>']) + self.assert_sidebar_lines_synced() + + @run_in_tk_mainloop + def test_interrupt_recall_undo_redo(self): + text = self.shell.text + # Block statements are not indented because IDLE auto-indents. + initial_sidebar_lines = self.get_sidebar_lines() + + self.do_input(dedent('''\ + if True: + print(1) + ''')) + yield + self.assert_sidebar_lines_end_with(['>>>', '...', '...']) + with_block_sidebar_lines = self.get_sidebar_lines() + self.assertNotEqual(with_block_sidebar_lines, initial_sidebar_lines) + + # Control-C + text.event_generate('<<interrupt-execution>>') + yield + self.assert_sidebar_lines_end_with(['>>>', '...', '...', None, '>>>']) + + # Recall previous via history + text.event_generate('<<history-previous>>') + text.event_generate('<<interrupt-execution>>') + yield + self.assert_sidebar_lines_end_with(['>>>', '...', None, '>>>']) + + # Recall previous via recall + text.mark_set('insert', text.index('insert -2l')) + text.event_generate('<<newline-and-indent>>') + yield + + text.event_generate('<<undo>>') + yield + self.assert_sidebar_lines_end_with(['>>>']) + + text.event_generate('<<redo>>') + yield + self.assert_sidebar_lines_end_with(['>>>', '...']) + + text.event_generate('<<newline-and-indent>>') + text.event_generate('<<newline-and-indent>>') + yield + self.assert_sidebar_lines_end_with( + ['>>>', '...', '...', '...', None, '>>>'] + ) + + @run_in_tk_mainloop + def test_very_long_wrapped_line(self): + with swap_attr(self.shell, 'squeezer', None): + self.do_input('x = ' + '1'*10_000 + '\n') + yield + self.assertEqual(self.get_sidebar_lines(), ['>>>']) + + def test_font(self): + sidebar = self.shell.shell_sidebar + + test_font = 'TkTextFont' + + def mock_idleconf_GetFont(root, configType, section): + return test_font + GetFont_patcher = unittest.mock.patch.object( + idlelib.sidebar.idleConf, 'GetFont', mock_idleconf_GetFont) + GetFont_patcher.start() + def cleanup(): + GetFont_patcher.stop() + sidebar.update_font() + self.addCleanup(cleanup) + + def get_sidebar_font(): + canvas = sidebar.canvas + texts = list(canvas.find(tk.ALL)) + fonts = {canvas.itemcget(text, 'font') for text in texts} + self.assertEqual(len(fonts), 1) + return next(iter(fonts)) + + self.assertNotEqual(get_sidebar_font(), test_font) + sidebar.update_font() + self.assertEqual(get_sidebar_font(), test_font) + + def test_highlight_colors(self): + sidebar = self.shell.shell_sidebar + + test_colors = {"background": '#abcdef', "foreground": '#123456'} + + orig_idleConf_GetHighlight = idlelib.sidebar.idleConf.GetHighlight + def mock_idleconf_GetHighlight(theme, element): + if element in ['linenumber', 'console']: + return test_colors + return orig_idleConf_GetHighlight(theme, element) + GetHighlight_patcher = unittest.mock.patch.object( + idlelib.sidebar.idleConf, 'GetHighlight', + mock_idleconf_GetHighlight) + GetHighlight_patcher.start() + def cleanup(): + GetHighlight_patcher.stop() + sidebar.update_colors() + self.addCleanup(cleanup) + + def get_sidebar_colors(): + canvas = sidebar.canvas + texts = list(canvas.find(tk.ALL)) + fgs = {canvas.itemcget(text, 'fill') for text in texts} + self.assertEqual(len(fgs), 1) + fg = next(iter(fgs)) + bg = canvas.cget('background') + return {"background": bg, "foreground": fg} + + self.assertNotEqual(get_sidebar_colors(), test_colors) + sidebar.update_colors() + self.assertEqual(get_sidebar_colors(), test_colors) + + @run_in_tk_mainloop + def test_mousewheel(self): + sidebar = self.shell.shell_sidebar + text = self.shell.text + + # Enter a 100-line string to scroll the shell screen down. + self.do_input('x = """' + '\n'*100 + '"""\n') + yield + self.assertGreater(get_lineno(text, '@0,0'), 1) + + last_lineno = get_end_linenumber(text) + self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) + + # Scroll up using the <MouseWheel> event. + # The meaning delta is platform-dependant. + delta = -1 if sys.platform == 'darwin' else 120 + sidebar.canvas.event_generate('<MouseWheel>', x=0, y=0, delta=delta) + yield + self.assertIsNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) + + # Scroll back down using the <Button-5> event. + sidebar.canvas.event_generate('<Button-5>', x=0, y=0) + yield + self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_squeezer.py b/Lib/idlelib/idle_test/test_squeezer.py index ee1bbd7..eaf81a5 100644 --- a/Lib/idlelib/idle_test/test_squeezer.py +++ b/Lib/idlelib/idle_test/test_squeezer.py @@ -7,13 +7,12 @@ from unittest.mock import Mock, NonCallableMagicMock, patch, sentinel, ANY from test.support import requires from idlelib.config import idleConf +from idlelib.percolator import Percolator from idlelib.squeezer import count_lines_with_wrapping, ExpandingButton, \ Squeezer from idlelib import macosx from idlelib.textview import view_text from idlelib.tooltip import Hovertip -from idlelib.pyshell import PyShell - SENTINEL_VALUE = sentinel.SENTINEL_VALUE @@ -205,8 +204,8 @@ class SqueezerTest(unittest.TestCase): self.assertEqual(text_widget.get('1.0', 'end'), '\n') self.assertEqual(len(squeezer.expandingbuttons), 1) - def test_squeeze_current_text_event(self): - """Test the squeeze_current_text event.""" + def test_squeeze_current_text(self): + """Test the squeeze_current_text method.""" # Squeezing text should work for both stdout and stderr. for tag_name in ["stdout", "stderr"]: editwin = self.make_mock_editor_window(with_text_widget=True) @@ -222,7 +221,7 @@ class SqueezerTest(unittest.TestCase): self.assertEqual(len(squeezer.expandingbuttons), 0) # Test squeezing the current text. - retval = squeezer.squeeze_current_text_event(event=Mock()) + retval = squeezer.squeeze_current_text() self.assertEqual(retval, "break") self.assertEqual(text_widget.get('1.0', 'end'), '\n\n') self.assertEqual(len(squeezer.expandingbuttons), 1) @@ -230,11 +229,11 @@ class SqueezerTest(unittest.TestCase): # Test that expanding the squeezed text works and afterwards # the Text widget contains the original text. - squeezer.expandingbuttons[0].expand(event=Mock()) + squeezer.expandingbuttons[0].expand() self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n') self.assertEqual(len(squeezer.expandingbuttons), 0) - def test_squeeze_current_text_event_no_allowed_tags(self): + def test_squeeze_current_text_no_allowed_tags(self): """Test that the event doesn't squeeze text without a relevant tag.""" editwin = self.make_mock_editor_window(with_text_widget=True) text_widget = editwin.text @@ -249,7 +248,7 @@ class SqueezerTest(unittest.TestCase): self.assertEqual(len(squeezer.expandingbuttons), 0) # Test squeezing the current text. - retval = squeezer.squeeze_current_text_event(event=Mock()) + retval = squeezer.squeeze_current_text() self.assertEqual(retval, "break") self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n') self.assertEqual(len(squeezer.expandingbuttons), 0) @@ -264,13 +263,13 @@ class SqueezerTest(unittest.TestCase): # Prepare some text in the Text widget and squeeze it. text_widget.insert("1.0", "SOME\nTEXT\n", "stdout") text_widget.mark_set("insert", "1.0") - squeezer.squeeze_current_text_event(event=Mock()) + squeezer.squeeze_current_text() self.assertEqual(len(squeezer.expandingbuttons), 1) # Test squeezing the current text. text_widget.insert("1.0", "MORE\nSTUFF\n", "stdout") text_widget.mark_set("insert", "1.0") - retval = squeezer.squeeze_current_text_event(event=Mock()) + retval = squeezer.squeeze_current_text() self.assertEqual(retval, "break") self.assertEqual(text_widget.get('1.0', 'end'), '\n\n\n') self.assertEqual(len(squeezer.expandingbuttons), 2) @@ -311,6 +310,7 @@ class ExpandingButtonTest(unittest.TestCase): root = get_test_tk_root(self) squeezer = Mock() squeezer.editwin.text = Text(root) + squeezer.editwin.per = Percolator(squeezer.editwin.text) # Set default values for the configuration settings. squeezer.auto_squeeze_min_lines = 50 @@ -352,14 +352,9 @@ class ExpandingButtonTest(unittest.TestCase): # Insert the button into the text widget # (this is normally done by the Squeezer class). - text_widget = expandingbutton.text + text_widget = squeezer.editwin.text text_widget.window_create("1.0", window=expandingbutton) - # Set base_text to the text widget, so that changes are actually - # made to it (by ExpandingButton) and we can inspect these - # changes afterwards. - expandingbutton.base_text = expandingbutton.text - # trigger the expand event retval = expandingbutton.expand(event=Mock()) self.assertEqual(retval, None) @@ -390,11 +385,6 @@ class ExpandingButtonTest(unittest.TestCase): text_widget = expandingbutton.text text_widget.window_create("1.0", window=expandingbutton) - # Set base_text to the text widget, so that changes are actually - # made to it (by ExpandingButton) and we can inspect these - # changes afterwards. - expandingbutton.base_text = expandingbutton.text - # Patch the message box module to always return False. with patch('idlelib.squeezer.messagebox') as mock_msgbox: mock_msgbox.askokcancel.return_value = False diff --git a/Lib/idlelib/idle_test/tkinter_testing_utils.py b/Lib/idlelib/idle_test/tkinter_testing_utils.py new file mode 100644 index 0000000..a9f8386 --- /dev/null +++ b/Lib/idlelib/idle_test/tkinter_testing_utils.py @@ -0,0 +1,56 @@ +"""Utilities for testing with Tkinter""" +import functools + + +def run_in_tk_mainloop(test_method): + """Decorator for running a test method with a real Tk mainloop. + + This starts a Tk mainloop before running the test, and stops it + at the end. This is faster and more robust than the common + alternative method of calling .update() and/or .update_idletasks(). + + Test methods using this must be written as generator functions, + using "yield" to allow the mainloop to process events and "after" + callbacks, and then continue the test from that point. + + This also assumes that the test class has a .root attribute, + which is a tkinter.Tk object. + + For example (from test_sidebar.py): + + @run_test_with_tk_mainloop + def test_single_empty_input(self): + self.do_input('\n') + yield + self.assert_sidebar_lines_end_with(['>>>', '>>>']) + """ + @functools.wraps(test_method) + def new_test_method(self): + test_generator = test_method(self) + root = self.root + # Exceptions raised by self.assert...() need to be raised + # outside of the after() callback in order for the test + # harness to capture them. + exception = None + def after_callback(): + nonlocal exception + try: + next(test_generator) + except StopIteration: + root.quit() + except Exception as exc: + exception = exc + root.quit() + else: + # Schedule the Tk mainloop to call this function again, + # using a robust method of ensuring that it gets a + # chance to process queued events before doing so. + # See: https://stackoverflow.com/q/18499082#comment65004099_38817470 + root.after(1, root.after_idle, after_callback) + root.after(0, root.after_idle, after_callback) + root.mainloop() + + if exception: + raise exception + + return new_test_method |