8000 bpo-32631: IDLE: Enable zzdummy example extension module (GH-14491) · python/cpython@e40e2a2 · GitHub
[go: up one dir, main page]

Skip to content

Commit e40e2a2

Browse files
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>
1 parent 59f9b4e commit e40e2a2

File tree

6 files changed

+227
-31
lines changed

6 files changed

+227
-31
lines changed

Lib/idlelib/NEWS.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ Released on 2021-10-04?
33
======================================
44

55

6+
bpo-32631: Finish zzdummy example extension module: make menu entries
7+
work; add docstrings and tests with 100% coverage.
8+
69
bpo-42508: Keep IDLE running on macOS. Remove obsolete workaround
710
that prevented running files with shortcuts when using new universal2
811
installers built on macOS 11.

Lib/idlelib/configdialog.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2316,7 +2316,15 @@ def detach(self):
23162316
23172317
Shell Preferences: Auto-Squeeze Min. Lines is the minimum number of lines
23182318
of output to automatically "squeeze".
2319-
'''
2319+
''',
2320+
'Extensions': '''
2321+
ZzDummy: This extension is provided as an example for how to create and
2322+
use an extension. Enable indicates whether the extension is active or
2323+
not; likewise enable_editor and enable_shell indicate which windows it
2324+
will be active on. For this extension, z-text is the text that will be
2325+
inserted at or removed from the beginning of the lines of selected text,
2326+
or the current line if no selection.
2327+
''',
23202328
}
23212329

23222330

Lib/idlelib/extend.txt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ variables:
2828
(There are a few more, but they are rarely useful.)
2929

