mirror of
				https://github.com/python/cpython.git
				synced 2025-10-30 21:21:22 +00:00 
			
		
		
		
	[3.9] 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>
(cherry picked from commit e40e2a2cc9)
Co-authored-by: Cheryl Sabella <cheryl.sabella@gmail.com>
			
			
This commit is contained in:
		
							parent
							
								
									30e9ee3f43
								
							
						
					
					
						commit
						d82392face
					
				
					 6 changed files with 229 additions and 33 deletions
				
			
		|  | @ -3,6 +3,9 @@ Released on 2020-12-07? | ||||||
| ====================================== | ====================================== | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | 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 @@ def detach(self): | ||||||
| 
 | 
 | ||||||
| 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 | ||||||
|  |  | ||||||
							
								
								
									
										152
									
								
								Lib/idlelib/idle_test/test_zzdummy.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								Lib/idlelib/idle_test/test_zzdummy.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Terry Jan Reedy
						Terry Jan Reedy