mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 05:31:20 +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 | ||||
| that prevented running files with shortcuts when using new universal2 | ||||
| installers built on macOS 11. | ||||
|  |  | |||
|  | @ -2316,7 +2316,15 @@ 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, | ||||
| or the current line if no selection. | ||||
| ''', | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
							
								
								
									
										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 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: | ||||
|     """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): | ||||
|         """ | ||||
|         """ | ||||
|         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" | ||||
|     @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. | ||||
|         """ | ||||
|         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) | ||||
|  |  | |||
|  | @ -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