aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTerry Jan Reedy <tjreedy@udel.edu>2021-01-05 04:18:02 -0500
committerGitHub <noreply@github.com>2021-01-05 04:18:02 -0500
commita087a97438e922fcace357ff4c29806ff65838d8 (patch)
tree5904a9d715ee373f6fb3b5bfdced2adf091ee9d3
parentFix broken NEWS markup (GH-24110) (diff)
downloadcpython-a087a97438e922fcace357ff4c29806ff65838d8.tar.gz
cpython-a087a97438e922fcace357ff4c29806ff65838d8.tar.bz2
cpython-a087a97438e922fcace357ff4c29806ff65838d8.zip
[3.8] bpo-32631: IDLE: Enable zzdummy example extension module (GH-14491)
Make menu items work with formatter, add docstrings, add 100% tests. Co-authored-by: Cheryl Sabella <cheryl.sabella@gmail.com> Co-authored-by: Terry Jan Reedy <tjreedy@udel.edu> (cherry picked from commit e40e2a2cc94c554e7e245a8ca5a7432d31a95766)
-rw-r--r--Lib/idlelib/NEWS.txt3
-rw-r--r--Lib/idlelib/configdialog.py10
-rw-r--r--Lib/idlelib/extend.txt18
-rw-r--r--Lib/idlelib/idle_test/test_zzdummy.py152
-rw-r--r--Lib/idlelib/zzdummy.py73
-rw-r--r--Misc/NEWS.d/next/IDLE/2019-06-30-20-31-09.bpo-32631.e7_4BG.rst2
6 files changed, 227 insertions, 31 deletions
diff --git a/Lib/idlelib/NEWS.txt b/Lib/idlelib/NEWS.txt
index 27faa70bf72..bc15501a7b0 100644
--- a/Lib/idlelib/NEWS.txt
+++ b/Lib/idlelib/NEWS.txt
@@ -3,6 +3,9 @@ Released on 2020-12-??
======================================
+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 a84e1c5668f..73e64852c69 100644
--- a/Lib/idlelib/configdialog.py
+++ b/Lib/idlelib/configdialog.py
@@ -2316,7 +2316,15 @@ display when Code Context is turned on for an editor window.
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,
+or the current line if no selection.
+''',
}
diff --git a/Lib/idlelib/extend.txt b/Lib/idlelib/extend.txt
index c9cb2e8297e..b482f76c4fb 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. <<zoom-height>>, and
-corresponding methods, e.g. zoom_height_event(). The virtual events will be
+Rather, it must define one or more virtual events, e.g. <<z-in>>, 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
@@ -54,21 +54,21 @@ 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 ZoomHeight:
+class ZzDummy:
menudefs = [
- ('edit', [
- None, # Separator
- ('_Zoom Height', '<<zoom-height>>'),
- ])
+ ('format', [
+ ('Z in', '<<z-in>>'),
+ ('Z out', '<<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 00000000000..1013cdc3c46
--- /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
+from idlelib import format
+
+
+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_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()
+
+
+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 80844996466..1247e8f1cc0 100644
--- a/Lib/idlelib/zzdummy.py
+++ b/Lib/idlelib/zzdummy.py
@@ -1,42 +1,73 @@
-"Example extension, also used for testing."
+"""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
+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.formatter.get_region()
+ for pos in range(len(lines) - 1):
+ line = lines[pos]
+ lines[pos] = format_line(self, line)
+ self.formatter.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-in>>'),
-## ('Z out', '<<z-out>>'),
-## ] )
-## ]
+ # Extend the format menu.
+ menudefs = [
+ ('format', [
+ ('Z in', '<<z-in>>'),
+ ('Z out', '<<z-out>>'),
+ ] )
+ ]
def __init__(self, editwin):
+ "Initialize the settings for this extension."
+ self.editwin = editwin
self.text = editwin.text
- z_in = False
+ self.formatter = editwin.fregion
@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 <<z-in>> 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 <<z-out>> 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', ztest)
- 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 00000000000..c422f43b6d6
--- /dev/null
+++ b/Misc/NEWS.d/next/IDLE/2019-06-30-20-31-09.bpo-32631.e7_4BG.rst
@@ -0,0 +1,2 @@
+Finish zzdummy example extension module: make menu entries work;
+add docstrings and tests with 100% coverage.