Skip to content

gh-79951: IDLE - Convert menudefs to dictionary #11615

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 38 additions & 8 deletions Lib/idlelib/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -815,11 +815,11 @@ def ApplyKeybindings(self):
self.apply_bindings(xkeydefs)
#update menu accelerators
menuEventDict = {}
for menu in self.mainmenu.menudefs:
menuEventDict[menu[0]] = {}
for item in menu[1]:
for menu, items in self.mainmenu.menudefs.items():
menuEventDict[menu] = {}
for item in items:
if item:
menuEventDict[menu[0]][prepstr(item[0])[1]] = item[1]
menuEventDict[menu][prepstr(item[0])[1]] = item[1]
for menubarItem in self.menudict:
menu = self.menudict[menubarItem]
end = menu.index(END)
Expand Down Expand Up @@ -1104,17 +1104,47 @@ def apply_bindings(self, keydefs=None):
text.event_add(event, *keylist)

def fill_menus(self, menudefs=None, keydefs=None):
"""Add appropriate entries to the menus and submenus

Menus that are absent or None in self.menudict are ignored.
"""Add appropriate entries to the menus and submenus.

The default menudefs and keydefs are loaded from idlelib.mainmenu.
Menus that are absent or None in self.menudict are ignored. The
default menu type created for submenus from menudefs is `command`.
A submenu item of None results in a `separator` menu type.
A submenu name beginning with ! represents a `checkbutton` type.

The menus are stored in self.menudict.

