bpo-32631: IDLE: Enable zzdummy example extension module (GH-14491)
Make menu items work with formatter, add docstrings, add 100% tests. Co-authored-by: Terry Jan Reedy <tjreedy@udel.edu>
This commit is contained in:
parent
59f9b4e450
commit
e40e2a2cc9
|
@ -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
|
bpo-42508: Keep IDLE running on macOS. Remove obsolete workaround
|
||||||
that prevented running files with shortcuts when using new universal2
|
that prevented running files with shortcuts when using new universal2
|
||||||
installers built on macOS 11.
|
installers built on macOS 11.
|
||||||
|
|
|
@ -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
|
Shell Preferences: Auto-Squeeze Min. Lines is the minimum number of lines
|
||||||
of output to automatically "squeeze".
|
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.
|
||||||
|
''',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,8 @@ variables:
|
||||||
(There are a few more, but they are rarely useful.)
|
(There are a few more, but they are rarely useful.)
|
||||||
|
|
||||||
The extension class must not directly bind Window Manager (e.g. X) events.
|
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
|
Rather, it must define one or more virtual events, e.g. <<z-in>>, and
|
||||||
corresponding methods, e.g. zoom_height_event(). The virtual events will be
|
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
|
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
|
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
|
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
|
implement. (They are also not required to create keybindings, but in that
|
||||||
case there must be empty bindings in cofig-extensions.def)
|
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 = [
|
menudefs = [
|
||||||
('edit', [
|
('format', [
|
||||||
None, # Separator
|
('Z in', '<<z-in>>'),
|
||||||
('_Zoom Height', '<<zoom-height>>'),
|
('Z out', '<<z-out>>'),
|
||||||
] )
|
] )
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, editwin):
|
def __init__(self, editwin):
|
||||||
self.editwin = editwin
|
self.editwin = editwin
|
||||||
|
|
||||||
def zoom_height_event(self, event):
|
def z_in_event(self, event=None):
|
||||||
"...Do what you want here..."
|
"...Do what you want here..."
|
||||||
|
|
||||||
The final piece of the puzzle is the file "config-extensions.def", which is
|
The final piece of the puzzle is the file "config-extensions.def", which is
|
||||||
|
|
|
@ -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)
|
|
@ -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 idlelib.config import idleConf
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
ztext = idleConf.GetOption('extensions', 'ZzDummy', 'z-text')
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
return apply
|
||||||
|
|
||||||
|
|
||||||
class ZzDummy:
|
class ZzDummy:
|
||||||
|
"""Prepend or remove initial text from selected lines."""
|
||||||
|
|
||||||
## menudefs = [
|
# Extend the format menu.
|
||||||
## ('format', [
|
menudefs = [
|
||||||
## ('Z in', '<<z-in>>'),
|
('format', [
|
||||||
## ('Z out', '<<z-out>>'),
|
('Z in', '<<z-in>>'),
|
||||||
## ] )
|
('Z out', '<<z-out>>'),
|
||||||
## ]
|
] )
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, editwin):
|
def __init__(self, editwin):
|
||||||
|
"Initialize the settings for this extension."
|
||||||
|
self.editwin = editwin
|
||||||
self.text = editwin.text
|
self.text = editwin.text
|
||||||
z_in = False
|
self.formatter = editwin.fregion
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def reload(cls):
|
def reload(cls):
|
||||||
|
"Load class variables from config."
|
||||||
cls.ztext = idleConf.GetOption('extensions', 'ZzDummy', 'z-text')
|
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.
|
||||||
text = self.text
|
|
||||||
text.undo_block_start()
|
This is bound to the <<z-in>> virtual event when the extensions
|
||||||
for line in range(1, text.index('end')):
|
are loaded.
|
||||||
text.insert('%d.0', ztext)
|
"""
|
||||||
text.undo_block_stop()
|
return f'{self.ztext}{line}'
|
||||||
return "break"
|
|
||||||
|
@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.
|
||||||
|
"""
|
||||||
|
zlength = 0 if not line.startswith(self.ztext) else len(self.ztext)
|
||||||
|
return line[zlength:]
|
||||||
|
|
||||||
def z_out_event(self, event): pass
|
|
||||||
|
|
||||||
ZzDummy.reload()
|
ZzDummy.reload()
|
||||||
|
|
||||||
##if __name__ == "__main__":
|
|
||||||
## import unittest
|
if __name__ == "__main__":
|
||||||
## unittest.main('idlelib.idle_test.test_zzdummy',
|
import unittest
|
||||||
## verbosity=2, exit=False)
|
unittest.main('idlelib.idle_test.test_zzdummy', verbosity=2, exit=False)
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Finish zzdummy example extension module: make menu entries work;
|
||||||
|
add docstrings and tests with 100% coverage.
|
Loading…
Reference in New Issue