Skip to content

gh-75710: IDLE: Add tests to some editor.py functions #3669

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 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
134 changes: 119 additions & 15 deletions Lib/idlelib/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,21 @@ def set_line_and_column(self, event=None):


def createmenubar(self):
"""Populate the menu bar widget for the editor window.

Each option on the menubar is itself a cascade-type Menu widget
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO in this case, the rest of the doc-string after the first line should be a comment instead, since it describes the method's code rather than it's purpose.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem goes deeper. There should be separate window-with-menu, frame, and text classes, and possibly an IDLE-menu class with a docstring that explains the menu system. And it maybe should be in mainmenu.py.

At present, the menu functions are scattered around this file. Right now, they should be at least gathered together in one section with one class-level block comment. Or maybe the added comment should be in mainmenu.py. This should be a separate PR.

with the menubar as the parent. The names, labels, and menu
shortcuts for the menubar items are stored in menu_specs. Each
submenu is subsequently populated in fill_menus(), except for
'Recent Files' which is added to the File menu here.

Instance variables:
menubar: Menu widget containing first level menu items.
menudict: Dictionary of {menuname: Menu instance} items. The keys
represent the valid menu items for this window and may be a
subset of all the menudefs available.
recent_files_menu: Menu widget contained within the 'file' menudict.
"""
mbar = self.menubar
self.menudict = menudict = {}
for name, label in self.menu_specs:
Expand Down Expand Up @@ -760,7 +775,13 @@ def ResetFont(self):
self.text['font'] = idleConf.GetFont(self.root, 'main','EditorWindow')

def RemoveKeybindings(self):
"Remove the keybindings before they are changed."
"""Remove the virtual, configurable keybindings.

This should be called before the keybindings are applied
in ApplyKeyBindings() otherwise the old bindings will still exist.
Note: this does not remove the Tk/Tcl keybindings attached to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add another blank line before the "Note:".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or leave out 'Note:'. I am going to revise these added docstrings.

Text widgets by default.
"""
# Called from configdialog.py
self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
for event, keylist in keydefs.items():
Expand All @@ -772,15 +793,21 @@ def RemoveKeybindings(self):
self.text.event_delete(event, *keylist)

def ApplyKeybindings(self):
"Update the keybindings after they are changed"
"""Apply the virtual, configurable keybindings.

The binding events are attached to self.text. Also, the
menu accelerator keys are updated to match the current
configuration.
"""
# Called from configdialog.py
self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
self.apply_bindings()
for extensionName in self.get_standard_extension_names():
xkeydefs = idleConf.GetExtensionBindings(extensionName)
if xkeydefs:
self.apply_bindings(xkeydefs)
#update menu accelerators
# Update menu accelerators.
# XXX - split into its own function and call it from here?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I don't like adding such comments to the code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Such comments should have initials and at least a year. But yes, they can hang around for decades. In this case I agree, as a separate method is needed to group together menu methods. Understanding this code requires knowing the menu term definitions.

menuEventDict = {}
for menu in self.mainmenu.menudefs:
menuEventDict[menu[0]] = {}
Expand Down Expand Up @@ -815,32 +842,43 @@ def set_notabs_indentwidth(self):
type='int')

def reset_help_menu_entries(self):
"Update the additional help entries on the Help menu"
"""Update the additional help entries on the Help menu.

First the existing additional help entries are removed from
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this doc-string, I find the added description of how the method works entirely unnecessary, because it is already similarly detailed in comments.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

the help menu, then the new help entries are added from idleConf.
"""
help_list = idleConf.GetAllExtraHelpSourcesList()
helpmenu = self.menudict['help']
# first delete the extra help entries, if any
# First delete the extra help entries, if any.
helpmenu_length = helpmenu.index(END)
if helpmenu_length > self.base_helpmenu_length:
helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length)
# then rebuild them
# Then rebuild them.
if help_list:
helpmenu.add_separator()
for entry in help_list:
cmd = self.__extra_help_callback(entry[1])
helpmenu.add_command(label=entry[0], command=cmd)
# and update the menu dictionary
# And update the menu dictionary.
self.menudict['help'] = helpmenu