Args:
menudefs: Menu and submenu names, underlines (shortcuts),
and events which is a dictionary of the form:
{menu1: [(submenu1a, '<<virtual event>>'),
(submenu1b, '<<virtual event>>'), ...],
menu2: [(submenu2a, '<<virtual event>>'),
(submenu2b, '<<virtual event>>'), ...],
}
Alternate format (may have been used in extensions):
[(menu1, [(submenu1a, '<<virtual event>>'),
(submenu1b, '<<virtual event>>'), ...]),
(menu2, [(submenu2a, '<<virtual event>>'),
(submenu2b, '<<virtual event>>'), ...]),
]
keydefs: Virtual events and keybinding definitions. Used for
the 'accelerator' text on the menu. Stored as a
dictionary of
{'<<virtual event>>': ['<binding1>', '<binding2>'],}
"""
if menudefs is None:
menudefs = self.mainmenu.menudefs
# menudefs was changed from a list of tuples to a dictionary.
# This conversion is needed for backward-compatibility for
# existing extensions that use the list format.
if isinstance(menudefs, list):
menudefs = dict(menudefs)
if keydefs is None:
keydefs = self.mainmenu.default_keydefs
menudict = self.menudict
text = self.text
for mname, entrylist in menudefs:
for mname, entrylist in menudefs.items():
menu = menudict.get(mname)
if not menu:
continue
Expand Down
107 changes: 105 additions & 2 deletions Lib/idlelib/idle_test/test_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from idlelib import editor
import unittest
from unittest import mock
from test.support import requires
from tkinter import Tk
import tkinter as tk
from functools import partial

Editor = editor.EditorWindow

Expand All @@ -13,7 +15,7 @@ class EditorWindowTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root = tk.Tk()
cls.root.withdraw()

@classmethod
Expand Down Expand Up @@ -42,5 +44,106 @@ class dummy():
self.assertEqual(func(dummy, inp), out)


class MenubarTest(unittest.TestCase):
"""Test functions involved with creating the menubar."""

@classmethod
def setUpClass(cls):
requires('gui')
root = cls.root = tk.Tk()
cls.root.withdraw()
# Test the functions called during the __init__ for
# EditorWindow that create the menubar and submenus.
# The class is mocked in order to prevent the functions
# from being called automatically.
w = cls.mock_editwin = mock.Mock(editor.EditorWindow)
w.menubar = tk.Menu(root, tearoff=False)
w.text = tk.Text(root)
w.tkinter_vars = {}

@classmethod
def tearDownClass(cls):
w = cls.mock_editwin
w.text.destroy()
w.menubar.destroy()
del w.menubar, w.text, w
cls.root.update_idletasks()
for id in cls.root.tk.call('after', 'info'):
cls.root.after_cancel(id)
cls.root.destroy()
del cls.root

def test_fill_menus(self):
eq = self.assertEqual
ed = editor.EditorWindow
w = self.mock_editwin
# Call real functions instead of mock.
fm = partial(editor.EditorWindow.fill_menus, w)
w.get_var_obj = ed.get_var_obj.__get__(w)

# Initialize top level menubar.
w.menudict = {}
edit = w.menudict['edit'] = tk.Menu(w.menubar, name='edit', tearoff=False)
win = w.menudict['windows'] = tk.Menu(w.menubar, name='windows', tearoff=False)
form = w.menudict['format'] = tk.Menu(w.menubar, name='format', tearoff=False)

# Submenus.
dict_menudefs = {'edit': [('_New', '<<open-new>>'),
None,
('!Deb_ug', '<<debug>>')],
'shell': [('_View', '<<view-restart>>'), ],
'windows': [('Zoom Height', '<<zoom-height>>')],
}
list_menudefs = [('edit', [('_New', '<<open-new>>'),
None,
('!Deb_ug', '<<debug>>')]),
('shell', [('_View', '<<view-restart>>'), ]),
('windows', [('Zoom Height', '<<zoom-height>>')]),
]
keydefs = {'<<zoom-height>>': ['<Alt-Key-9>']}
for menudefs in (dict_menudefs, list_menudefs):
with self.subTest(menudefs=menudefs):
fm(menudefs, keydefs)

eq(edit.index('end'), 2)
eq(edit.type(0), tk.COMMAND)
eq(edit.entrycget(0, 'label'), 'New')
eq(edit.entrycget(0, 'underline'), 0)
self.assertIsNotNone(edit.entrycget(0, 'command'))
with self.assertRaises(tk.TclError):
self.assertIsNone(edit.entrycget(0, 'var'))

eq(edit.type(1), tk.SEPARATOR)
with self.assertRaises(tk.TclError):
self.assertIsNone(edit.entrycget(1, 'label'))

eq(edit.type(2), tk.CHECKBUTTON)
# Strip !.
eq(edit.entrycget(2, 'label'), 'Debug')
# Check that underline ignores !.
eq(edit.entrycget(2, 'underline'), 3)
self.assertIsNotNone(edit.entrycget(2, 'var'))
self.assertIn('<<debug>>', w.tkinter_vars)

eq(win.index('end'), 0)
eq(win.entrycget(0, 'underline'), -1)
eq(win.entrycget(0, 'accelerator'), 'Alt+9')

eq(form.index('end'), None)
self.assertNotIn('shell', w.menudict)

# Cleanup menus by deleting all menu items.
edit.delete(0, 2)
win.delete(0)
form.delete(0)

# Test defaults.
w.mainmenu.menudefs = ed.mainmenu.menudefs
w.mainmenu.default_keydefs = ed.mainmenu.default_keydefs
fm()
eq(form.index('end'), 9) # Default Format menu has 10 items.
self.assertNotIn('run', w.menudict)


if __name__ == '__main__':
unittest.main(verbosity=2)
2 changes: 1 addition & 1 deletion Lib/idlelib/idle_test/test_mainmenu.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
class MainMenuTest(unittest.TestCase):

def test_menudefs(self):
actual = [item[0] for item in mainmenu.menudefs]
actual = list(mainmenu.menudefs.keys())
expect = ['file', 'edit', 'format', 'run', 'shell',
'debug', 'options', 'window', 'help']
self.assertEqual(actual, expect)
Expand Down
23 changes: 12 additions & 11 deletions Lib/idlelib/macosx.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,20 +165,21 @@ def overrideRootMenu(root, flist):
from idlelib import mainmenu
from idlelib import window

closeItem = mainmenu.menudefs[0][1][-2]
closeItem = mainmenu.menudefs['file'][-2]

# Remove the last 3 items of the file menu: a separator, close window and
# quit. Close window will be reinserted just above the save item, where
# it should be according to the HIG. Quit is in the application menu.
del mainmenu.menudefs[0][1][-3:]
mainmenu.menudefs[0][1].insert(6, closeItem)
del mainmenu.menudefs['file'][-3:]
mainmenu.menudefs['file'].insert(6, closeItem)

# Remove the 'About' entry from the help menu, it is in the application
# menu
del mainmenu.menudefs[-1][1][0:2]
del mainmenu.menudefs['help'][0:2]
# Remove the 'Configure Idle' entry from the options menu, it is in the
# application menu as 'Preferences'
del mainmenu.menudefs[-3][1][0:2]
del mainmenu.menudefs['options'][0:2]

menubar = Menu(root)
root.configure(menu=menubar)
menudict = {}
Expand Down Expand Up @@ -236,18 +237,18 @@ def help_dialog(event=None):
menudict['application'] = menu = Menu(menubar, name='apple',
tearoff=0)
menubar.add_cascade(label='IDLE', menu=menu)
mainmenu.menudefs.insert(0,
('application', [
('About IDLE', '<<about-idle>>'),
None,
]))
appmenu = {'application': [
('About IDLE', '<<about-idle>>'),
None,
]}
mainmenu.menudefs = {**appmenu, **mainmenu.menudefs}
if isCocoaTk():
# replace default About dialog with About IDLE one
root.createcommand('tkAboutDialog', about_dialog)
# replace default "Help" item in Help menu
root.createcommand('::tk::mac::ShowHelp', help_dialog)
# remove redundant "IDLE Help" from menu
del mainmenu.menudefs[-1][1][0]
del mainmenu.menudefs['help'][0]

def fixb2context(root):
'''Removed bad AquaTk Button-2 (right) and Paste bindings.
Expand Down
40 changes: 20 additions & 20 deletions Lib/idlelib/mainmenu.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
# without altering overrideRootMenu() as well.
# TODO: Make this more robust

menudefs = [
menudefs = {
# underscore prefixes character to underscore
('file', [
'file': [
('_New File', '<<open-new-window>>'),
('_Open...', '<<open-window-from-file>>'),
('Open _Module...', '<<open-module>>'),
Expand All @@ -36,9 +36,9 @@
None,
('_Close', '<<close-window>>'),
('E_xit', '<<close-all-windows>>'),
]),
],

('edit', [
'edit': [
('_Undo', '<<undo>>'),
('_Redo', '<<redo>>'),
None,
Expand All @@ -57,9 +57,9 @@
('E_xpand Word', '<<expand-word>>'),
('Show C_all Tip', '<<force-open-calltip>>'),
('Show Surrounding P_arens', '<<flash-paren>>'),
]),
],

('format', [
'format': [
('_Indent Region', '<<indent-region>>'),
('_Dedent Region', '<<dedent-region>>'),
('Comment _Out Region', '<<comment-region>>'),
Expand All @@ -70,52 +70,52 @@
('New Indent Width', '<<change-indentwidth>>'),
('F_ormat Paragraph', '<<format-paragraph>>'),
('S_trip Trailing Whitespace', '<<do-rstrip>>'),
]),
],

('run', [
'run': [
('Python Shell', '<<open-python-shell>>'),
('C_heck Module', '<<check-module>>'),
('R_un Module', '<<run-module>>'),
('Run... _Customized', '<<run-custom>>'),
]),

('shell', [
'shell': [
('_View Last Restart', '<<view-restart>>'),
('_Restart Shell', '<<restart-shell>>'),
None,
('_Previous History', '<<history-previous>>'),
('_Next History', '<<history-next>>'),
None,
('_Interrupt Execution', '<<interrupt-execution>>'),
]),
],

('debug', [
'debug': [
('_Go to File/Line', '<<goto-file-line>>'),
('!_Debugger', '<<toggle-debugger>>'),
('_Stack Viewer', '<<open-stack-viewer>>'),
('!_Auto-open Stack Viewer', '<<toggle-jit-stack-viewer>>'),
]),
],

('options', [
'options': [
('Configure _IDLE', '<<open-config-dialog>>'),
None,
('Show _Code Context', '<<toggle-code-context>>'),
('Zoom Height', '<<zoom-height>>'),
]),
],

('window', [
]),
'window': [
],

('help', [
'help': [
('_About IDLE', '<<about-idle>>'),
None,
('_IDLE Help', '<<help>>'),
('Python _Docs', '<<python-docs>>'),
]),
]
],
}

if find_spec('turtledemo'):
menudefs[-1][1].append(('Turtle Demo', '<<open-turtle-demo>>'))
menudefs['help'].append(('Turtle Demo', '<<open-turtle-demo>>'))

default_keydefs = idleConf.GetCurrentKeySet()

Expand Down
8 changes: 4 additions & 4 deletions Lib/idlelib/zzdummy.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@

class ZzDummy:

## menudefs = [
## ('format', [
## menudefs = {
## 'format': [
## ('Z in', '<<z-in>>'),
## ('Z out', '<<z-out>>'),
## ] )
## ]
## ]
## }

def __init__(self, editwin):
self.text = editwin.text
Expand Down