3030
The extension class must not directly bind Window Manager (e.g. X) events.
31-
Rather, it must define one or more virtual events, e.g. <<zoom-height>>, and
32-
corresponding methods, e.g. zoom_height_event(). The virtual events will be
31+
Rather, it must define one or more virtual events, e.g. <<z-in>>, and
32+
corresponding methods, e.g. z_in_event(). The virtual events will be
3333
bound to the corresponding methods, and Window Manager events can then be bound
3434
to the virtual events. (This indirection is done so that the key bindings can
3535
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
5454
implement. (They are also not required to create keybindings, but in that
5555
case there must be empty bindings in cofig-extensions.def)
5656

57-
Here is a complete example:
57+
Here is a partial example from zzdummy.py:
5858

59-
class ZoomHeight:
59+
class ZzDummy:
6060

6161
menudefs = [
62-
('edit', [
63-
None, # Separator
64-
('_Zoom Height', '<<zoom-height>>'),
65-
])
62+
('format', [
63+
('Z in', '<<z-in>>'),
64+
('Z out', '<<z-out>>'),
65+
] )
6666
]
6767

6868
def __init__(self, editwin):
6969
self.editwin = editwin
7070

71-
def zoom_height_event(self, event):
71+
def z_in_event(self, event=None):
7272
"...Do what you want here..."
7373

7474
The final piece of the puzzle is the file "config-extensions.def", which is

Lib/idlelib/idle_test/test_zzdummy.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
"Test zzdummy, coverage 100%."
2+
3+
from idlelib import zzdummy
4+
import unittest
5+
from test.support import requires
6+
from tkinter import Tk, Text
7+
from unittest import mock
8+
from idlelib import config
9+
from idlelib import editor
10+
from idlelib import format
11+
12+
13+
usercfg = zzdummy.idleConf.userCfg
14+
testcfg = {
15+
'main': config.IdleUserConfParser(''),
16+
'highlight': config.IdleUserConfParser(''),
17+
'keys': config.IdleUserConfParser(''),
18+
'extensions': config.IdleUserConfParser(''),
19+
}
20+
code_sample = """\
21+
22+
class C1():
23+
# Class comment.
24+
def __init__(self, a, b):
25+
self.a = a
26+
self.b = b
27+
"""
28+
29+
30+
class DummyEditwin:
31+
get_selection_indices = editor.EditorWindow.get_selection_indices
32+
def __init__(self, root, text):
33+
self.root = root
34+
self.top = root
35+
self.text = text
36+
self.fregion = format.FormatRegion(self)
37+
self.text.undo_block_start = mock.Mock()
38+
self.text.undo_block_stop = mock.Mock()
39+
40+
41+
class ZZDummyTest(unittest.TestCase):
42+
43+
@classmethod
44+
def setUpClass(cls):
45+
requires('gui')
46+
root = cls.root = Tk()
47+
root.withdraw()
48+
text = cls.text = Text(cls.root)
49+
cls.editor = DummyEditwin(root, text)
50+
zzdummy.idleConf.userCfg = testcfg
51+
52+
@classmethod
53+
def tearDownClass(cls):
54+
zzdummy.idleConf.userCfg = usercfg
55+
del cls.editor, cls.text
56+
cls.root.update_idletasks()
57+
for id in cls.root.tk.call('after', 'info'):
58+
cls.root.after_cancel(id) # Need for EditorWindow.
59+
cls.root.destroy()
60+
del cls.root
61+
62+
def setUp(self):
63+
text = self.text
64+
text.insert('1.0', code_sample)
65+
text.undo_block_start.reset_mock()
66+
text.undo_block_stop.reset_mock()
67+
zz = self.zz = zzdummy.ZzDummy(self.editor)
68+
zzdummy.ZzDummy.ztext = '# ignore #'
69+
70+
def tearDown(self):
71+
self.text.delete('1.0', 'end')
72+
del self.zz
73+
74+
def checklines(self, text, value):
75+
# Verify that there are lines being checked.
76+
end_line = int(float(text.index('end')))
77+
78+
# Check each line for the starting text.
79+
actual = []
80+
for line in range(1, end_line):
81+
txt = text.get(f'{line}.0', f'{line}.end')
82+
actual.append(txt.startswith(value))
83+
return actual
84+
85+
def test_init(self):
86+
zz = self.zz
87+
self.assertEqual(zz.editwin, self.editor)
88+
self.assertEqual(zz.text, self.editor.text)
89+
90+
def test_reload(self):
91+
self.assertEqual(self.zz.ztext, '# ignore #')
92+
testcfg['extensions'].SetOption('ZzDummy', 'z-text', 'spam')
93+
zzdummy.ZzDummy.reload()
94+
self.assertEqual(self.zz.ztext, 'spam')
95+
96+
def test_z_in_event(self):
97+
eq = self.assertEqual
98+
zz = self.zz
99+
text = zz.text
100+
eq(self.zz.ztext, '# ignore #')
101+
102+
# No lines have the leading text.
103+
expected = [False, False, False, False, False, False, False]
104+
actual = self.checklines(text, zz.ztext)
105+
eq(expected, actual)
106+
107+
text.tag_add('sel', '2.0', '4.end')
108+
eq(zz.z_in_event(), 'break')
109+
expected = [False, True, True, True, False, False, False]
110+
actual = self.checklines(text, zz.ztext)
111+
eq(expected, actual)
112+
113+
text.undo_block_start.assert_called_once()
114+
text.undo_block_stop.assert_called_once()
115+
116+
def test_z_out_event(self):
117+
eq = self.assertEqual
118+
zz = self.zz
119+
text = zz.text
120+
eq(self.zz.ztext, '# ignore #')
121+
122+
# Prepend text.
123+
text.tag_add('sel', '2.0', '5.end')
124+
zz.z_in_event()
125+
text.undo_block_start.reset_mock()
126+
text.undo_block_stop.reset_mock()
127+
128+
# Select a few lines to remove text.
129+
text.tag_remove('sel', '1.0', 'end')
130+
text.tag_add('sel', '3.0', '4.end')
131+
eq(zz.z_out_event(), 'break')
132+
expected = [False, True, False, False, True, False, False]
133+
actual = self.checklines(text, zz.ztext)
134+
eq(expected, actual)
135+
136+
text.undo_block_start.assert_called_once()
137+
text.undo_block_stop.assert_called_once()
138+
139+
def test_roundtrip(self):
140+
# Insert and remove to all code should give back original text.
141+
zz = self.zz
142+
text = zz.text
143+
144+
text.tag_add('sel', '1.0', 'end-1c')
145+
zz.z_in_event()
146+
zz.z_out_event()
147+
148+
self.assertEqual(text.get('1.0', 'end-1c'), code_sample)
149+
150+
151+
if __name__ == '__main__':
152+
unittest.main(verbosity=2)

Lib/idlelib/zzdummy.py

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,73 @@
1-
"Example extension, also used for testing."
1+
"""Example extension, also used for testing.
2+
3+
See extend.txt for more details on creating an extension.
4+
See config-extension.def for configuring an extension.
5+
"""
26

37
from idlelib.config import idleConf
8+
from functools import wraps
9+
10+
11+
def format_selection(format_line):
12+
"Apply a formatting function to all of the selected lines."
13+
14+
@wraps(format_line)
15+
def apply(self, event=None):
16+
head, tail, chars, lines = self.formatter.get_region()
17+
for pos in range(len(lines) - 1):
18+
line = lines[pos]
19+
lines[pos] = format_line(self, line)
20+
self.formatter.set_region(head, tail, chars, lines)
21+
return 'break'
422

5-
ztext = idleConf.GetOption('extensions', 'ZzDummy', 'z-text')
23+
return apply
624

725

826
class ZzDummy:
27+
"""Prepend or remove initial text from selected lines."""
928

10-
## menudefs = [
11-
## ('format', [
12-
## ('Z in', '<<z-in>>'),
13-
## ('Z out', '<<z-out>>'),
14-
## ] )
15-
## ]
29+
# Extend the format menu.
30+
menudefs = [
31+
('format', [
32+
('Z in', '<<z-in>>'),
33+
('Z out', '<<z-out>>'),
34+
] )
35+
]
1636

1737
def __init__(self, editwin):
38+
"Initialize the settings for this extension."
39+
self.editwin = editwin
1840
self.text = editwin.text
19-
z_in = False
41+
self.formatter = editwin.fregion
2042

2143
@classmethod
2244
def reload(cls):
45+
"Load class variables from config."
2346
cls.ztext = idleConf.GetOption('extensions', 'ZzDummy', 'z-text')
2447

25-
def z_in_event(self, event):
48+
@format_selection
49+
def z_in_event(self, line):
50+
"""Insert text at the beginning of each selected line.
51+
52+
This is bound to the <<z-in>> virtual event when the extensions
53+
are loaded.
2654
"""
55+
return f'{self.ztext}{line}'
56+
57+
@format_selection
58+
def z_out_event(self, line):
59+
"""Remove specific text from the beginning of each selected line.
60+
61+
This is bound to the <<z-out>> virtual event when the extensions
62+
are loaded.
2763
"""
28-
text = self.text
29-
text.undo_block_start()
30-
for line in range(1, text.index('end')):
31-
text.insert('%d.0', ztext)
32-
text.undo_block_stop()
33-
return "break"
64+
zlength = 0 if not line.startswith(self.ztext) else len(self.ztext)
65+
return line[zlength:]
3466

35-
def z_out_event(self, event): pass
3667

3768
ZzDummy.reload()
3869

39-
##if __name__ == "__main__":
40-
## import unittest
41-
## unittest.main('idlelib.idle_test.test_zzdummy',
42-
## verbosity=2, exit=False)
70+
71+
if __name__ == "__main__":
72+
import unittest
73+
unittest.main('idlelib.idle_test.test_zzdummy', verbosity=2, exit=False)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Finish zzdummy example extension module: make menu entries work;
2+
add docstrings and tests with 100% coverage.

0 commit comments

Comments
 (0)
0