def __extra_help_callback(self, helpfile):
"Create a callback with the helpfile value frozen at definition time"
"""Create a callback with the helpfile value frozen at definition time.

Args:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This style of describing args in doc-strings is non-standard. I think it would be shorter and simpler to just remove the "Args:" heading, and change the "Returns:" section into a single line of text.

Likewise for other methods.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Cheryl did this PR after we refactored configdialog. For that, we had some exceptionally wordy docstrings to keep things clear. This docstring should start with 'Return...'

helpfile: Filename or website to open.

Returns:
Function to open the helpfile.
"""
def display_extra_help(helpfile=helpfile):
if not helpfile.startswith(('www', 'http')):
helpfile = os.path.normpath(helpfile)
if sys.platform[:3] == 'win':
try:
os.startfile(helpfile)
except OSError as why:
tkMessageBox.showerror(title='Document Start Failure',
self.showerror(title='Document Start Failure',
Copy link
Member

@terryjreedy terryjreedy Sep 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked this. self.showerror is set in init .

message=str(why), parent=self.text)
else:
webbrowser.open(helpfile)
Expand Down Expand Up @@ -999,6 +1037,8 @@ def _close(self):
if self.color:
self.color.close(False)
self.color = None
# Allow code context to close its text.after calls.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does unbinding the event allow the code context to do anything?

Also this is surprising to find given the title of this PR. If kept, it should be mentioned in the description and in the final commit message as an additional fix.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that revising _close, which has been somewhat problematical, is a separate issue.

self.text.unbind('<<toggle-code-context>>')
self.text = None
self.tkinter_vars = None
self.per.close()
Expand Down Expand Up @@ -1062,6 +1102,11 @@ def load_extension(self, name):
self.text.bind(vevent, getattr(ins, methodname))

def apply_bindings(self, keydefs=None):
"""Add the event bindings in keydefs to self.text.

Args:
keydefs: Virtual events and keybinding definitions.
"""
if keydefs is None:
keydefs = self.mainmenu.default_keydefs
text = self.text
Expand All @@ -1071,9 +1116,28 @@ 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 list of tuples of the form:
[(menu1, [(submenu1a, '<<virtual event>>'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the menu and sub-menu names in the examples be stings?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be here. See previous comment about explaining menu terms once.

(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
Expand Down Expand Up @@ -1123,6 +1187,17 @@ def setvar(self, name, value, vartype=None):
raise NameError(name)

def get_var_obj(self, name, vartype=None):
"""Return a tkinter variable instance for the event.

Cache vars in self.tkinter_vars as {name: Var instance}.

Args:
name: Event name.
vartype: Tkinter Var type.

Returns:
Tkinter Var instance.
"""
var = self.tkinter_vars.get(name)
if not var and vartype:
# create a Tkinter variable object with self.text as master:
Expand Down Expand Up @@ -1630,8 +1705,16 @@ def run(self):
### end autoindent code ###

def prepstr(s):
# Helper to extract the underscore from a string, e.g.
# prepstr("Co_py") returns (2, "Copy").
"""Extract the underscore from a string.

For example, prepstr("Co_py") returns (2, "Copy").

Args:
s: String with underscore.

Returns:
Tuple of (position of underscore, string without underscore).
"""
i = s.find('_')
if i >= 0:
s = s[:i] + s[i+1:]
Expand All @@ -1645,6 +1728,18 @@ def prepstr(s):
}

def get_accelerator(keydefs, eventname):
"""Return a formatted string for the keybinding of an event.

Convert the first keybinding for a given event to a form that
can be displayed as an accelerator on the menu.

Args:
keydefs: Dictionary of valid events to keybindings.
eventname: Event to retrieve keybinding for.

Returns:
Formatted string of the keybinding.
"""
keylist = keydefs.get(eventname)
# issue10940: temporary workaround to prevent hang with OS X Cocoa Tk 8.5
# if not keylist:
Expand All @@ -1654,14 +1749,23 @@ def get_accelerator(keydefs, eventname):
"<<change-indentwidth>>"}):
return ""
s = keylist[0]
# Convert strings of the form -singlelowercase to -singleuppercase.
s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s)
# Convert certain keynames to their symbol.
s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
# Remove Key- from string.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first 2 added comments are helpful, the rest are obvious.

s = re.sub("Key-", "", s)
s = re.sub("Cancel","Ctrl-Break",s) # [email protected]
# Convert Cancel to Ctrl-Break.
s = re.sub("Cancel", "Ctrl-Break", s) # [email protected]
# Convert Control to Ctrl-.
s = re.sub("Control-", "Ctrl-", s)
# Change - to +.
s = re.sub("-", "+", s)
# Change >< to space.
s = re.sub("><", " ", s)
# Remove <.
s = re.sub("<", "", s)
# Remove >.
s = re.sub(">", "", s)
return s

Expand Down
Loading