From 8ea79ec7330eaab59aa379388c41c7db65ab0505 Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Sun, 30 Jun 2019 20:31:19 -0400 Subject: [PATCH 1/3] bpo-32631: IDLE: Enable zzdummy extension module * Complete functionality to add or remove prefix string to each line. * Add comments and docstrings to zzdummy. * Add unittests in test_zzdummy. * Add help section for extensions tab to configdialog. --- Lib/idlelib/configdialog.py | 9 +- Lib/idlelib/extend.txt | 16 +- Lib/idlelib/idle_test/test_zzdummy.py | 152 ++++++++++++++++++ Lib/idlelib/zzdummy.py | 71 +++++--- .../2019-06-30-20-31-09.bpo-32631.e7_4BG.rst | 1 + 5 files changed, 219 insertions(+), 30 deletions(-) create mode 100644 Lib/idlelib/idle_test/test_zzdummy.py create mode 100644 Misc/NEWS.d/next/IDLE/2019-06-30-20-31-09.bpo-32631.e7_4BG.rst diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index 82596498d34611..b5581e84d467f6 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -2314,7 +2314,14 @@ def detach(self): Shell Preferences: Auto-Squeeze Min. Lines is the minimum number of lines of output to automatically "squeeze". -''' +''', + 'Extensions': ''' +ZzDummy: This extension is provided as an example for how to create and +use an extension. Enable indicates whether the extension is active or not; +likewise enable_editor and enable_shell indicate which windows it will +be active on. For this extension, z-text is the text that will be inserted +at or removed from the beginning of the lines of selected text. +''', } diff --git a/Lib/idlelib/extend.txt b/Lib/idlelib/extend.txt index c9cb2e8297eb35..96f0e92a29d422 100644 --- a/Lib/idlelib/extend.txt +++ b/Lib/idlelib/extend.txt @@ -28,8 +28,8 @@ variables: (There are a few more, but they are rarely useful.) The extension class must not directly bind Window Manager (e.g. X) events. -Rather, it must define one or more virtual events, e.g. <>, and -corresponding methods, e.g. zoom_height_event(). The virtual events will be +Rather, it must define one or more virtual events, e.g. <>, and +corresponding methods, e.g. z_in_event(). The virtual events will be bound to the corresponding methods, and Window Manager events can then be bound to the virtual events. (This indirection is done so that the key bindings can easily be changed, and so that other sources of virtual events can exist, such @@ -56,19 +56,19 @@ case there must be empty bindings in cofig-extensions.def) Here is a complete example: -class ZoomHeight: +class ZzDummy: menudefs = [ - ('edit', [ - None, # Separator - ('_Zoom Height', '<>'), - ]) + ('format', [ + ('Z in', '<>'), + ('Z out', '<>'), + ] ) ] def __init__(self, editwin): self.editwin = editwin - def zoom_height_event(self, event): + def z_in_event(self, event=None): "...Do what you want here..." The final piece of the puzzle is the file "config-extensions.def", which is diff --git a/Lib/idlelib/idle_test/test_zzdummy.py b/Lib/idlelib/idle_test/test_zzdummy.py new file mode 100644 index 00000000000000..d5c2ef7241eb73 --- /dev/null +++ b/Lib/idlelib/idle_test/test_zzdummy.py @@ -0,0 +1,152 @@ +"Test zzdummy, coverage 100%." + +from idlelib import zzdummy +import unittest +from test.support import requires +from tkinter import Tk, Text +from unittest import mock +from idlelib import config +from idlelib import editor + + +usercfg = zzdummy.idleConf.userCfg +testcfg = { + 'main': config.IdleUserConfParser(''), + 'highlight': config.IdleUserConfParser(''), + 'keys': config.IdleUserConfParser(''), + 'extensions': config.IdleUserConfParser(''), +} +code_sample = """\ + +class C1(): + # Class comment. + def __init__(self, a, b): + self.a = a + self.b = b +""" + + +class DummyEditwin: + get_region = editor.EditorWindow.get_region + set_region = editor.EditorWindow.set_region + get_selection_indices = editor.EditorWindow.get_selection_indices + def __init__(self, root, text): + self.root = root + self.top = root + self.text = text + self.text.undo_block_start = mock.Mock() + self.text.undo_block_stop = mock.Mock() + + +class ZZDummyTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + root = cls.root = Tk() + root.withdraw() + text = cls.text = Text(cls.root) + cls.editor = DummyEditwin(root, text) + zzdummy.idleConf.userCfg = testcfg + + @classmethod + def tearDownClass(cls): + zzdummy.idleConf.userCfg = usercfg + del cls.editor, cls.text + cls.root.update_idletasks() + for id in cls.root.tk.call('after', 'info'): + cls.root.after_cancel(id) # Need for EditorWindow. + cls.root.destroy() + del cls.root + + def setUp(self): + text = self.text + text.insert('1.0', code_sample) + text.undo_block_start.reset_mock() + text.undo_block_stop.reset_mock() + zz = self.zz = zzdummy.ZzDummy(self.editor) + zzdummy.ZzDummy.ztext = '# ignore #' + + def tearDown(self): + self.text.delete('1.0', 'end') + del self.zz + + def checklines(self, text, value): + # Verify that there are lines being checked. + end_line = int(float(text.index('end'))) + + # Check each line for the starting text. + actual = [] + for line in range(1, end_line): + txt = text.get(f'{line}.0', f'{line}.end') + actual.append(txt.startswith(value)) + return actual + + def test_init(self): + zz = self.zz + self.assertEqual(zz.editwin, self.editor) + self.assertEqual(zz.text, self.editor.text) + + def test_reload(self): + self.assertEqual(self.zz.ztext, '# ignore #') + testcfg['extensions'].SetOption('ZzDummy', 'z-text', 'spam') + zzdummy.ZzDummy.reload() + self.assertEqual(self.zz.ztext, 'spam') + + def test_z_in_event(self): + eq = self.assertEqual + zz = self.zz + text = zz.text + eq(self.zz.ztext, '# ignore #') + + # No lines have the leading text. + expected = [False, False, False, False, False, False, False] + actual = self.checklines(text, zz.ztext) + eq(expected, actual) + + text.tag_add('sel', '2.0', '4.end') + eq(zz.z_in_event(), 'break') + expected = [False, True, True, True, False, False, False] + actual = self.checklines(text, zz.ztext) + eq(expected, actual) + + text.undo_block_start.assert_called_once() + text.undo_block_stop.assert_called_once() + + def test_z_out_event(self): + eq = self.assertEqual + zz = self.zz + text = zz.text + eq(self.zz.ztext, '# ignore #') + + # Prepend text. + text.tag_add('sel', '2.0', '5.end') + zz.z_in_event() + text.undo_block_start.reset_mock() + text.undo_block_stop.reset_mock() + + # Select a few lines to remove text. + text.tag_remove('sel', '1.0', 'end') + text.tag_add('sel', '3.0', '4.end') + eq(zz.z_out_event(), 'break') + expected = [False, True, False, False, True, False, False] + actual = self.checklines(text, zz.ztext) + eq(expected, actual) + + text.undo_block_start.assert_called_once() + text.undo_block_stop.assert_called_once() + + def test_roundtrip(self): + # Insert and remove to all code should give back original text. + zz = self.zz + text = zz.text + + text.tag_add('sel', '1.0', 'end-1c') + zz.z_in_event() + zz.z_out_event() + + self.assertEqual(text.get('1.0', 'end-1c'), code_sample) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/Lib/idlelib/zzdummy.py b/Lib/idlelib/zzdummy.py index 3c4b1d23b0d379..1b4b3c7bcef3c8 100644 --- a/Lib/idlelib/zzdummy.py +++ b/Lib/idlelib/zzdummy.py @@ -1,42 +1,71 @@ -"Example extension, also used for testing." +"""Example extension, also used for testing. + +See extend.txt for more details on creating an extension. +""" from idlelib.config import idleConf +from functools import wraps + + +def format_selection(format_line): + "Apply a formatting function to all of the selected lines." + + @wraps(format_line) + def apply(self, event=None): + head, tail, chars, lines = self.editwin.get_region() + for pos in range(len(lines) - 1): + line = lines[pos] + lines[pos] = format_line(self, line) + self.editwin.set_region(head, tail, chars, lines) + return 'break' -ztext = idleConf.GetOption('extensions', 'ZzDummy', 'z-text') + return apply class ZzDummy: + """Prepend or remove initial text from selected lines.""" -## menudefs = [ -## ('format', [ -## ('Z in', '<>'), -## ('Z out', '<>'), -## ] ) -## ] + # Extend the format menu. + menudefs = [ + ('format', [ + ('Z in', '<>'), + ('Z out', '<>'), + ] ) + ] def __init__(self, editwin): + "Initialize the settings for this extension." + self.editwin = editwin self.text = editwin.text - z_in = False @classmethod def reload(cls): + "Load class variables from config." cls.ztext = idleConf.GetOption('extensions', 'ZzDummy', 'z-text') - def z_in_event(self, event): + @format_selection + def z_in_event(self, line): + """Insert text at the beginning of each selected line. + + This is bound to the <> virtual event when the extensions + are loaded. """ + return f'{self.ztext}{line}' + + @format_selection + def z_out_event(self, line): + """Remove specific text from the beginning of each selected line. + + This is bound to the <> virtual event when the extensions + are loaded. """ - text = self.text - text.undo_block_start() - for line in range(1, text.index('end')): - text.insert('%d.0', ztext) - text.undo_block_stop() - return "break" + zlength = 0 if not line.startswith(self.ztext) else len(self.ztext) + return line[zlength:] - def z_out_event(self, event): pass ZzDummy.reload() -##if __name__ == "__main__": -## import unittest -## unittest.main('idlelib.idle_test.test_zzdummy', -## verbosity=2, exit=False) + +if __name__ == "__main__": + import unittest + unittest.main('idlelib.idle_test.test_zzdummy', verbosity=2, exit=False) diff --git a/Misc/NEWS.d/next/IDLE/2019-06-30-20-31-09.bpo-32631.e7_4BG.rst b/Misc/NEWS.d/next/IDLE/2019-06-30-20-31-09.bpo-32631.e7_4BG.rst new file mode 100644 index 00000000000000..116aa38eed15d2 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2019-06-30-20-31-09.bpo-32631.e7_4BG.rst @@ -0,0 +1 @@ +Add tests and docstrings for the zzdummy extension module. From 9bcfe5146a4ce77646280ebae4cd6102125e21ae Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Sat, 13 Jun 2020 11:46:50 -0400 Subject: [PATCH 2/3] Incorporate new formatter --- Lib/idlelib/idle_test/test_zzdummy.py | 4 ++-- Lib/idlelib/zzdummy.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/idlelib/idle_test/test_zzdummy.py b/Lib/idlelib/idle_test/test_zzdummy.py index d5c2ef7241eb73..1013cdc3c46f4f 100644 --- a/Lib/idlelib/idle_test/test_zzdummy.py +++ b/Lib/idlelib/idle_test/test_zzdummy.py @@ -7,6 +7,7 @@ from unittest import mock from idlelib import config from idlelib import editor +from idlelib import format usercfg = zzdummy.idleConf.userCfg @@ -27,13 +28,12 @@ def __init__(self, a, b): class DummyEditwin: - get_region = editor.EditorWindow.get_region - set_region = editor.EditorWindow.set_region get_selection_indices = editor.EditorWindow.get_selection_indices def __init__(self, root, text): self.root = root self.top = root self.text = text + self.fregion = format.FormatRegion(self) self.text.undo_block_start = mock.Mock() self.text.undo_block_stop = mock.Mock() diff --git a/Lib/idlelib/zzdummy.py b/Lib/idlelib/zzdummy.py index 1b4b3c7bcef3c8..b0912fe85beff2 100644 --- a/Lib/idlelib/zzdummy.py +++ b/Lib/idlelib/zzdummy.py @@ -12,11 +12,11 @@ def format_selection(format_line): @wraps(format_line) def apply(self, event=None): - head, tail, chars, lines = self.editwin.get_region() + head, tail, chars, lines = self.formatter.get_region() for pos in range(len(lines) - 1): line = lines[pos] lines[pos] = format_line(self, line) - self.editwin.set_region(head, tail, chars, lines) + self.formatter.set_region(head, tail, chars, lines) return 'break' return apply @@ -37,6 +37,7 @@ def __init__(self, editwin): "Initialize the settings for this extension." self.editwin = editwin self.text = editwin.text + self.formatter = editwin.fregion @classmethod def reload(cls): From 6efd20a1eae01952a3ec00abae406377816fc0ca Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Tue, 5 Jan 2021 01:49:01 -0500 Subject: [PATCH 3/3] Minor doc changes --- Lib/idlelib/NEWS.txt | 3 +++ Lib/idlelib/configdialog.py | 9 +++++---- Lib/idlelib/extend.txt | 2 +- Lib/idlelib/zzdummy.py | 1 + .../next/IDLE/2019-06-30-20-31-09.bpo-32631.e7_4BG.rst | 3 ++- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Lib/idlelib/NEWS.txt b/Lib/idlelib/NEWS.txt index 7167314ca7e598..b04ea7493477ea 100644 --- a/Lib/idlelib/NEWS.txt +++ b/Lib/idlelib/NEWS.txt @@ -3,6 +3,9 @@ Released on 2021-10-04? ====================================== +bpo-32631: Finish zzdummy example extension module: make menu entries +work; add docstrings and tests with 100% coverage. + bpo-42508: Keep IDLE running on macOS. Remove obsolete workaround that prevented running files with shortcuts when using new universal2 installers built on macOS 11. diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index 3632686533e48f..73e64852c69dfd 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -2319,10 +2319,11 @@ def detach(self): ''', 'Extensions': ''' ZzDummy: This extension is provided as an example for how to create and -use an extension. Enable indicates whether the extension is active or not; -likewise enable_editor and enable_shell indicate which windows it will -be active on. For this extension, z-text is the text that will be inserted -at or removed from the beginning of the lines of selected text. +use an extension. Enable indicates whether the extension is active or +not; likewise enable_editor and enable_shell indicate which windows it +will be active on. For this extension, z-text is the text that will be +inserted at or removed from the beginning of the lines of selected text, +or the current line if no selection. ''', } diff --git a/Lib/idlelib/extend.txt b/Lib/idlelib/extend.txt index 96f0e92a29d422..b482f76c4fb0f7 100644 --- a/Lib/idlelib/extend.txt +++ b/Lib/idlelib/extend.txt @@ -54,7 +54,7 @@ Extensions are not required to define menu entries for all the events they implement. (They are also not required to create keybindings, but in that case there must be empty bindings in cofig-extensions.def) -Here is a complete example: +Here is a partial example from zzdummy.py: class ZzDummy: diff --git a/Lib/idlelib/zzdummy.py b/Lib/idlelib/zzdummy.py index b0912fe85beff2..1247e8f1cc0528 100644 --- a/Lib/idlelib/zzdummy.py +++ b/Lib/idlelib/zzdummy.py @@ -1,6 +1,7 @@ """Example extension, also used for testing. See extend.txt for more details on creating an extension. +See config-extension.def for configuring an extension. """ from idlelib.config import idleConf diff --git a/Misc/NEWS.d/next/IDLE/2019-06-30-20-31-09.bpo-32631.e7_4BG.rst b/Misc/NEWS.d/next/IDLE/2019-06-30-20-31-09.bpo-32631.e7_4BG.rst index 116aa38eed15d2..c422f43b6d6dd8 100644 --- a/Misc/NEWS.d/next/IDLE/2019-06-30-20-31-09.bpo-32631.e7_4BG.rst +++ b/Misc/NEWS.d/next/IDLE/2019-06-30-20-31-09.bpo-32631.e7_4BG.rst @@ -1 +1,2 @@ -Add tests and docstrings for the zzdummy extension module. +Finish zzdummy example extension module: make menu entries work; +add docstrings and tests with 100% coverage.