2021-04-28 19:27:55 -03:00
|
|
|
"""Test sidebar, coverage 85%"""
|
|
|
|
from textwrap import dedent
|
|
|
|
import sys
|
|
|
|
|
2019-07-23 09:22:11 -03:00
|
|
|
from itertools import chain
|
|
|
|
import unittest
|
|
|
|
import unittest.mock
|
2021-04-28 19:27:55 -03:00
|
|
|
from test.support import requires, swap_attr
|
2022-09-02 13:35:08 -03:00
|
|
|
from test import support
|
2019-07-23 09:22:11 -03:00
|
|
|
import tkinter as tk
|
2021-04-30 00:52:47 -03:00
|
|
|
from idlelib.idle_test.tkinter_testing_utils import run_in_tk_mainloop
|
2019-07-23 09:22:11 -03:00
|
|
|
|
|
|
|
from idlelib.delegator import Delegator
|
2021-04-28 19:27:55 -03:00
|
|
|
from idlelib.editor import fixwordbreaks
|
2019-07-23 09:22:11 -03:00
|
|
|
from idlelib.percolator import Percolator
|
2021-04-28 19:27:55 -03:00
|
|
|
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
|
2019-07-23 09:22:11 -03:00
|
|
|
|
|
|
|
|
|
|
|
class Dummy_editwin:
|
|
|
|
def __init__(self, text):
|
|
|
|
self.text = text
|
|
|
|
self.text_frame = self.text.master
|
|
|
|
self.per = Percolator(text)
|
|
|
|
self.undo = Delegator()
|
|
|
|
self.per.insertfilter(self.undo)
|
|
|
|
|
|
|
|
def setvar(self, name, value):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def getlineno(self, index):
|
|
|
|
return int(float(self.text.index(index)))
|
|
|
|
|
|
|
|
|
|
|
|
class LineNumbersTest(unittest.TestCase):
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def setUpClass(cls):
|
|
|
|
requires('gui')
|
|
|
|
cls.root = tk.Tk()
|
2021-04-28 19:27:55 -03:00
|
|
|
cls.root.withdraw()
|
2019-07-23 09:22:11 -03:00
|
|
|
|
|
|
|
cls.text_frame = tk.Frame(cls.root)
|
|
|
|
cls.text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
|
|
cls.text_frame.rowconfigure(1, weight=1)
|
|
|
|
cls.text_frame.columnconfigure(1, weight=1)
|
|
|
|
|
|
|
|
cls.text = tk.Text(cls.text_frame, width=80, height=24, wrap=tk.NONE)
|
|
|
|
cls.text.grid(row=1, column=1, sticky=tk.NSEW)
|
|
|
|
|
|
|
|
cls.editwin = Dummy_editwin(cls.text)
|
|
|
|
cls.editwin.vbar = tk.Scrollbar(cls.text_frame)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def tearDownClass(cls):
|
|
|
|
cls.editwin.per.close()
|
|
|
|
cls.root.update()
|
|
|
|
cls.root.destroy()
|
|
|
|
del cls.text, cls.text_frame, cls.editwin, cls.root
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
self.linenumber = idlelib.sidebar.LineNumbers(self.editwin)
|
|
|
|
|
|
|
|
self.highlight_cfg = {"background": '#abcdef',
|
|
|
|
"foreground": '#123456'}
|
|
|
|
orig_idleConf_GetHighlight = idlelib.sidebar.idleConf.GetHighlight
|
|
|
|
def mock_idleconf_GetHighlight(theme, element):
|
|
|
|
if element == 'linenumber':
|
|
|
|
return self.highlight_cfg
|
|
|
|
return orig_idleConf_GetHighlight(theme, element)
|
|
|
|
GetHighlight_patcher = unittest.mock.patch.object(
|
|
|
|
idlelib.sidebar.idleConf, 'GetHighlight', mock_idleconf_GetHighlight)
|
|
|
|
GetHighlight_patcher.start()
|
|
|
|
self.addCleanup(GetHighlight_patcher.stop)
|
|
|
|
|
|
|
|
self.font_override = 'TkFixedFont'
|
|
|
|
def mock_idleconf_GetFont(root, configType, section):
|
|
|
|
return self.font_override
|
|
|
|
GetFont_patcher = unittest.mock.patch.object(
|
|
|
|
idlelib.sidebar.idleConf, 'GetFont', mock_idleconf_GetFont)
|
|
|
|
GetFont_patcher.start()
|
|
|
|
self.addCleanup(GetFont_patcher.stop)
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
self.text.delete('1.0', 'end')
|
|
|
|
|
|
|
|
def get_selection(self):
|
|
|
|
return tuple(map(str, self.text.tag_ranges('sel')))
|
|
|
|
|
|
|
|
def get_line_screen_position(self, line):
|
|
|
|
bbox = self.linenumber.sidebar_text.bbox(f'{line}.end -1c')
|
|
|
|
x = bbox[0] + 2
|
|
|
|
y = bbox[1] + 2
|
|
|
|
return x, y
|
|
|
|
|
|
|
|
def assert_state_disabled(self):
|
|
|
|
state = self.linenumber.sidebar_text.config()['state']
|
|
|
|
self.assertEqual(state[-1], tk.DISABLED)
|
|
|
|
|
|
|
|
def get_sidebar_text_contents(self):
|
|
|
|
return self.linenumber.sidebar_text.get('1.0', tk.END)
|
|
|
|
|
|
|
|
def assert_sidebar_n_lines(self, n_lines):
|
|
|
|
expected = '\n'.join(chain(map(str, range(1, n_lines + 1)), ['']))
|
|
|
|
self.assertEqual(self.get_sidebar_text_contents(), expected)
|
|
|
|
|
|
|
|
def assert_text_equals(self, expected):
|
|
|
|
return self.assertEqual(self.text.get('1.0', 'end'), expected)
|
|
|
|
|
|
|
|
def test_init_empty(self):
|
|
|
|
self.assert_sidebar_n_lines(1)
|
|
|
|
|
|
|
|
def test_init_not_empty(self):
|
|
|
|
self.text.insert('insert', 'foo bar\n'*3)
|
|
|
|
self.assert_text_equals('foo bar\n'*3 + '\n')
|
|
|
|
self.assert_sidebar_n_lines(4)
|
|
|
|
|
|
|
|
def test_toggle_linenumbering(self):
|
|
|
|
self.assertEqual(self.linenumber.is_shown, False)
|
|
|
|
self.linenumber.show_sidebar()
|
|
|
|
self.assertEqual(self.linenumber.is_shown, True)
|
|
|
|
self.linenumber.hide_sidebar()
|
|
|
|
self.assertEqual(self.linenumber.is_shown, False)
|
|
|
|
self.linenumber.hide_sidebar()
|
|
|
|
self.assertEqual(self.linenumber.is_shown, False)
|
|
|
|
self.linenumber.show_sidebar()
|
|
|
|
self.assertEqual(self.linenumber.is_shown, True)
|
|
|
|
self.linenumber.show_sidebar()
|
|
|
|
self.assertEqual(self.linenumber.is_shown, True)
|
|
|
|
|
|
|
|
def test_insert(self):
|
|
|
|
self.text.insert('insert', 'foobar')
|
|
|
|
self.assert_text_equals('foobar\n')
|
|
|
|
self.assert_sidebar_n_lines(1)
|
|
|
|
self.assert_state_disabled()
|
|
|
|
|
|
|
|
self.text.insert('insert', '\nfoo')
|
|
|
|
self.assert_text_equals('foobar\nfoo\n')
|
|
|
|
self.assert_sidebar_n_lines(2)
|
|
|
|
self.assert_state_disabled()
|
|
|
|
|
|
|
|
self.text.insert('insert', 'hello\n'*2)
|
|
|
|
self.assert_text_equals('foobar\nfoohello\nhello\n\n')
|
|
|
|
self.assert_sidebar_n_lines(4)
|
|
|
|
self.assert_state_disabled()
|
|
|
|
|
|
|
|
self.text.insert('insert', '\nworld')
|
|
|
|
self.assert_text_equals('foobar\nfoohello\nhello\n\nworld\n')
|
|
|
|
self.assert_sidebar_n_lines(5)
|
|
|
|
self.assert_state_disabled()
|
|
|
|
|
|
|
|
def test_delete(self):
|
|
|
|
self.text.insert('insert', 'foobar')
|
|
|
|
self.assert_text_equals('foobar\n')
|
|
|
|
self.text.delete('1.1', '1.3')
|
|
|
|
self.assert_text_equals('fbar\n')
|
|
|
|
self.assert_sidebar_n_lines(1)
|
|
|
|
self.assert_state_disabled()
|
|
|
|
|
|
|
|
self.text.insert('insert', 'foo\n'*2)
|
|
|
|
self.assert_text_equals('fbarfoo\nfoo\n\n')
|
|
|
|
self.assert_sidebar_n_lines(3)
|
|
|
|
self.assert_state_disabled()
|
|
|
|
|
2021-04-28 19:27:55 -03:00
|
|
|
# Deleting up to "2.end" doesn't delete the final newline.
|
2019-07-23 09:22:11 -03:00
|
|
|
self.text.delete('2.0', '2.end')
|
|
|
|
self.assert_text_equals('fbarfoo\n\n\n')
|
|
|
|
self.assert_sidebar_n_lines(3)
|
|
|
|
self.assert_state_disabled()
|
|
|
|
|
|
|
|
self.text.delete('1.3', 'end')
|
|
|
|
self.assert_text_equals('fba\n')
|
|
|
|
self.assert_sidebar_n_lines(1)
|
|
|
|
self.assert_state_disabled()
|
|
|
|
|
2021-04-28 19:27:55 -03:00
|
|
|
# Text widgets always keep a single '\n' character at the end.
|
2019-07-23 09:22:11 -03:00
|
|
|
self.text.delete('1.0', 'end')
|
|
|
|
self.assert_text_equals('\n')
|
|
|
|
self.assert_sidebar_n_lines(1)
|
|
|
|
self.assert_state_disabled()
|
|
|
|
|
|
|
|
def test_sidebar_text_width(self):
|
|
|
|
"""
|
|
|
|
Test that linenumber text widget is always at the minimum
|
|
|
|
width
|
|
|
|
"""
|
|
|
|
def get_width():
|
|
|
|
return self.linenumber.sidebar_text.config()['width'][-1]
|
|
|
|
|
|
|
|
self.assert_sidebar_n_lines(1)
|
|
|
|
self.assertEqual(get_width(), 1)
|
|
|
|
|
|
|
|
self.text.insert('insert', 'foo')
|
|
|
|
self.assert_sidebar_n_lines(1)
|
|
|
|
self.assertEqual(get_width(), 1)
|
|
|
|
|
|
|
|
self.text.insert('insert', 'foo\n'*8)
|
|
|
|
self.assert_sidebar_n_lines(9)
|
|
|
|
self.assertEqual(get_width(), 1)
|
|
|
|
|
|
|
|
self.text.insert('insert', 'foo\n')
|
|
|
|
self.assert_sidebar_n_lines(10)
|
|
|
|
self.assertEqual(get_width(), 2)
|
|
|
|
|
|
|
|
self.text.insert('insert', 'foo\n')
|
|
|
|
self.assert_sidebar_n_lines(11)
|
|
|
|
self.assertEqual(get_width(), 2)
|
|
|
|
|
|
|
|
self.text.delete('insert -1l linestart', 'insert linestart')
|
|
|
|
self.assert_sidebar_n_lines(10)
|
|
|
|
self.assertEqual(get_width(), 2)
|
|
|
|
|
|
|
|
self.text.delete('insert -1l linestart', 'insert linestart')
|
|
|
|
self.assert_sidebar_n_lines(9)
|
|
|
|
self.assertEqual(get_width(), 1)
|
|
|
|
|
|
|
|
self.text.insert('insert', 'foo\n'*90)
|
|
|
|
self.assert_sidebar_n_lines(99)
|
|
|
|
self.assertEqual(get_width(), 2)
|
|
|
|
|
|
|
|
self.text.insert('insert', 'foo\n')
|
|
|
|
self.assert_sidebar_n_lines(100)
|
|
|
|
self.assertEqual(get_width(), 3)
|
|
|
|
|
|
|
|
self.text.insert('insert', 'foo\n')
|
|
|
|
self.assert_sidebar_n_lines(101)
|
|
|
|
self.assertEqual(get_width(), 3)
|
|
|
|
|
|
|
|
self.text.delete('insert -1l linestart', 'insert linestart')
|
|
|
|
self.assert_sidebar_n_lines(100)
|
|
|
|
self.assertEqual(get_width(), 3)
|
|
|
|
|
|
|
|
self.text.delete('insert -1l linestart', 'insert linestart')
|
|
|
|
self.assert_sidebar_n_lines(99)
|
|
|
|
self.assertEqual(get_width(), 2)
|
|
|
|
|
|
|
|
self.text.delete('50.0 -1c', 'end -1c')
|
|
|
|
self.assert_sidebar_n_lines(49)
|
|
|
|
self.assertEqual(get_width(), 2)
|
|
|
|
|
|
|
|
self.text.delete('5.0 -1c', 'end -1c')
|
|
|
|
self.assert_sidebar_n_lines(4)
|
|
|
|
self.assertEqual(get_width(), 1)
|
|
|
|
|
2021-04-28 19:27:55 -03:00
|
|
|
# Text widgets always keep a single '\n' character at the end.
|
2019-07-23 09:22:11 -03:00
|
|
|
self.text.delete('1.0', 'end -1c')
|
|
|
|
self.assert_sidebar_n_lines(1)
|
|
|
|
self.assertEqual(get_width(), 1)
|
|
|
|
|
2021-04-28 19:27:55 -03:00
|
|
|
# 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')
|
2019-07-23 09:22:11 -03:00
|
|
|
def test_click_selection(self):
|
|
|
|
self.linenumber.show_sidebar()
|
|
|
|
self.text.insert('1.0', 'one\ntwo\nthree\nfour\n')
|
|
|
|
self.root.update()
|
|
|
|
|
|
|
|
# Click on the second line.
|
|
|
|
x, y = self.get_line_screen_position(2)
|
|
|
|
self.linenumber.sidebar_text.event_generate('<Button-1>', x=x, y=y)
|
|
|
|
self.linenumber.sidebar_text.update()
|
|
|
|
self.root.update()
|
|
|
|
|
|
|
|
self.assertEqual(self.get_selection(), ('2.0', '3.0'))
|
|
|
|
|
2019-08-04 13:25:27 -03:00
|
|
|
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)
|
2019-07-23 09:22:11 -03:00
|
|
|
|
|
|
|
self.linenumber.sidebar_text.event_generate('<Button-1>',
|
|
|
|
x=start_x, y=start_y)
|
2019-08-04 13:25:27 -03:00
|
|
|
self.root.update()
|
|
|
|
|
|
|
|
def lerp(a, b, steps):
|
|
|
|
"""linearly interpolate from a to b (inclusive) in equal steps"""
|
|
|
|
last_step = steps - 1
|
|
|
|
for i in range(steps):
|
|
|
|
yield ((last_step - i) / last_step) * a + (i / last_step) * b
|
|
|
|
|
|
|
|
for x, y in zip(
|
|
|
|
map(int, lerp(start_x, end_x, steps=11)),
|
|
|
|
map(int, lerp(start_y, end_y, steps=11)),
|
|
|
|
):
|
|
|
|
self.linenumber.sidebar_text.event_generate('<B1-Motion>', x=x, y=y)
|
|
|
|
self.root.update()
|
|
|
|
|
2019-07-23 09:22:11 -03:00
|
|
|
self.linenumber.sidebar_text.event_generate('<ButtonRelease-1>',
|
|
|
|
x=end_x, y=end_y)
|
|
|
|
self.root.update()
|
2019-08-04 13:25:27 -03:00
|
|
|
|
2021-04-28 19:27:55 -03:00
|
|
|
@unittest.skip('test disabled')
|
2019-08-04 13:25:27 -03:00
|
|
|
def test_drag_selection_down(self):
|
|
|
|
self.linenumber.show_sidebar()
|
|
|
|
self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n')
|
|
|
|
self.root.update()
|
|
|
|
|
|
|
|
# Drag from the second line to the fourth line.
|
|
|
|
self.simulate_drag(2, 4)
|
2019-07-29 18:57:36 -03:00
|
|
|
self.assertEqual(self.get_selection(), ('2.0', '5.0'))
|
|
|
|
|
2021-04-28 19:27:55 -03:00
|
|
|
@unittest.skip('test disabled')
|
2019-07-29 18:57:36 -03:00
|
|
|
def test_drag_selection_up(self):
|
|
|
|
self.linenumber.show_sidebar()
|
|
|
|
self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n')
|
|
|
|
self.root.update()
|
2019-07-23 09:22:11 -03:00
|
|
|
|
2019-07-29 18:57:36 -03:00
|
|
|
# Drag from the fourth line to the second line.
|
2019-08-04 13:25:27 -03:00
|
|
|
self.simulate_drag(4, 2)
|
2019-07-29 18:57:36 -03:00
|
|
|
self.assertEqual(self.get_selection(), ('2.0', '5.0'))
|
2019-07-23 09:22:11 -03:00
|
|
|
|
|
|
|
def test_scroll(self):
|
|
|
|
self.linenumber.show_sidebar()
|
|
|
|
self.text.insert('1.0', 'line\n' * 100)
|
|
|
|
self.root.update()
|
|
|
|
|
|
|
|
# Scroll down 10 lines.
|
|
|
|
self.text.yview_scroll(10, 'unit')
|
|
|
|
self.root.update()
|
|
|
|
self.assertEqual(self.text.index('@0,0'), '11.0')
|
|
|
|
self.assertEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0')
|
|
|
|
|
|
|
|
# Generate a mouse-wheel event and make sure it scrolled up or down.
|
|
|
|
# The meaning of the "delta" is OS-dependant, so this just checks for
|
|
|
|
# any change.
|
|
|
|
self.linenumber.sidebar_text.event_generate('<MouseWheel>',
|
|
|
|
x=0, y=0,
|
|
|
|
delta=10)
|
|
|
|
self.root.update()
|
|
|
|
self.assertNotEqual(self.text.index('@0,0'), '11.0')
|
|
|
|
self.assertNotEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0')
|
|
|
|
|
|
|
|
def test_font(self):
|
|
|
|
ln = self.linenumber
|
|
|
|
|
|
|
|
orig_font = ln.sidebar_text['font']
|
|
|
|
test_font = 'TkTextFont'
|
|
|
|
self.assertNotEqual(orig_font, test_font)
|
|
|
|
|
|
|
|
# Ensure line numbers aren't shown.
|
|
|
|
ln.hide_sidebar()
|
|
|
|
|
|
|
|
self.font_override = test_font
|
|
|
|
# Nothing breaks when line numbers aren't shown.
|
|
|
|
ln.update_font()
|
|
|
|
|
|
|
|
# Activate line numbers, previous font change is immediately effective.
|
|
|
|
ln.show_sidebar()
|
|
|
|
self.assertEqual(ln.sidebar_text['font'], test_font)
|
|
|
|
|
|
|
|
# Call the font update with line numbers shown, change is picked up.
|
|
|
|
self.font_override = orig_font
|
|
|
|
ln.update_font()
|
|
|
|
self.assertEqual(ln.sidebar_text['font'], orig_font)
|
|
|
|
|
|
|
|
def test_highlight_colors(self):
|
|
|
|
ln = self.linenumber
|
|
|
|
|
|
|
|
orig_colors = dict(self.highlight_cfg)
|
|
|
|
test_colors = {'background': '#222222', 'foreground': '#ffff00'}
|
|
|
|
|
|
|
|
def assert_colors_are_equal(colors):
|
|
|
|
self.assertEqual(ln.sidebar_text['background'], colors['background'])
|
|
|
|
self.assertEqual(ln.sidebar_text['foreground'], colors['foreground'])
|
|
|
|
|
|
|
|
# Ensure line numbers aren't shown.
|
|
|
|
ln.hide_sidebar()
|
|
|
|
|
|
|
|
self.highlight_cfg = test_colors
|
2021-04-28 19:27:55 -03:00
|
|
|
# Nothing breaks with inactive line numbers.
|
2019-07-23 09:22:11 -03:00
|
|
|
ln.update_colors()
|
|
|
|
|
|
|
|
# Show line numbers, previous colors change is immediately effective.
|
|
|
|
ln.show_sidebar()
|
|
|
|
assert_colors_are_equal(test_colors)
|
|
|
|
|
|
|
|
# Call colors update with no change to the configured colors.
|
|
|
|
ln.update_colors()
|
|
|
|
assert_colors_are_equal(test_colors)
|
|
|
|
|
|
|
|
# Call the colors update with line numbers shown, change is picked up.
|
|
|
|
self.highlight_cfg = orig_colors
|
|
|
|
ln.update_colors()
|
|
|
|
assert_colors_are_equal(orig_colors)
|
|
|
|
|
|
|
|
|
2021-04-28 19:27:55 -03:00
|
|
|
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)
|
2021-04-30 00:52:47 -03:00
|
|
|
# See #43981 about macosx.setupApp(root, flist) causing failure.
|
2021-04-28 19:27:55 -03:00
|
|
|
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")
|
2022-02-02 21:59:24 -04:00
|
|
|
while (lineinfo := text.dlineinfo(index)) is not None:
|
2021-04-28 19:27:55 -03:00
|
|
|
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()
|
|
|
|
|
2021-06-02 20:53:41 -03:00
|
|
|
@run_in_tk_mainloop()
|
2021-04-28 19:27:55 -03:00
|
|
|
def test_single_empty_input(self):
|
|
|
|
self.do_input('\n')
|
|
|
|
yield
|
|
|
|
self.assert_sidebar_lines_end_with(['>>>', '>>>'])
|
|
|
|
|
2021-06-02 20:53:41 -03:00
|
|
|
@run_in_tk_mainloop()
|
2021-04-28 19:27:55 -03:00
|
|
|
def test_single_line_statement(self):
|
|
|
|
self.do_input('1\n')
|
|
|
|
yield
|
|
|
|
self.assert_sidebar_lines_end_with(['>>>', None, '>>>'])
|
|
|
|
|
2021-06-02 20:53:41 -03:00
|
|
|
@run_in_tk_mainloop()
|
2021-04-28 19:27:55 -03:00
|
|
|
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,
|
|
|
|
'>>>',
|
|
|
|
])
|
|
|
|
|
2021-06-02 20:53:41 -03:00
|
|
|
@run_in_tk_mainloop()
|
2021-04-28 19:27:55 -03:00
|
|
|
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()
|
|
|
|
|
2021-06-02 20:53:41 -03:00
|
|
|
@run_in_tk_mainloop()
|
2021-04-28 19:27:55 -03:00
|
|
|
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()
|
|
|
|
|
2021-06-02 20:53:41 -03:00
|
|
|
@run_in_tk_mainloop()
|
2021-04-28 19:27:55 -03:00
|
|
|
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, '>>>']
|
|
|
|
)
|
|
|
|
|
2021-06-02 20:53:41 -03:00
|
|
|
@run_in_tk_mainloop()
|
2021-04-28 19:27:55 -03:00
|
|
|
def test_very_long_wrapped_line(self):
|
2022-09-02 13:35:08 -03:00
|
|
|
with support.adjust_int_max_str_digits(11_111), \
|
|
|
|
swap_attr(self.shell, 'squeezer', None):
|
2021-04-28 19:27:55 -03:00
|
|
|
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)
|
|
|
|
|
2021-06-02 20:53:41 -03:00
|
|
|
@run_in_tk_mainloop()
|
2021-04-28 19:27:55 -03:00
|
|
|
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')))
|
|
|
|
|
2021-06-02 20:53:41 -03:00
|
|
|
@run_in_tk_mainloop()
|
2021-05-02 23:27:38 -03:00
|
|
|
def test_copy(self):
|
|
|
|
sidebar = self.shell.shell_sidebar
|
|
|
|
text = self.shell.text
|
|
|
|
|
|
|
|
first_line = get_end_linenumber(text)
|
|
|
|
|
|
|
|
self.do_input(dedent('''\
|
|
|
|
if True:
|
|
|
|
print(1)
|
|
|
|
|
|
|
|
'''))
|
|
|
|
yield
|
|
|
|
|
|
|
|
text.tag_add('sel', f'{first_line}.0', 'end-1c')
|
|
|
|
selected_text = text.get('sel.first', 'sel.last')
|
|
|
|
self.assertTrue(selected_text.startswith('if True:\n'))
|
|
|
|
self.assertIn('\n1\n', selected_text)
|
|
|
|
|
|
|
|
text.event_generate('<<copy>>')
|
|
|
|
self.addCleanup(text.clipboard_clear)
|
|
|
|
|
|
|
|
copied_text = text.clipboard_get()
|
|
|
|
self.assertEqual(copied_text, selected_text)
|
|
|
|
|
2021-06-02 20:53:41 -03:00
|
|
|
@run_in_tk_mainloop()
|
2021-05-02 23:27:38 -03:00
|
|
|
def test_copy_with_prompts(self):
|
|
|
|
sidebar = self.shell.shell_sidebar
|
|
|
|
text = self.shell.text
|
|
|
|
|
|
|
|
first_line = get_end_linenumber(text)
|
|
|
|
self.do_input(dedent('''\
|
|
|
|
if True:
|
2022-08-01 02:06:13 -03:00
|
|
|
print(1)
|
2021-05-02 23:27:38 -03:00
|
|
|
|
|
|
|
'''))
|
|
|
|
yield
|
|
|
|
|
|
|
|
text.tag_add('sel', f'{first_line}.3', 'end-1c')
|
|
|
|
selected_text = text.get('sel.first', 'sel.last')
|
|
|
|
self.assertTrue(selected_text.startswith('True:\n'))
|
|
|
|
|
|
|
|
selected_lines_text = text.get('sel.first linestart', 'sel.last')
|
|
|
|
selected_lines = selected_lines_text.split('\n')
|
2022-08-01 02:06:13 -03:00
|
|
|
selected_lines.pop() # Final '' is a split artifact, not a line.
|
|
|
|
# Expect a block of input and a single output line.
|
2021-05-02 23:27:38 -03:00
|
|
|
expected_prompts = \
|
2022-08-01 02:06:13 -03:00
|
|
|
['>>>'] + ['...'] * (len(selected_lines) - 2) + [None]
|
2021-05-02 23:27:38 -03:00
|
|
|
selected_text_with_prompts = '\n'.join(
|
|
|
|
line if prompt is None else prompt + ' ' + line
|
|
|
|
for prompt, line in zip(expected_prompts,
|
|
|
|
selected_lines,
|
|
|
|
strict=True)
|
|
|
|
) + '\n'
|
|
|
|
|
|
|
|
text.event_generate('<<copy-with-prompts>>')
|
|
|
|
self.addCleanup(text.clipboard_clear)
|
|
|
|
|
|
|
|
copied_text = text.clipboard_get()
|
|
|
|
self.assertEqual(copied_text, selected_text_with_prompts)
|
|
|
|
|
2021-04-28 19:27:55 -03:00
|
|
|
|
2019-07-23 09:22:11 -03:00
|
|
|
if __name__ == '__main__':
|
|
|
|
unittest.main(verbosity=2)
|