diff options
Diffstat (limited to 'Lib/idlelib')
85 files changed, 6302 insertions, 2398 deletions
diff --git a/Lib/idlelib/AutoComplete.py b/Lib/idlelib/AutoComplete.py index f3660307d3..b20512dfa0 100644 --- a/Lib/idlelib/AutoComplete.py +++ b/Lib/idlelib/AutoComplete.py @@ -226,3 +226,8 @@ class AutoComplete: namespace = sys.modules.copy() namespace.update(__main__.__dict__) return eval(name, namespace) + + +if __name__ == '__main__': + from unittest import main + main('idlelib.idle_test.test_autocomplete', verbosity=2) diff --git a/Lib/idlelib/AutoCompleteWindow.py b/Lib/idlelib/AutoCompleteWindow.py index f666ea628b..2ee6878396 100644 --- a/Lib/idlelib/AutoCompleteWindow.py +++ b/Lib/idlelib/AutoCompleteWindow.py @@ -192,6 +192,7 @@ class AutoCompleteWindow: scrollbar.config(command=listbox.yview) scrollbar.pack(side=RIGHT, fill=Y) listbox.pack(side=LEFT, fill=BOTH, expand=True) + acw.lift() # work around bug in Tk 8.5.18+ (issue #24570) # Initialize the listbox selection self.listbox.select_set(self._binary_search(self.start)) diff --git a/Lib/idlelib/AutoExpand.py b/Lib/idlelib/AutoExpand.py index 9e93d57d65..7059054281 100644 --- a/Lib/idlelib/AutoExpand.py +++ b/Lib/idlelib/AutoExpand.py @@ -1,3 +1,17 @@ +'''Complete the current word before the cursor with words in the editor. + +Each menu selection or shortcut key selection replaces the word with a +different word with the same prefix. The search for matches begins +before the target and moves toward the top of the editor. It then starts +after the cursor and moves down. It then returns to the original word and +the cycle starts again. + +Changing the current text line or leaving the cursor in a different +place before requesting the next selection causes AutoExpand to reset +its state. + +This is an extension file and there is only one instance of AutoExpand. +''' import string import re @@ -20,6 +34,7 @@ class AutoExpand: self.state = None def expand_word_event(self, event): + "Replace the current word with the next expansion." curinsert = self.text.index("insert") curline = self.text.get("insert linestart", "insert lineend") if not self.state: @@ -46,6 +61,7 @@ class AutoExpand: return "break" def getwords(self): + "Return a list of words that match the prefix before the cursor." word = self.getprevword() if not word: return [] @@ -76,8 +92,13 @@ class AutoExpand: return words def getprevword(self): + "Return the word prefix before the cursor." line = self.text.get("insert linestart", "insert") i = len(line) while i > 0 and line[i-1] in self.wordchars: i = i-1 return line[i:] + +if __name__ == '__main__': + import unittest + unittest.main('idlelib.idle_test.test_autoexpand', verbosity=2) diff --git a/Lib/idlelib/Bindings.py b/Lib/idlelib/Bindings.py index 65c0317e60..ab25ff18b6 100644 --- a/Lib/idlelib/Bindings.py +++ b/Lib/idlelib/Bindings.py @@ -8,9 +8,16 @@ the PythonShell window, and a Format menu which is only present in the Editor windows. """ -import sys +from importlib.util import find_spec + from idlelib.configHandler import idleConf -from idlelib import macosxSupport + +# Warning: menudefs is altered in macosxSupport.overrideRootMenu() +# after it is determined that an OS X Aqua Tk is in use, +# which cannot be done until after Tk() is first called. +# Do not alter the 'file', 'options', or 'help' cascades here +# without altering overrideRootMenu() as well. +# TODO: Make this more robust menudefs = [ # underscore prefixes character to underscore @@ -70,7 +77,7 @@ menudefs = [ ('!_Auto-open Stack Viewer', '<<toggle-jit-stack-viewer>>'), ]), ('options', [ - ('_Configure IDLE...', '<<open-config-dialog>>'), + ('Configure _IDLE', '<<open-config-dialog>>'), None, ]), ('help', [ @@ -81,27 +88,7 @@ menudefs = [ ]), ] -if macosxSupport.runningAsOSXApp(): - # Running as a proper MacOS application bundle. This block restructures - # the menus a little to make them conform better to the HIG. - - quitItem = menudefs[0][1][-1] - closeItem = menudefs[0][1][-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 menudefs[0][1][-3:] - menudefs[0][1].insert(6, closeItem) - - # Remove the 'About' entry from the help menu, it is in the application - # menu - del menudefs[-1][1][0:2] - - # Remove the 'Configure' entry from the options menu, it is in the - # application menu as 'Preferences' - del menudefs[-2][1][0:2] +if find_spec('turtledemo'): + menudefs[-1][1].append(('Turtle Demo', '<<open-turtle-demo>>')) default_keydefs = idleConf.GetCurrentKeySet() - -del sys diff --git a/Lib/idlelib/CallTipWindow.py b/Lib/idlelib/CallTipWindow.py index 8e29dabaea..8e68a76b2a 100644 --- a/Lib/idlelib/CallTipWindow.py +++ b/Lib/idlelib/CallTipWindow.py @@ -2,9 +2,8 @@ After ToolTip.py, which uses ideas gleaned from PySol Used by the CallTips IDLE extension. - """ -from tkinter import * +from tkinter import Toplevel, Label, LEFT, SOLID, TclError HIDE_VIRTUAL_EVENT_NAME = "<<calltipwindow-hide>>" HIDE_SEQUENCES = ("<Key-Escape>", "<FocusOut>") @@ -73,6 +72,7 @@ class CallTip: background="#ffffe0", relief=SOLID, borderwidth=1, font = self.widget['font']) self.label.pack() + tw.lift() # work around bug in Tk 8.5.18+ (issue #24570) self.checkhideid = self.widget.bind(CHECKHIDE_VIRTUAL_EVENT_NAME, self.checkhide_event) @@ -133,37 +133,29 @@ class CallTip: return bool(self.tipwindow) - -############################### -# -# Test Code -# -class container: # Conceptually an editor_window - def __init__(self): - root = Tk() - text = self.text = Text(root) - text.pack(side=LEFT, fill=BOTH, expand=1) - text.insert("insert", "string.split") - root.update() - self.calltip = CallTip(text) - - text.event_add("<<calltip-show>>", "(") - text.event_add("<<calltip-hide>>", ")") - text.bind("<<calltip-show>>", self.calltip_show) - text.bind("<<calltip-hide>>", self.calltip_hide) - - text.focus_set() - root.mainloop() - - def calltip_show(self, event): - self.calltip.showtip("Hello world") - - def calltip_hide(self, event): - self.calltip.hidetip() - -def main(): - # Test code - c=container() +def _calltip_window(parent): # htest # + from tkinter import Toplevel, Text, LEFT, BOTH + + top = Toplevel(parent) + top.title("Test calltips") + top.geometry("200x100+%d+%d" % (parent.winfo_rootx() + 200, + parent.winfo_rooty() + 150)) + text = Text(top) + text.pack(side=LEFT, fill=BOTH, expand=1) + text.insert("insert", "string.split") + top.update() + calltip = CallTip(text) + + def calltip_show(event): + calltip.showtip("(s=Hello world)", "insert", "end") + def calltip_hide(event): + calltip.hidetip() + text.event_add("<<calltip-show>>", "(") + text.event_add("<<calltip-hide>>", ")") + text.bind("<<calltip-show>>", calltip_show) + text.bind("<<calltip-hide>>", calltip_hide) + text.focus_set() if __name__=='__main__': - main() + from idlelib.idle_test.htest import run + run(_calltip_window) diff --git a/Lib/idlelib/ClassBrowser.py b/Lib/idlelib/ClassBrowser.py index 71176cd701..d09c52fe4d 100644 --- a/Lib/idlelib/ClassBrowser.py +++ b/Lib/idlelib/ClassBrowser.py @@ -19,13 +19,23 @@ from idlelib.WindowList import ListedToplevel from idlelib.TreeWidget import TreeNode, TreeItem, ScrolledCanvas from idlelib.configHandler import idleConf +file_open = None # Method...Item and Class...Item use this. +# Normally PyShell.flist.open, but there is no PyShell.flist for htest. + class ClassBrowser: - def __init__(self, flist, name, path): + def __init__(self, flist, name, path, _htest=False): # XXX This API should change, if the file doesn't end in ".py" # XXX the code here is bogus! + """ + _htest - bool, change box when location running htest. + """ + global file_open + if not _htest: + file_open = PyShell.flist.open self.name = name self.file = os.path.join(path[0], self.name + ".py") + self._htest = _htest self.init(flist) def close(self, event=None): @@ -40,10 +50,13 @@ class ClassBrowser: self.top = top = ListedToplevel(flist.root) top.protocol("WM_DELETE_WINDOW", self.close) top.bind("<Escape>", self.close) + if self._htest: # place dialog below parent if running htest + top.geometry("+%d+%d" % + (flist.root.winfo_rootx(), flist.root.winfo_rooty() + 200)) self.settitle() top.focus_set() # create scrolled canvas - theme = idleConf.GetOption('main','Theme','name') + theme = idleConf.CurrentTheme() background = idleConf.GetHighlight(theme, 'normal')['background'] sc = ScrolledCanvas(top, bg=background, highlightthickness=0, takefocus=1) sc.frame.pack(expand=1, fill="both") @@ -94,7 +107,7 @@ class ModuleBrowserTreeItem(TreeItem): return [] try: dict = pyclbr.readmodule_ex(name, [dir] + sys.path) - except ImportError as msg: + except ImportError: return [] items = [] self.classes = {} @@ -163,7 +176,7 @@ class ClassBrowserTreeItem(TreeItem): def OnDoubleClick(self): if not os.path.exists(self.file): return - edit = PyShell.flist.open(self.file) + edit = file_open(self.file) if hasattr(self.cl, 'lineno'): lineno = self.cl.lineno edit.gotoline(lineno) @@ -199,10 +212,10 @@ class MethodBrowserTreeItem(TreeItem): def OnDoubleClick(self): if not os.path.exists(self.file): return - edit = PyShell.flist.open(self.file) + edit = file_open(self.file) edit.gotoline(self.cl.methods[self.name]) -def main(): +def _class_browser(parent): #Wrapper for htest try: file = __file__ except NameError: @@ -213,9 +226,11 @@ def main(): file = sys.argv[0] dir, file = os.path.split(file) name = os.path.splitext(file)[0] - ClassBrowser(PyShell.flist, name, [dir]) - if sys.stdin is sys.__stdin__: - mainloop() + flist = PyShell.PyShellFileList(parent) + global file_open + file_open = flist.open + ClassBrowser(flist, name, [dir], _htest=True) if __name__ == "__main__": - main() + from idlelib.idle_test.htest import run + run(_class_browser) diff --git a/Lib/idlelib/CodeContext.py b/Lib/idlelib/CodeContext.py index 84491d5a9d..44783b69d0 100644 --- a/Lib/idlelib/CodeContext.py +++ b/Lib/idlelib/CodeContext.py @@ -15,8 +15,8 @@ import re from sys import maxsize as INFINITY from idlelib.configHandler import idleConf -BLOCKOPENERS = set(["class", "def", "elif", "else", "except", "finally", "for", - "if", "try", "while", "with"]) +BLOCKOPENERS = {"class", "def", "elif", "else", "except", "finally", "for", + "if", "try", "while", "with"} UPDATEINTERVAL = 100 # millisec FONTUPDATEINTERVAL = 1000 # millisec diff --git a/Lib/idlelib/ColorDelegator.py b/Lib/idlelib/ColorDelegator.py index 61e2be47c7..9f31349604 100644 --- a/Lib/idlelib/ColorDelegator.py +++ b/Lib/idlelib/ColorDelegator.py @@ -2,7 +2,6 @@ import time import re import keyword import builtins -from tkinter import * from idlelib.Delegator import Delegator from idlelib.configHandler import idleConf @@ -32,7 +31,6 @@ def make_pat(): prog = re.compile(make_pat(), re.S) idprog = re.compile(r"\s+(\w+)", re.S) -asprog = re.compile(r".*?\b(as)\b") class ColorDelegator(Delegator): @@ -40,7 +38,6 @@ class ColorDelegator(Delegator): Delegator.__init__(self) self.prog = prog self.idprog = idprog - self.asprog = asprog self.LoadTagDefs() def setdelegate(self, delegate): @@ -63,7 +60,7 @@ class ColorDelegator(Delegator): self.tag_raise('sel') def LoadTagDefs(self): - theme = idleConf.GetOption('main','Theme','name') + theme = idleConf.CurrentTheme() self.tagdefs = { "COMMENT": idleConf.GetHighlight(theme, "comment"), "KEYWORD": idleConf.GetHighlight(theme, "keyword"), @@ -72,7 +69,6 @@ class ColorDelegator(Delegator): "DEFINITION": idleConf.GetHighlight(theme, "definition"), "SYNC": {'background':None,'foreground':None}, "TODO": {'background':None,'foreground':None}, - "BREAK": idleConf.GetHighlight(theme, "break"), "ERROR": idleConf.GetHighlight(theme, "error"), # The following is used by ReplaceDialog: "hit": idleConf.GetHighlight(theme, "hit"), @@ -214,22 +210,6 @@ class ColorDelegator(Delegator): self.tag_add("DEFINITION", head + "+%dc" % a, head + "+%dc" % b) - elif value == "import": - # color all the "as" words on same line, except - # if in a comment; cheap approximation to the - # truth - if '#' in chars: - endpos = chars.index('#') - else: - endpos = len(chars) - while True: - m1 = self.asprog.match(chars, b, endpos) - if not m1: - break - a, b = m1.span(1) - self.tag_add("KEYWORD", - head + "+%dc" % a, - head + "+%dc" % b) m = self.prog.search(chars, m.end()) if "SYNC" in self.tag_names(next + "-1c"): head = next @@ -253,17 +233,24 @@ class ColorDelegator(Delegator): for tag in self.tagdefs: self.tag_remove(tag, "1.0", "end") -def main(): +def _color_delegator(parent): # htest # + from tkinter import Toplevel, Text from idlelib.Percolator import Percolator - root = Tk() - root.wm_protocol("WM_DELETE_WINDOW", root.quit) - text = Text(background="white") + + top = Toplevel(parent) + top.title("Test ColorDelegator") + top.geometry("200x100+%d+%d" % (parent.winfo_rootx() + 200, + parent.winfo_rooty() + 150)) + source = "if somename: x = 'abc' # comment\nprint\n" + text = Text(top, background="white") text.pack(expand=1, fill="both") + text.insert("insert", source) text.focus_set() + p = Percolator(text) d = ColorDelegator() p.insertfilter(d) - root.mainloop() if __name__ == "__main__": - main() + from idlelib.idle_test.htest import run + run(_color_delegator) diff --git a/Lib/idlelib/Debugger.py b/Lib/idlelib/Debugger.py index d4872ed42a..250422edbb 100644 --- a/Lib/idlelib/Debugger.py +++ b/Lib/idlelib/Debugger.py @@ -1,6 +1,5 @@ import os import bdb -import types from tkinter import * from idlelib.WindowList import ListedToplevel from idlelib.ScrolledList import ScrolledList @@ -18,7 +17,10 @@ class Idb(bdb.Bdb): self.set_step() return message = self.__frame2message(frame) - self.gui.interaction(message, frame) + try: + self.gui.interaction(message, frame) + except TclError: # When closing debugger window with [x] in 3.x + pass def user_exception(self, frame, info): if self.in_rpc_code(frame): @@ -60,8 +62,42 @@ class Debugger: self.frame = None self.make_gui() self.interacting = 0 + self.nesting_level = 0 def run(self, *args): + # Deal with the scenario where we've already got a program running + # in the debugger and we want to start another. If that is the case, + # our second 'run' was invoked from an event dispatched not from + # the main event loop, but from the nested event loop in 'interaction' + # below. So our stack looks something like this: + # outer main event loop + # run() + # <running program with traces> + # callback to debugger's interaction() + # nested event loop + # run() for second command + # + # This kind of nesting of event loops causes all kinds of problems + # (see e.g. issue #24455) especially when dealing with running as a + # subprocess, where there's all kinds of extra stuff happening in + # there - insert a traceback.print_stack() to check it out. + # + # By this point, we've already called restart_subprocess() in + # ScriptBinding. However, we also need to unwind the stack back to + # that outer event loop. To accomplish this, we: + # - return immediately from the nested run() + # - abort_loop ensures the nested event loop will terminate + # - the debugger's interaction routine completes normally + # - the restart_subprocess() will have taken care of stopping + # the running program, which will also let the outer run complete + # + # That leaves us back at the outer main event loop, at which point our + # after event can fire, and we'll come back to this routine with a + # clean stack. + if self.nesting_level > 0: + self.abort_loop() + self.root.after(100, lambda: self.run(*args)) + return try: self.interacting = 1 return self.idb.run(*args) @@ -69,6 +105,10 @@ class Debugger: self.interacting = 0 def close(self, event=None): + try: + self.quit() + except Exception: + pass if self.interacting: self.top.bell() return @@ -192,7 +232,12 @@ class Debugger: b.configure(state="normal") # self.top.wakeup() - self.root.mainloop() + # Nested main loop: Tkinter's main loop is not reentrant, so use + # Tcl's vwait facility, which reenters the event loop until an + # event handler sets the variable we're waiting on + self.nesting_level += 1 + self.root.tk.call('vwait', '::idledebugwait') + self.nesting_level -= 1 # for b in self.buttons: b.configure(state="disabled") @@ -216,23 +261,26 @@ class Debugger: def cont(self): self.idb.set_continue() - self.root.quit() + self.abort_loop() def step(self): self.idb.set_step() - self.root.quit() + self.abort_loop() def next(self): self.idb.set_next(self.frame) - self.root.quit() + self.abort_loop() def ret(self): self.idb.set_return(self.frame) - self.root.quit() + self.abort_loop() def quit(self): self.idb.set_quit() - self.root.quit() + self.abort_loop() + + def abort_loop(self): + self.root.tk.call('set', '::idledebugwait', '1') stackviewer = None @@ -322,7 +370,7 @@ class Debugger: class StackViewer(ScrolledList): def __init__(self, master, flist, gui): - if macosxSupport.runningAsOSXApp(): + if macosxSupport.isAquaTk(): # At least on with the stock AquaTk version on OSX 10.4 you'll # get an shaking GUI that eventually kills IDLE if the width # argument is specified. diff --git a/Lib/idlelib/EditorWindow.py b/Lib/idlelib/EditorWindow.py index 4bf1111482..b5868be3fb 100644 --- a/Lib/idlelib/EditorWindow.py +++ b/Lib/idlelib/EditorWindow.py @@ -1,7 +1,8 @@ import importlib import importlib.abc +import importlib.util import os -from platform import python_version +import platform import re import string import sys @@ -12,7 +13,6 @@ import traceback import webbrowser from idlelib.MultiCall import MultiCallCreator -from idlelib import idlever from idlelib import WindowList from idlelib import SearchDialog from idlelib import GrepDialog @@ -21,10 +21,13 @@ from idlelib import PyParse from idlelib.configHandler import idleConf from idlelib import aboutDialog, textView, configDialog from idlelib import macosxSupport +from idlelib import help # The default tab setting for a Text widget, in average-width characters. TK_TABWIDTH_DEFAULT = 8 +_py_version = ' (%s)' % platform.python_version() + def _sphinx_version(): "Format sys.version_info to produce the Sphinx version string used to install the chm docs" major, minor, micro, level, serial = sys.version_info @@ -51,6 +54,11 @@ class HelpDialog(object): near - a Toplevel widget (e.g. EditorWindow or PyShell) to use as a reference for placing the help window """ + import warnings as w + w.warn("EditorWindow.HelpDialog is no longer used by Idle.\n" + "It will be removed in 3.6 or later.\n" + "It has been replaced by private help.HelpWindow\n", + DeprecationWarning, stacklevel=2) if self.dlg is None: self.show_dialog(parent) if near: @@ -77,7 +85,7 @@ class HelpDialog(object): self.dlg = None self.parent = None -helpDialog = HelpDialog() # singleton instance +helpDialog = HelpDialog() # singleton instance, no longer used class EditorWindow(object): @@ -108,8 +116,8 @@ class EditorWindow(object): 'Python%s.chm' % _sphinx_version()) if os.path.isfile(chmfile): dochome = chmfile - elif macosxSupport.runningAsOSXApp(): - # documentation is stored inside the python framework + elif sys.platform == 'darwin': + # documentation may be stored inside a python framework dochome = os.path.join(sys.base_prefix, 'Resources/English.lproj/Documentation/index.html') dochome = os.path.normpath(dochome) @@ -119,8 +127,7 @@ class EditorWindow(object): # Safari requires real file:-URLs EditorWindow.help_url = 'file://' + EditorWindow.help_url else: - EditorWindow.help_url = "http://docs.python.org/%d.%d" % sys.version_info[:2] - currentTheme=idleConf.CurrentTheme() + EditorWindow.help_url = "https://docs.python.org/%d.%d/" % sys.version_info[:2] self.flist = flist root = root or flist.root self.root = root @@ -149,6 +156,7 @@ class EditorWindow(object): 'name': 'text', 'padx': 5, 'wrap': 'none', + 'highlightthickness': 0, 'width': self.width, 'height': idleConf.GetOption('main', 'EditorWindow', 'height', type='int')} @@ -165,16 +173,16 @@ class EditorWindow(object): self.top.protocol("WM_DELETE_WINDOW", self.close) self.top.bind("<<close-window>>", self.close_event) - if macosxSupport.runningAsOSXApp(): + if macosxSupport.isAquaTk(): # Command-W on editorwindows doesn't work without this. text.bind('<<close-window>>', self.close_event) - # Some OS X systems have only one mouse button, - # so use control-click for pulldown menus there. - # (Note, AquaTk defines <2> as the right button if - # present and the Tk Text widget already binds <2>.) + # Some OS X systems have only one mouse button, so use + # control-click for popup context menus there. For two + # buttons, AquaTk defines <2> as the right button, not <3>. text.bind("<Control-Button-1>",self.right_menu_event) + text.bind("<2>", self.right_menu_event) else: - # Elsewhere, use right-click for pulldown menus. + # Elsewhere, use right-click for popup menus. text.bind("<3>",self.right_menu_event) text.bind("<<cut>>", self.cut) text.bind("<<copy>>", self.copy) @@ -219,18 +227,13 @@ class EditorWindow(object): text.bind("<<close-all-windows>>", self.flist.close_all_callback) text.bind("<<open-class-browser>>", self.open_class_browser) text.bind("<<open-path-browser>>", self.open_path_browser) + text.bind("<<open-turtle-demo>>", self.open_turtle_demo) self.set_status_bar() vbar['command'] = text.yview vbar.pack(side=RIGHT, fill=Y) text['yscrollcommand'] = vbar.set - fontWeight = 'normal' - if idleConf.GetOption('main', 'EditorWindow', 'font-bold', type='bool'): - fontWeight='bold' - text.config(font=(idleConf.GetOption('main', 'EditorWindow', 'font'), - idleConf.GetOption('main', 'EditorWindow', - 'font-size', type='int'), - fontWeight)) + text['font'] = idleConf.GetFont(self.root, 'main', 'EditorWindow') text_frame.pack(side=LEFT, fill=BOTH, expand=1) text.pack(side=TOP, fill=BOTH, expand=1) text.focus_set() @@ -313,50 +316,20 @@ class EditorWindow(object): self.askinteger = tkSimpleDialog.askinteger self.showerror = tkMessageBox.showerror - self._highlight_workaround() # Fix selection tags on Windows - - def _highlight_workaround(self): - # On Windows, Tk removes painting of the selection - # tags which is different behavior than on Linux and Mac. - # See issue14146 for more information. - if not sys.platform.startswith('win'): - return - - text = self.text - text.event_add("<<Highlight-FocusOut>>", "<FocusOut>") - text.event_add("<<Highlight-FocusIn>>", "<FocusIn>") - def highlight_fix(focus): - sel_range = text.tag_ranges("sel") - if sel_range: - if focus == 'out': - HILITE_CONFIG = idleConf.GetHighlight( - idleConf.CurrentTheme(), 'hilite') - text.tag_config("sel_fix", HILITE_CONFIG) - text.tag_raise("sel_fix") - text.tag_add("sel_fix", *sel_range) - elif focus == 'in': - text.tag_remove("sel_fix", "1.0", "end") - - text.bind("<<Highlight-FocusOut>>", - lambda ev: highlight_fix("out")) - text.bind("<<Highlight-FocusIn>>", - lambda ev: highlight_fix("in")) - - def _filename_to_unicode(self, filename): - """convert filename to unicode in order to display it in Tk""" - if isinstance(filename, str) or not filename: - return filename - else: + """Return filename as BMP unicode so diplayable in Tk.""" + # Decode bytes to unicode. + if isinstance(filename, bytes): try: - return filename.decode(self.filesystemencoding) + filename = filename.decode(self.filesystemencoding) except UnicodeDecodeError: - # XXX try: - return filename.decode(self.encoding) + filename = filename.decode(self.encoding) except UnicodeDecodeError: # byte-to-byte conversion - return filename.decode('iso8859-1') + filename = filename.decode('iso8859-1') + # Replace non-BMP char with diamond questionmark. + return re.sub('[\U00010000-\U0010FFFF]', '\ufffd', filename) def new_callback(self, event): dirname, basename = self.io.defaultfilename() @@ -408,13 +381,15 @@ class EditorWindow(object): def set_status_bar(self): self.status_bar = self.MultiStatusBar(self.top) - if macosxSupport.runningAsOSXApp(): + sep = Frame(self.top, height=1, borderwidth=1, background='grey75') + if sys.platform == "darwin": # Insert some padding to avoid obscuring some of the statusbar # by the resize widget. self.status_bar.set_label('_padding1', ' ', side=RIGHT) self.status_bar.set_label('column', 'Col: ?', side=RIGHT) self.status_bar.set_label('line', 'Ln: ?', side=RIGHT) self.status_bar.pack(side=BOTTOM, fill=X) + sep.pack(side=BOTTOM, fill=X) self.text.bind("<<set-line-and-column>>", self.set_line_and_column) self.text.event_add("<<set-line-and-column>>", "<KeyRelease>", "<ButtonRelease>") @@ -431,27 +406,25 @@ class EditorWindow(object): ("format", "F_ormat"), ("run", "_Run"), ("options", "_Options"), - ("windows", "_Windows"), + ("windows", "_Window"), ("help", "_Help"), ] - if macosxSupport.runningAsOSXApp(): - menu_specs[-2] = ("windows", "_Window") - def createmenubar(self): mbar = self.menubar self.menudict = menudict = {} for name, label in self.menu_specs: underline, label = prepstr(label) - menudict[name] = menu = Menu(mbar, name=name) + menudict[name] = menu = Menu(mbar, name=name, tearoff=0) mbar.add_cascade(label=label, menu=menu, underline=underline) - if macosxSupport.isCarbonAquaTk(self.root): + if macosxSupport.isCarbonTk(): # Insert the application menu - menudict['application'] = menu = Menu(mbar, name='apple') + menudict['application'] = menu = Menu(mbar, name='apple', + tearoff=0) mbar.add_cascade(label='IDLE', menu=menu) self.fill_menus() - self.recent_files_menu = Menu(self.menubar) + self.recent_files_menu = Menu(self.menubar, tearoff=0) self.menudict['file'].insert_cascade(3, label='Recent Files', underline=0, menu=self.recent_files_menu) @@ -533,23 +506,29 @@ class EditorWindow(object): return 'normal' def about_dialog(self, event=None): + "Handle Help 'About IDLE' event." + # Synchronize with macosxSupport.overrideRootMenu.about_dialog. aboutDialog.AboutDialog(self.top,'About IDLE') def config_dialog(self, event=None): + "Handle Options 'Configure IDLE' event." + # Synchronize with macosxSupport.overrideRootMenu.config_dialog. configDialog.ConfigDialog(self.top,'Settings') def help_dialog(self, event=None): + "Handle Help 'IDLE Help' event." + # Synchronize with macosxSupport.overrideRootMenu.help_dialog. if self.root: parent = self.root else: parent = self.top - helpDialog.display(parent, near=self.top) + help.show_idlehelp(parent) def python_docs(self, event=None): if sys.platform[:3] == 'win': try: os.startfile(self.help_url) - except WindowsError as why: + except OSError as why: tkMessageBox.showerror(title='Document Start Failure', message=str(why), parent=self.text) else: @@ -660,20 +639,20 @@ class EditorWindow(object): return # XXX Ought to insert current file's directory in front of path try: - loader = importlib.find_loader(name) + spec = importlib.util.find_spec(name) except (ValueError, ImportError) as msg: tkMessageBox.showerror("Import error", str(msg), parent=self.text) return - if loader is None: + if spec is None: tkMessageBox.showerror("Import error", "module not found", parent=self.text) return - if not isinstance(loader, importlib.abc.SourceLoader): + if not isinstance(spec.loader, importlib.abc.SourceLoader): tkMessageBox.showerror("Import error", "not a source-based module", parent=self.text) return try: - file_path = loader.get_filename(name) + file_path = spec.loader.get_filename(name) except AttributeError: tkMessageBox.showerror("Import error", "loader does not support get_filename", @@ -683,16 +662,15 @@ class EditorWindow(object): self.flist.open(file_path) else: self.io.loadfile(file_path) + return file_path def open_class_browser(self, event=None): filename = self.io.filename - if not filename: - tkMessageBox.showerror( - "No filename", - "This buffer has no associated filename", - master=self.text) - self.text.focus_set() - return None + if not (self.__class__.__name__ == 'PyShellEditorWindow' + and filename): + filename = self.open_module() + if filename is None: + return head, tail = os.path.split(filename) base, ext = os.path.splitext(tail) from idlelib import ClassBrowser @@ -702,6 +680,14 @@ class EditorWindow(object): from idlelib import PathBrowser PathBrowser.PathBrowser(self.flist) + def open_turtle_demo(self, event = None): + import subprocess + + cmd = [sys.executable, + '-c', + 'from turtledemo.__main__ import main; main()'] + subprocess.Popen(cmd, shell=False) + def gotoline(self, lineno): if lineno is not None and lineno > 0: self.text.mark_set("insert", "%d.0" % lineno) @@ -752,11 +738,11 @@ class EditorWindow(object): self.color = None def ResetColorizer(self): - "Update the colour theme" + "Update the color theme" # Called from self.filename_change_hook and from configDialog.py self._rmcolorizer() self._addcolorizer() - theme = idleConf.GetOption('main','Theme','name') + theme = idleConf.CurrentTheme() normal_colors = idleConf.GetHighlight(theme, 'normal') cursor_color = idleConf.GetHighlight(theme, 'cursor', fgBg='fg') select_colors = idleConf.GetHighlight(theme, 'hilite') @@ -767,6 +753,9 @@ class EditorWindow(object): selectforeground=select_colors['foreground'], selectbackground=select_colors['background'], ) + if TkVersion >= 8.5: + self.text.config( + inactiveselectbackground=select_colors['background']) IDENTCHARS = string.ascii_letters + string.digits + "_" @@ -784,13 +773,8 @@ class EditorWindow(object): def ResetFont(self): "Update the text widgets' font if it is changed" # Called from configDialog.py - fontWeight='normal' - if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'): - fontWeight='bold' - self.text.config(font=(idleConf.GetOption('main','EditorWindow','font'), - idleConf.GetOption('main','EditorWindow','font-size', - type='int'), - fontWeight)) + + self.text['font'] = idleConf.GetFont(self.root, 'main','EditorWindow') def RemoveKeybindings(self): "Remove the keybindings before they are changed." @@ -872,7 +856,7 @@ class EditorWindow(object): if sys.platform[:3] == 'win': try: os.startfile(helpfile) - except WindowsError as why: + except OSError as why: tkMessageBox.showerror(title='Document Start Failure', message=str(why), parent=self.text) else: @@ -906,9 +890,11 @@ class EditorWindow(object): except OSError as err: if not getattr(self.root, "recentfilelist_error_displayed", False): self.root.recentfilelist_error_displayed = True - tkMessageBox.showerror(title='IDLE Error', - message='Unable to update Recent Files list:\n%s' - % str(err), + tkMessageBox.showwarning(title='IDLE Warning', + message="Cannot update File menu Recent Files list. " + "Your operating system says:\n%s\n" + "Select OK and IDLE will continue without updating." + % self._filename_to_unicode(str(err)), parent=self.text) # for each edit window instance, construct the recent files menu for instance in self.top.instance_dict: @@ -932,7 +918,7 @@ class EditorWindow(object): short = self.short_title() long = self.long_title() if short and long: - title = short + " - " + long + title = short + " - " + long + _py_version elif short: title = short elif long: @@ -956,14 +942,13 @@ class EditorWindow(object): self.undo.reset_undo() def short_title(self): - pyversion = "Python " + python_version() + ": " filename = self.io.filename if filename: filename = os.path.basename(filename) else: filename = "Untitled" # return unicode string to display non-ASCII chars correctly - return pyversion + self._filename_to_unicode(filename) + return self._filename_to_unicode(filename) def long_title(self): # return unicode string to display non-ASCII chars correctly @@ -1063,7 +1048,7 @@ class EditorWindow(object): try: try: mod = importlib.import_module('.' + name, package=__package__) - except ImportError: + except (ImportError, TypeError): mod = importlib.import_module(name) except ImportError: print("\nFailed to import extension: ", name) @@ -1397,7 +1382,7 @@ class EditorWindow(object): text.see("insert") text.undo_block_stop() - # Our editwin provides a is_char_in_string function that works + # Our editwin provides an is_char_in_string function that works # with a Tk text index, but PyParse only knows about offsets into # a string. This builds a function for PyParse that accepts an # offset. @@ -1672,7 +1657,7 @@ def get_accelerator(keydefs, eventname): keylist = keydefs.get(eventname) # issue10940: temporary workaround to prevent hang with OS X Cocoa Tk 8.5 # if not keylist: - if (not keylist) or (macosxSupport.runningAsOSXApp() and eventname in { + if (not keylist) or (macosxSupport.isCocoaTk() and eventname in { "<<open-module>>", "<<goto-line>>", "<<change-indentwidth>>"}): @@ -1699,19 +1684,20 @@ def fixwordbreaks(root): tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]') -def test(): - root = Tk() +def _editor_window(parent): # htest # + # error if close master window first - timer event, after script + root = parent fixwordbreaks(root) - root.withdraw() if sys.argv[1:]: filename = sys.argv[1] else: filename = None + macosxSupport.setupApp(root, None) edit = EditorWindow(root=root, filename=filename) - edit.set_close_hook(root.quit) edit.text.bind("<<close-all-windows>>", edit.close_event) - root.mainloop() - root.destroy() + # Does not stop error, neither does following + # edit.text.bind("<<close-window>>", edit.close_event) if __name__ == '__main__': - test() + from idlelib.idle_test.htest import run + run(_editor_window) diff --git a/Lib/idlelib/FileList.py b/Lib/idlelib/FileList.py index 37a337ed9a..a9989a8624 100644 --- a/Lib/idlelib/FileList.py +++ b/Lib/idlelib/FileList.py @@ -103,7 +103,7 @@ class FileList: if not os.path.isabs(filename): try: pwd = os.getcwd() - except os.error: + except OSError: pass else: filename = os.path.join(pwd, filename) diff --git a/Lib/idlelib/FormatParagraph.py b/Lib/idlelib/FormatParagraph.py index ae4e6e7b91..7a9d185042 100644 --- a/Lib/idlelib/FormatParagraph.py +++ b/Lib/idlelib/FormatParagraph.py @@ -32,7 +32,7 @@ class FormatParagraph: def close(self): self.editwin = None - def format_paragraph_event(self, event): + def format_paragraph_event(self, event, limit=None): """Formats paragraph to a max width specified in idleConf. If text is selected, format_paragraph_event will start breaking lines @@ -41,9 +41,14 @@ class FormatParagraph: If no text is selected, format_paragraph_event uses the current cursor location to determine the paragraph (lines of text surrounded by blank lines) and formats it. + + The length limit parameter is for testing with a known value. """ - maxformatwidth = idleConf.GetOption( - 'main', 'FormatParagraph', 'paragraph', type='int') + if limit is None: + # The default length limit is that defined by pep8 + limit = idleConf.GetOption( + 'extensions', 'FormatParagraph', 'max-width', + type='int', default=72) text = self.editwin.text first, last = self.editwin.get_selection_indices() if first and last: @@ -53,9 +58,9 @@ class FormatParagraph: first, last, comment_header, data = \ find_paragraph(text, text.index("insert")) if comment_header: - newdata = reformat_comment(data, maxformatwidth, comment_header) + newdata = reformat_comment(data, limit, comment_header) else: - newdata = reformat_paragraph(data, maxformatwidth) + newdata = reformat_paragraph(data, limit) text.tag_remove("sel", "1.0", "end") if newdata != data: @@ -185,7 +190,6 @@ def get_comment_header(line): return m.group(1) if __name__ == "__main__": - from test import support; support.use_resources = ['gui'] import unittest unittest.main('idlelib.idle_test.test_formatparagraph', verbosity=2, exit=False) diff --git a/Lib/idlelib/GrepDialog.py b/Lib/idlelib/GrepDialog.py index c3590742eb..721b231a9e 100644 --- a/Lib/idlelib/GrepDialog.py +++ b/Lib/idlelib/GrepDialog.py @@ -1,9 +1,13 @@ import os import fnmatch +import re # for htest import sys -from tkinter import * +from tkinter import StringVar, BooleanVar, Checkbutton # for GrepDialog +from tkinter import Tk, Text, Button, SEL, END # for htest from idlelib import SearchEngine from idlelib.SearchDialogBase import SearchDialogBase +# Importing OutputWindow fails due to import loop +# EditorWindow -> GrepDialop -> OutputWindow -> EditorWindow def grep(text, io=None, flist=None): root = text._root() @@ -40,10 +44,10 @@ class GrepDialog(SearchDialogBase): def create_entries(self): SearchDialogBase.create_entries(self) - self.globent = self.make_entry("In files:", self.globvar) + self.globent = self.make_entry("In files:", self.globvar)[0] def create_other_buttons(self): - f = self.make_frame() + f = self.make_frame()[0] btn = Checkbutton(f, anchor="w", variable=self.recvar, @@ -63,7 +67,7 @@ class GrepDialog(SearchDialogBase): if not path: self.top.bell() return - from idlelib.OutputWindow import OutputWindow + from idlelib.OutputWindow import OutputWindow # leave here! save = sys.stdout try: sys.stdout = OutputWindow(self.flist) @@ -79,21 +83,26 @@ class GrepDialog(SearchDialogBase): pat = self.engine.getpat() print("Searching %r in %s ..." % (pat, path)) hits = 0 - for fn in list: - try: - with open(fn, errors='replace') as f: - for lineno, line in enumerate(f, 1): - if line[-1:] == '\n': - line = line[:-1] - if prog.search(line): - sys.stdout.write("%s: %s: %s\n" % - (fn, lineno, line)) - hits += 1 - except OSError as msg: - print(msg) - print(("Hits found: %s\n" - "(Hint: right-click to open locations.)" - % hits) if hits else "No hits.") + try: + for fn in list: + try: + with open(fn, errors='replace') as f: + for lineno, line in enumerate(f, 1): + if line[-1:] == '\n': + line = line[:-1] + if prog.search(line): + sys.stdout.write("%s: %s: %s\n" % + (fn, lineno, line)) + hits += 1 + except OSError as msg: + print(msg) + print(("Hits found: %s\n" + "(Hint: right-click to open locations.)" + % hits) if hits else "No hits.") + except AttributeError: + # Tk window has been closed, OutputWindow.text = None, + # so in OW.write, OW.text.insert fails. + pass def findfiles(self, dir, base, rec): try: @@ -120,9 +129,30 @@ class GrepDialog(SearchDialogBase): self.top.grab_release() self.top.withdraw() + +def _grep_dialog(parent): # htest # + from idlelib.PyShell import PyShellFileList + root = Tk() + root.title("Test GrepDialog") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) + + flist = PyShellFileList(root) + text = Text(root, height=5) + text.pack() + + def show_grep_dialog(): + text.tag_add(SEL, "1.0", END) + grep(text, flist=flist) + text.tag_remove(SEL, "1.0", END) + + button = Button(root, text="Show GrepDialog", command=show_grep_dialog) + button.pack() + root.mainloop() + if __name__ == "__main__": - # A human test is a bit tricky since EditorWindow() imports this module. - # Hence Idle must be restarted after editing this file for a live test. import unittest unittest.main('idlelib.idle_test.test_grep', verbosity=2, exit=False) + from idlelib.idle_test.htest import run + run(_grep_dialog) diff --git a/Lib/idlelib/HyperParser.py b/Lib/idlelib/HyperParser.py index 4af4b085c7..77cb057ce2 100644 --- a/Lib/idlelib/HyperParser.py +++ b/Lib/idlelib/HyperParser.py @@ -1,23 +1,31 @@ -""" -HyperParser -=========== -This module defines the HyperParser class, which provides advanced parsing -abilities for the ParenMatch and other extensions. -The HyperParser uses PyParser. PyParser is intended mostly to give information -on the proper indentation of code. HyperParser gives some information on the -structure of code, used by extensions to help the user. +"""Provide advanced parsing abilities for ParenMatch and other extensions. + +HyperParser uses PyParser. PyParser mostly gives information on the +proper indentation of code. HyperParser gives additional information on +the structure of code. """ import string -import keyword +from keyword import iskeyword from idlelib import PyParse -class HyperParser: +# all ASCII chars that may be in an identifier +_ASCII_ID_CHARS = frozenset(string.ascii_letters + string.digits + "_") +# all ASCII chars that may be the first char of an identifier +_ASCII_ID_FIRST_CHARS = frozenset(string.ascii_letters + "_") + +# lookup table for whether 7-bit ASCII chars are valid in a Python identifier +_IS_ASCII_ID_CHAR = [(chr(x) in _ASCII_ID_CHARS) for x in range(128)] +# lookup table for whether 7-bit ASCII chars are valid as the first +# char in a Python identifier +_IS_ASCII_ID_FIRST_CHAR = \ + [(chr(x) in _ASCII_ID_FIRST_CHARS) for x in range(128)] + + +class HyperParser: def __init__(self, editwin, index): - """Initialize the HyperParser to analyze the surroundings of the given - index. - """ + "To initialize, analyze the surroundings of the given index." self.editwin = editwin self.text = text = editwin.text @@ -33,9 +41,10 @@ class HyperParser: startat = max(lno - context, 1) startatindex = repr(startat) + ".0" stopatindex = "%d.end" % lno - # We add the newline because PyParse requires a newline at end. - # We add a space so that index won't be at end of line, so that - # its status will be the same as the char before it, if should. + # We add the newline because PyParse requires a newline + # at end. We add a space so that index won't be at end + # of line, so that its status will be the same as the + # char before it, if should. parser.set_str(text.get(startatindex, stopatindex)+' \n') bod = parser.find_good_parse_start( editwin._build_char_in_string_func(startatindex)) @@ -49,122 +58,175 @@ class HyperParser: else: startatindex = "1.0" stopatindex = "%d.end" % lno - # We add the newline because PyParse requires a newline at end. - # We add a space so that index won't be at end of line, so that - # its status will be the same as the char before it, if should. + # We add the newline because PyParse requires it. We add a + # space so that index won't be at end of line, so that its + # status will be the same as the char before it, if should. parser.set_str(text.get(startatindex, stopatindex)+' \n') parser.set_lo(0) - # We want what the parser has, except for the last newline and space. + # We want what the parser has, minus the last newline and space. self.rawtext = parser.str[:-2] - # As far as I can see, parser.str preserves the statement we are in, - # so that stopatindex can be used to synchronize the string with the - # text box indices. + # Parser.str apparently preserves the statement we are in, so + # that stopatindex can be used to synchronize the string with + # the text box indices. self.stopatindex = stopatindex self.bracketing = parser.get_last_stmt_bracketing() - # find which pairs of bracketing are openers. These always correspond - # to a character of rawtext. - self.isopener = [i>0 and self.bracketing[i][1] > self.bracketing[i-1][1] + # find which pairs of bracketing are openers. These always + # correspond to a character of rawtext. + self.isopener = [i>0 and self.bracketing[i][1] > + self.bracketing[i-1][1] for i in range(len(self.bracketing))] self.set_index(index) def set_index(self, index): - """Set the index to which the functions relate. Note that it must be - in the same statement. + """Set the index to which the functions relate. + + The index must be in the same statement. """ - indexinrawtext = \ - len(self.rawtext) - len(self.text.get(index, self.stopatindex)) + indexinrawtext = (len(self.rawtext) - + len(self.text.get(index, self.stopatindex))) if indexinrawtext < 0: - raise ValueError("The index given is before the analyzed statement") + raise ValueError("Index %s precedes the analyzed statement" + % index) self.indexinrawtext = indexinrawtext # find the rightmost bracket to which index belongs self.indexbracket = 0 - while self.indexbracket < len(self.bracketing)-1 and \ - self.bracketing[self.indexbracket+1][0] < self.indexinrawtext: + while (self.indexbracket < len(self.bracketing)-1 and + self.bracketing[self.indexbracket+1][0] < self.indexinrawtext): self.indexbracket += 1 - if self.indexbracket < len(self.bracketing)-1 and \ - self.bracketing[self.indexbracket+1][0] == self.indexinrawtext and \ - not self.isopener[self.indexbracket+1]: + if (self.indexbracket < len(self.bracketing)-1 and + self.bracketing[self.indexbracket+1][0] == self.indexinrawtext and + not self.isopener[self.indexbracket+1]): self.indexbracket += 1 def is_in_string(self): - """Is the index given to the HyperParser is in a string?""" + """Is the index given to the HyperParser in a string?""" # The bracket to which we belong should be an opener. # If it's an opener, it has to have a character. - return self.isopener[self.indexbracket] and \ - self.rawtext[self.bracketing[self.indexbracket][0]] in ('"', "'") + return (self.isopener[self.indexbracket] and + self.rawtext[self.bracketing[self.indexbracket][0]] + in ('"', "'")) def is_in_code(self): - """Is the index given to the HyperParser is in a normal code?""" - return not self.isopener[self.indexbracket] or \ - self.rawtext[self.bracketing[self.indexbracket][0]] not in \ - ('#', '"', "'") + """Is the index given to the HyperParser in normal code?""" + return (not self.isopener[self.indexbracket] or + self.rawtext[self.bracketing[self.indexbracket][0]] + not in ('#', '"', "'")) def get_surrounding_brackets(self, openers='([{', mustclose=False): - """If the index given to the HyperParser is surrounded by a bracket - defined in openers (or at least has one before it), return the - indices of the opening bracket and the closing bracket (or the - end of line, whichever comes first). - If it is not surrounded by brackets, or the end of line comes before - the closing bracket and mustclose is True, returns None. + """Return bracket indexes or None. + + If the index given to the HyperParser is surrounded by a + bracket defined in openers (or at least has one before it), + return the indices of the opening bracket and the closing + bracket (or the end of line, whichever comes first). + + If it is not surrounded by brackets, or the end of line comes + before the closing bracket and mustclose is True, returns None. """ + bracketinglevel = self.bracketing[self.indexbracket][1] before = self.indexbracket - while not self.isopener[before] or \ - self.rawtext[self.bracketing[before][0]] not in openers or \ - self.bracketing[before][1] > bracketinglevel: + while (not self.isopener[before] or + self.rawtext[self.bracketing[before][0]] not in openers or + self.bracketing[before][1] > bracketinglevel): before -= 1 if before < 0: return None bracketinglevel = min(bracketinglevel, self.bracketing[before][1]) after = self.indexbracket + 1 - while after < len(self.bracketing) and \ - self.bracketing[after][1] >= bracketinglevel: + while (after < len(self.bracketing) and + self.bracketing[after][1] >= bracketinglevel): after += 1 beforeindex = self.text.index("%s-%dc" % (self.stopatindex, len(self.rawtext)-self.bracketing[before][0])) - if after >= len(self.bracketing) or \ - self.bracketing[after][0] > len(self.rawtext): + if (after >= len(self.bracketing) or + self.bracketing[after][0] > len(self.rawtext)): if mustclose: return None afterindex = self.stopatindex else: - # We are after a real char, so it is a ')' and we give the index - # before it. - afterindex = self.text.index("%s-%dc" % - (self.stopatindex, + # We are after a real char, so it is a ')' and we give the + # index before it. + afterindex = self.text.index( + "%s-%dc" % (self.stopatindex, len(self.rawtext)-(self.bracketing[after][0]-1))) return beforeindex, afterindex - # This string includes all chars that may be in a white space - _whitespace_chars = " \t\n\\" - # This string includes all chars that may be in an identifier - _id_chars = string.ascii_letters + string.digits + "_" - # This string includes all chars that may be the first char of an identifier - _id_first_chars = string.ascii_letters + "_" - - # Given a string and pos, return the number of chars in the identifier - # which ends at pos, or 0 if there is no such one. Saved words are not - # identifiers. - def _eat_identifier(self, str, limit, pos): + # the set of built-in identifiers which are also keywords, + # i.e. keyword.iskeyword() returns True for them + _ID_KEYWORDS = frozenset({"True", "False", "None"}) + + @classmethod + def _eat_identifier(cls, str, limit, pos): + """Given a string and pos, return the number of chars in the + identifier which ends at pos, or 0 if there is no such one. + + This ignores non-identifier eywords are not identifiers. + """ + is_ascii_id_char = _IS_ASCII_ID_CHAR + + # Start at the end (pos) and work backwards. i = pos - while i > limit and str[i-1] in self._id_chars: + + # Go backwards as long as the characters are valid ASCII + # identifier characters. This is an optimization, since it + # is faster in the common case where most of the characters + # are ASCII. + while i > limit and ( + ord(str[i - 1]) < 128 and + is_ascii_id_char[ord(str[i - 1])] + ): i -= 1 - if i < pos and (str[i] not in self._id_first_chars or \ - keyword.iskeyword(str[i:pos])): - i = pos + + # If the above loop ended due to reaching a non-ASCII + # character, continue going backwards using the most generic + # test for whether a string contains only valid identifier + # characters. + if i > limit and ord(str[i - 1]) >= 128: + while i - 4 >= limit and ('a' + str[i - 4:pos]).isidentifier(): + i -= 4 + if i - 2 >= limit and ('a' + str[i - 2:pos]).isidentifier(): + i -= 2 + if i - 1 >= limit and ('a' + str[i - 1:pos]).isidentifier(): + i -= 1 + + # The identifier candidate starts here. If it isn't a valid + # identifier, don't eat anything. At this point that is only + # possible if the first character isn't a valid first + # character for an identifier. + if not str[i:pos].isidentifier(): + return 0 + elif i < pos: + # All characters in str[i:pos] are valid ASCII identifier + # characters, so it is enough to check that the first is + # valid as the first character of an identifier. + if not _IS_ASCII_ID_FIRST_CHAR[ord(str[i])]: + return 0 + + # All keywords are valid identifiers, but should not be + # considered identifiers here, except for True, False and None. + if i < pos and ( + iskeyword(str[i:pos]) and + str[i:pos] not in cls._ID_KEYWORDS + ): + return 0 + return pos - i + # This string includes all chars that may be in a white space + _whitespace_chars = " \t\n\\" + def get_expression(self): - """Return a string with the Python expression which ends at the given - index, which is empty if there is no real one. + """Return a string with the Python expression which ends at the + given index, which is empty if there is no real one. """ if not self.is_in_code(): - raise ValueError("get_expression should only be called if index "\ - "is inside a code.") + raise ValueError("get_expression should only be called" + "if index is inside a code.") rawtext = self.rawtext bracketing = self.bracketing @@ -177,20 +239,20 @@ class HyperParser: postdot_phase = True while 1: - # Eat whitespaces, comments, and if postdot_phase is False - one dot + # Eat whitespaces, comments, and if postdot_phase is False - a dot while 1: if pos>brck_limit and rawtext[pos-1] in self._whitespace_chars: # Eat a whitespace pos -= 1 - elif not postdot_phase and \ - pos > brck_limit and rawtext[pos-1] == '.': + elif (not postdot_phase and + pos > brck_limit and rawtext[pos-1] == '.'): # Eat a dot pos -= 1 postdot_phase = True - # The next line will fail if we are *inside* a comment, but we - # shouldn't be. - elif pos == brck_limit and brck_index > 0 and \ - rawtext[bracketing[brck_index-1][0]] == '#': + # The next line will fail if we are *inside* a comment, + # but we shouldn't be. + elif (pos == brck_limit and brck_index > 0 and + rawtext[bracketing[brck_index-1][0]] == '#'): # Eat a comment brck_index -= 2 brck_limit = bracketing[brck_index][0] @@ -200,8 +262,8 @@ class HyperParser: break if not postdot_phase: - # We didn't find a dot, so the expression end at the last - # identifier pos. + # We didn't find a dot, so the expression end at the + # last identifier pos. break ret = self._eat_identifier(rawtext, brck_limit, pos) @@ -209,13 +271,13 @@ class HyperParser: # There is an identifier to eat pos = pos - ret last_identifier_pos = pos - # Now, in order to continue the search, we must find a dot. + # Now, to continue the search, we must find a dot. postdot_phase = False # (the loop continues now) elif pos == brck_limit: - # We are at a bracketing limit. If it is a closing bracket, - # eat the bracket, otherwise, stop the search. + # We are at a bracketing limit. If it is a closing + # bracket, eat the bracket, otherwise, stop the search. level = bracketing[brck_index][1] while brck_index > 0 and bracketing[brck_index-1][1] > level: brck_index -= 1 @@ -244,3 +306,8 @@ class HyperParser: break return rawtext[last_identifier_pos:self.indexinrawtext] + + +if __name__ == '__main__': + import unittest + unittest.main('idlelib.idle_test.test_hyperparser', verbosity=2) diff --git a/Lib/idlelib/IOBinding.py b/Lib/idlelib/IOBinding.py index f008b46799..5ec9d546fd 100644 --- a/Lib/idlelib/IOBinding.py +++ b/Lib/idlelib/IOBinding.py @@ -1,18 +1,16 @@ +import codecs +from codecs import BOM_UTF8 import os -import types +import re import shlex import sys -import codecs import tempfile + import tkinter.filedialog as tkFileDialog import tkinter.messagebox as tkMessageBox -import re -from tkinter import * from tkinter.simpledialog import askstring -from idlelib.configHandler import idleConf -from codecs import BOM_UTF8 # Try setting the locale, so that we can find out # what encoding to use @@ -218,7 +216,7 @@ class IOBinding: f.seek(0) bytes = f.read() except OSError as msg: - tkMessageBox.showerror("I/O Error", str(msg), master=self.text) + tkMessageBox.showerror("I/O Error", str(msg), parent=self.text) return False chars, converted = self._decode(two_lines, bytes) if chars is None: @@ -267,7 +265,7 @@ class IOBinding: title="Error loading the file", message="The encoding '%s' is not known to this Python "\ "installation. The file may not display correctly" % name, - master = self.text) + parent = self.text) enc = None except UnicodeDecodeError: return None, False @@ -322,7 +320,7 @@ class IOBinding: title="Save On Close", message=message, default=tkMessageBox.YES, - master=self.text) + parent=self.text) if confirm: reply = "yes" self.save(None) @@ -382,7 +380,7 @@ class IOBinding: return True except OSError as msg: tkMessageBox.showerror("I/O Error", str(msg), - master=self.text) + parent=self.text) return False def encode(self, chars): @@ -419,7 +417,7 @@ class IOBinding: tkMessageBox.showerror( "I/O Error", "%s.\nSaving as UTF-8" % failed, - master = self.text) + parent = self.text) # Fallback: save as UTF-8, with BOM - ignoring the incorrect # declared encoding return BOM_UTF8 + chars.encode("utf-8") @@ -434,7 +432,7 @@ class IOBinding: title="Print", message="Print to Default Printer", default=tkMessageBox.OK, - master=self.text) + parent=self.text) if not confirm: self.text.focus_set() return "break" @@ -471,10 +469,10 @@ class IOBinding: status + output if output: output = "Printing command: %s\n" % repr(command) + output - tkMessageBox.showerror("Print status", output, master=self.text) + tkMessageBox.showerror("Print status", output, parent=self.text) else: #no printing for this platform message = "Printing is not enabled for this platform: %s" % platform - tkMessageBox.showinfo("Print status", message, master=self.text) + tkMessageBox.showinfo("Print status", message, parent=self.text) if tempfilename: os.unlink(tempfilename) return "break" @@ -493,7 +491,7 @@ class IOBinding: def askopenfile(self): dir, base = self.defaultfilename("open") if not self.opendialog: - self.opendialog = tkFileDialog.Open(master=self.text, + self.opendialog = tkFileDialog.Open(parent=self.text, filetypes=self.filetypes) filename = self.opendialog.show(initialdir=dir, initialfile=base) return filename @@ -506,7 +504,7 @@ class IOBinding: else: try: pwd = os.getcwd() - except os.error: + except OSError: pwd = "" return pwd, "" @@ -514,7 +512,7 @@ class IOBinding: dir, base = self.defaultfilename("save") if not self.savedialog: self.savedialog = tkFileDialog.SaveAs( - master=self.text, + parent=self.text, filetypes=self.filetypes, defaultextension=self.defaultextension) filename = self.savedialog.show(initialdir=dir, initialfile=base) @@ -525,16 +523,20 @@ class IOBinding: if self.editwin.flist: self.editwin.update_recent_files_list(filename) -def test(): - root = Tk() +def _io_binding(parent): # htest # + from tkinter import Toplevel, Text + from idlelib.configHandler import idleConf + + root = Toplevel(parent) + root.title("Test IOBinding") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) class MyEditWin: def __init__(self, text): self.text = text self.flist = None self.text.bind("<Control-o>", self.open) self.text.bind("<Control-s>", self.save) - self.text.bind("<Alt-s>", self.save_as) - self.text.bind("<Alt-z>", self.save_a_copy) def get_saved(self): return 0 def set_saved(self, flag): pass def reset_undo(self): pass @@ -542,16 +544,13 @@ def test(): self.text.event_generate("<<open-window-from-file>>") def save(self, event): self.text.event_generate("<<save-window>>") - def save_as(self, event): - self.text.event_generate("<<save-window-as-file>>") - def save_a_copy(self, event): - self.text.event_generate("<<save-copy-of-window-as-file>>") + text = Text(root) text.pack() text.focus_set() editwin = MyEditWin(text) - io = IOBinding(editwin) - root.mainloop() + IOBinding(editwin) if __name__ == "__main__": - test() + from idlelib.idle_test.htest import run + run(_io_binding) diff --git a/Lib/idlelib/IdleHistory.py b/Lib/idlelib/IdleHistory.py index d6cb16272b..078af29053 100644 --- a/Lib/idlelib/IdleHistory.py +++ b/Lib/idlelib/IdleHistory.py @@ -100,7 +100,5 @@ class History: self.prefix = None if __name__ == "__main__": - from test import support - support.use_resources = ['gui'] from unittest import main main('idlelib.idle_test.test_idlehistory', verbosity=2, exit=False) diff --git a/Lib/idlelib/MultiCall.py b/Lib/idlelib/MultiCall.py index 64729eab8c..251a84d083 100644 --- a/Lib/idlelib/MultiCall.py +++ b/Lib/idlelib/MultiCall.py @@ -32,7 +32,6 @@ Each function will be called at most once for each event. import sys import re import tkinter -from idlelib import macosxSupport # the event type constants, which define the meaning of mc_type MC_KEYPRESS=0; MC_KEYRELEASE=1; MC_BUTTONPRESS=2; MC_BUTTONRELEASE=3; @@ -45,7 +44,7 @@ MC_SHIFT = 1<<0; MC_CONTROL = 1<<2; MC_ALT = 1<<3; MC_META = 1<<5 MC_OPTION = 1<<6; MC_COMMAND = 1<<7 # define the list of modifiers, to be used in complex event types. -if macosxSupport.runningAsOSXApp(): +if sys.platform == "darwin": _modifiers = (("Shift",), ("Control",), ("Option",), ("Command",)) _modifier_masks = (MC_SHIFT, MC_CONTROL, MC_OPTION, MC_COMMAND) else: @@ -57,6 +56,12 @@ _modifier_names = dict([(name, number) for number in range(len(_modifiers)) for name in _modifiers[number]]) +# In 3.4, if no shell window is ever open, the underlying Tk widget is +# destroyed before .__del__ methods here are called. The following +# is used to selectively ignore shutdown exceptions to avoid +# 'Exception ignored' messages. See http://bugs.python.org/issue20167 +APPLICATION_GONE = "application has been destroyed" + # A binder is a class which binds functions to one type of event. It has two # methods: bind and unbind, which get a function and a parsed sequence, as # returned by _parse_sequence(). There are two types of binders: @@ -98,7 +103,12 @@ class _SimpleBinder: def __del__(self): if self.handlerid: - self.widget.unbind(self.widgetinst, self.sequence, self.handlerid) + try: + self.widget.unbind(self.widgetinst, self.sequence, + self.handlerid) + except tkinter.TclError as e: + if not APPLICATION_GONE in e.args[0]: + raise # An int in range(1 << len(_modifiers)) represents a combination of modifiers # (if the least significent bit is on, _modifiers[0] is on, and so on). @@ -227,7 +237,11 @@ class _ComplexBinder: def __del__(self): for seq, id in self.handlerids: - self.widget.unbind(self.widgetinst, seq, id) + try: + self.widget.unbind(self.widgetinst, seq, id) + except tkinter.TclError as e: + if not APPLICATION_GONE in e.args[0]: + raise # define the list of event types to be handled by MultiEvent. the order is # compatible with the definition of event type constants. @@ -390,15 +404,21 @@ def MultiCallCreator(widget): func, triplets = self.__eventinfo[virtual] if func: for triplet in triplets: - self.__binders[triplet[1]].unbind(triplet, func) - + try: + self.__binders[triplet[1]].unbind(triplet, func) + except tkinter.TclError as e: + if not APPLICATION_GONE in e.args[0]: + raise _multicall_dict[widget] = MultiCall return MultiCall -if __name__ == "__main__": - # Test + +def _multi_call(parent): root = tkinter.Tk() + root.title("Test MultiCall") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) text = MultiCallCreator(tkinter.Text)(root) text.pack() def bindseq(seq, n=[0]): @@ -414,8 +434,13 @@ if __name__ == "__main__": bindseq("<Alt-Control-Key-a>") bindseq("<Key-b>") bindseq("<Control-Button-1>") + bindseq("<Button-2>") bindseq("<Alt-Button-1>") bindseq("<FocusOut>") bindseq("<Enter>") bindseq("<Leave>") root.mainloop() + +if __name__ == "__main__": + from idlelib.idle_test.htest import run + run(_multi_call) diff --git a/Lib/idlelib/MultiStatusBar.py b/Lib/idlelib/MultiStatusBar.py index 4fc8dcf94b..e82ba9ab2f 100644 --- a/Lib/idlelib/MultiStatusBar.py +++ b/Lib/idlelib/MultiStatusBar.py @@ -8,25 +8,40 @@ class MultiStatusBar(Frame): Frame.__init__(self, master, **kw) self.labels = {} - def set_label(self, name, text='', side=LEFT): + def set_label(self, name, text='', side=LEFT, width=0): if name not in self.labels: - label = Label(self, bd=1, relief=SUNKEN, anchor=W) - label.pack(side=side) + label = Label(self, borderwidth=0, anchor=W) + label.pack(side=side, pady=0, padx=4) self.labels[name] = label else: label = self.labels[name] + if width != 0: + label.config(width=width) label.config(text=text) -def _test(): - b = Frame() - c = Text(b) - c.pack(side=TOP) - a = MultiStatusBar(b) - a.set_label("one", "hello") - a.set_label("two", "world") - a.pack(side=BOTTOM, fill=X) - b.pack() - b.mainloop() +def _multistatus_bar(parent): + root = Tk() + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d" %(x, y + 150)) + root.title("Test multistatus bar") + frame = Frame(root) + text = Text(frame) + text.pack() + msb = MultiStatusBar(frame) + msb.set_label("one", "hello") + msb.set_label("two", "world") + msb.pack(side=BOTTOM, fill=X) + + def change(): + msb.set_label("one", "foo") + msb.set_label("two", "bar") + + button = Button(root, text="Update status", command=change) + button.pack(side=BOTTOM) + frame.pack() + frame.mainloop() + root.mainloop() if __name__ == '__main__': - _test() + from idlelib.idle_test.htest import run + run(_multistatus_bar) diff --git a/Lib/idlelib/NEWS.txt b/Lib/idlelib/NEWS.txt index 6388d0dc31..8b8e10b5ea 100644 --- a/Lib/idlelib/NEWS.txt +++ b/Lib/idlelib/NEWS.txt @@ -1,74 +1,218 @@ -What's New in IDLE 3.3.4? +What's New in Idle 3.4.4? ========================= +*Release date: 2015-12-20* -- Issue #17390: Add Python version to Idle editor window title bar. - Original patches by Edmond Burnett and Kent Johnson. +- Issue 15348: Stop the debugger engine (normally in a user process) + before closing the debugger window (running in the IDLE process). + This prevents the RuntimeErrors that were being caught and ignored. -- Issue #18960: IDLE now ignores the source encoding declaration on the second - line if the first line contains anything except a comment. +- Issue #24455: Prevent IDLE from hanging when a) closing the shell while the + debugger is active (15347); b) closing the debugger with the [X] button + (15348); and c) activating the debugger when already active (24455). + The patch by Mark Roseman does this by making two changes. + 1. Suspend and resume the gui.interaction method with the tcl vwait + mechanism intended for this purpose (instead of root.mainloop & .quit). + 2. In gui.run, allow any existing interaction to terminate first. -- Issue #20058: sys.stdin.readline() in IDLE now always returns only one line. +- Change 'The program' to 'Your program' in an IDLE 'kill program?' message + to make it clearer that the program referred to is the currently running + user program, not IDLE itself. -- Issue #19481: print() of string subclass instance in IDLE no longer hangs. +- Issue #24750: Improve the appearance of the IDLE editor window status bar. + Patch by Mark Roseman. -- Issue #18270: Prevent possible IDLE AttributeError on OS X when no initial - shell window is present. +- Issue #25313: Change the handling of new built-in text color themes to better + address the compatibility problem introduced by the addition of IDLE Dark. + Consistently use the revised idleConf.CurrentTheme everywhere in idlelib. +- Issue #24782: Extension configuration is now a tab in the IDLE Preferences + dialog rather than a separate dialog. The former tabs are now a sorted + list. Patch by Mark Roseman. -What's New in IDLE 3.3.3? +- Issue #22726: Re-activate the config dialog help button with some content + about the other buttons and the new IDLE Dark theme. + +- Issue #24820: IDLE now has an 'IDLE Dark' built-in text color theme. + It is more or less IDLE Classic inverted, with a cobalt blue background. + Strings, comments, keywords, ... are still green, red, orange, ... . + To use it with IDLEs released before November 2015, hit the + 'Save as New Custom Theme' button and enter a new name, + such as 'Custom Dark'. The custom theme will work with any IDLE + release, and can be modified. + +- Issue #25224: README.txt is now an idlelib index for IDLE developers and + curious users. The previous user content is now in the IDLE doc chapter. + 'IDLE' now means 'Integrated Development and Learning Environment'. + +- Issue #24820: Users can now set breakpoint colors in + Settings -> Custom Highlighting. Original patch by Mark Roseman. + +- Issue #24972: Inactive selection background now matches active selection + background, as configured by users, on all systems. Found items are now + always highlighted on Windows. Initial patch by Mark Roseman. + +- Issue #24570: Idle: make calltip and completion boxes appear on Macs + affected by a tk regression. Initial patch by Mark Roseman. + +- Issue #24988: Idle ScrolledList context menus (used in debugger) + now work on Mac Aqua. Patch by Mark Roseman. + +- Issue #24801: Make right-click for context menu work on Mac Aqua. + Patch by Mark Roseman. + +- Issue #25173: Associate tkinter messageboxes with a specific widget. + For Mac OSX, make them a 'sheet'. Patch by Mark Roseman. + +- Issue #25198: Enhance the initial html viewer now used for Idle Help. + * Properly indent fixed-pitch text (patch by Mark Roseman). + * Give code snippet a very Sphinx-like light blueish-gray background. + * Re-use initial width and height set by users for shell and editor. + * When the Table of Contents (TOC) menu is used, put the section header + at the top of the screen. + +- Issue #25225: Condense and rewrite Idle doc section on text colors. + +- Issue #21995: Explain some differences between IDLE and console Python. + +- Issue #22820: Explain need for *print* when running file from Idle editor. + +- Issue #25224: Doc: augment Idle feature list and no-subprocess section. + +- Issue #25219: Update doc for Idle command line options. + Some were missing and notes were not correct. + +- Issue #24861: Most of idlelib is private and subject to change. + Use idleib.idle.* to start Idle. See idlelib.__init__.__doc__. + +- Issue #25199: Idle: add synchronization comments for future maintainers. + +- Issue #16893: Replace help.txt with help.html for Idle doc display. + The new idlelib/help.html is rstripped Doc/build/html/library/idle.html. + It looks better than help.txt and will better document Idle as released. + The tkinter html viewer that works for this file was written by Mark Roseman. + The now unused EditorWindow.HelpDialog class and helt.txt file are deprecated. + +- Issue #24199: Deprecate unused idlelib.idlever with possible removal in 3.6. + +- Issue #24790: Remove extraneous code (which also create 2 & 3 conflicts). + +- Issue #23672: Allow Idle to edit and run files with astral chars in name. + Patch by Mohd Sanad Zaki Rizvi. + +- Issue 24745: Idle editor default font. Switch from Courier to + platform-sensitive TkFixedFont. This should not affect current customized + font selections. If there is a problem, edit $HOME/.idlerc/config-main.cfg + and remove 'fontxxx' entries from [Editor Window]. Patch by Mark Roseman. + +- Issue #21192: Idle editor. When a file is run, put its name in the restart bar. + Do not print false prompts. Original patch by Adnan Umer. + +- Issue #13884: Idle menus. Remove tearoff lines. Patch by Roger Serwy. + +- Issue #23184: remove unused names and imports in idlelib. + Initial patch by Al Sweigart. + + +What's New in Idle 3.4.3? ========================= +*Release date: 2015-02-25* -- Issue #18873: IDLE now detects Python source code encoding only in comment - lines. +- Issue #20577: Configuration of the max line length for the FormatParagraph + extension has been moved from the General tab of the Idle preferences dialog + to the FormatParagraph tab of the Config Extensions dialog. + Patch by Tal Einat. -- Issue #18988: The "Tab" key now works when a word is already autocompleted. +- Issue #16893: Update Idle doc chapter to match current Idle and add new + information. -- Issue #18489: Add tests for SearchEngine. Original patch by Phil Webster. +- Issue #3068: Add Idle extension configuration dialog to Options menu. + Changes are written to HOME/.idlerc/config-extensions.cfg. + Original patch by Tal Einat. -- Issue #18429: Format / Format Paragraph, now works when comment blocks - are selected. As with text blocks, this works best when the selection - only includes complete lines. +- Issue #16233: A module browser (File : Class Browser, Alt+C) requires a + editor window with a filename. When Class Browser is requested otherwise, + from a shell, output window, or 'Untitled' editor, Idle no longer displays + an error box. It now pops up an Open Module box (Alt+M). If a valid name + is entered and a module is opened, a corresponding browser is also opened. -- Issue #18226: Add docstrings and unittests for FormatParagraph.py. - Original patches by Todd Rovito and Phil Webster. +- Issue #4832: Save As to type Python files automatically adds .py to the + name you enter (even if your system does not display it). Some systems + automatically add .txt when type is Text files. -- Issue #18279: Format - Strip trailing whitespace no longer marks a file as - changed when it has not been changed. This fix followed the addition of a - test file originally written by Phil Webster (the issue's main goal). +- Issue #21986: Code objects are not normally pickled by the pickle module. + To match this, they are no longer pickled when running under Idle. -- Issue #7136: In the Idle File menu, "New Window" is renamed "New File". - Patch by Tal Einat, Roget Serwy, and Todd Rovito. +- Issue #23180: Rename IDLE "Windows" menu item to "Window". + Patch by Al Sweigart. + + +What's New in IDLE 3.4.2? +========================= +*Release date: 2014-10-06* -- Remove dead imports of imp. +- Issue #17390: Adjust Editor window title; remove 'Python', + move version to end. -- Issue #18196: Avoid displaying spurious SystemExit tracebacks. +- Issue #14105: Idle debugger breakpoints no longer disappear + when inseting or deleting lines. -- Issue #5492: Avoid traceback when exiting IDLE caused by a race condition. +- Issue #17172: Turtledemo can now be run from Idle. + Currently, the entry is on the Help menu, but it may move to Run. + Patch by Ramchandra Apt and Lita Cho. -- Issue #17511: Keep IDLE find dialog open after clicking "Find Next". - Original patch by Sarah K. +- Issue #21765: Add support for non-ascii identifiers to HyperParser. -- Issue #18055: Move IDLE off of imp and on to importlib. +- Issue #21940: Add unittest for WidgetRedirector. Initial patch by Saimadhav + Heblikar. -- Issue #15392: Create a unittest framework for IDLE. - Initial patch by Rajagopalasarma Jayakrishnan. - See Lib/idlelib/idle_test/README.txt for how to run Idle tests. +- Issue #18592: Add unittest for SearchDialogBase. Patch by Phil Webster. -- Issue #14146: Highlight source line while debugging on Windows. +- Issue #21694: Add unittest for ParenMatch. Patch by Saimadhav Heblikar. -- Issue #17532: Always include Options menu for IDLE on OS X. - Patch by Guilherme Simões. +- Issue #21686: add unittest for HyperParser. Original patch by Saimadhav + Heblikar. +- Issue #12387: Add missing upper(lower)case versions of default Windows key + bindings for Idle so Caps Lock does not disable them. Patch by Roger Serwy. -What's New in IDLE 3.3.2? +- Issue #21695: Closing a Find-in-files output window while the search is + still in progress no longer closes Idle. + +- Issue #18910: Add unittest for textView. Patch by Phil Webster. + +- Issue #18292: Add unittest for AutoExpand. Patch by Saihadhav Heblikar. + +- Issue #18409: Add unittest for AutoComplete. Patch by Phil Webster. + +- Issue #18104: Add idlelib/idle_test/htest.py with a few sample tests to begin + consolidating and improving human-validated tests of Idle. Change other files + as needed to work with htest. Running the module as __main__ runs all tests. + + +What's New in IDLE 3.4.1? ========================= +*Release date: 2014-05-18* -- Issue #17390: Display Python version on Idle title bar. - Initial patch by Edmond Burnett. +- Issue #18104: Add idlelib/idle_test/htest.py with a few sample tests to begin + consolidating and improving human-validated tests of Idle. Change other files + as needed to work with htest. Running the module as __main__ runs all tests. + +- Issue #21139: Change default paragraph width to 72, the PEP 8 recommendation. + +- Issue #21284: Paragraph reformat test passes after user changes reformat width. + +- Issue #17654: Ensure IDLE menus are customized properly on OS X for + non-framework builds and for all variants of Tk. -What's New in IDLE 3.3.1? +What's New in IDLE 3.4.0? ========================= +*Release date: 2014-03-16* + +- Issue #17390: Display Python version on Idle title bar. + Initial patch by Edmond Burnett. + +- Issue #5066: Update IDLE docs. Patch by Todd Rovito. - Issue #17625: Close the replace dialog after it is used. @@ -81,6 +225,7 @@ What's New in IDLE 3.3.1? What's New in IDLE 3.3.0? ========================= +*Release date: 2012-09-29* - Issue #17625: Close the replace dialog after it is used. @@ -123,7 +268,6 @@ What's New in IDLE 3.3.0? What's New in IDLE 3.2.1? ========================= - *Release date: 15-May-11* - Issue #6378: Further adjust idle.bat to start associated Python @@ -141,7 +285,6 @@ What's New in IDLE 3.2.1? What's New in IDLE 3.1b1? ========================= - *Release date: 06-May-09* - Use of 'filter' in keybindingDialog.py was causing custom key assignment to @@ -150,7 +293,6 @@ What's New in IDLE 3.1b1? What's New in IDLE 3.1a1? ========================= - *Release date: 07-Mar-09* - Issue #4815: Offer conversion to UTF-8 if source files have @@ -166,9 +308,9 @@ What's New in IDLE 3.1a1? - Issue #2665: On Windows, an IDLE installation upgraded from an old version would not start if a custom theme was defined. + What's New in IDLE 2.7? (UNRELEASED, but merged into 3.1 releases above.) ======================= - *Release date: XX-XXX-2010* - idle.py modified and simplified to better support developing experimental @@ -192,9 +334,9 @@ What's New in IDLE 2.7? (UNRELEASED, but merged into 3.1 releases above.) - Issue #3549: On MacOS the preferences menu was not present + What's New in IDLE 3.0 final? ============================= - *Release date: 03-Dec-2008* - IDLE would print a "Unhandled server exception!" message when internal @@ -209,7 +351,6 @@ What's New in IDLE 3.0 final? What's New in IDLE 3.0a3? ========================= - *Release date: 29-Feb-2008* - help() was not paging to the shell. Issue1650. @@ -226,7 +367,6 @@ What's New in IDLE 3.0a3? What's New in IDLE 3.0a2? ========================= - *Release date: 06-Dec-2007* - Windows EOL sequence not converted correctly, encoding error. @@ -235,7 +375,6 @@ What's New in IDLE 3.0a2? What's New in IDLE 3.0a1? ========================= - *Release date: 31-Aug-2007* - IDLE converted to Python 3000 syntax. @@ -251,9 +390,8 @@ What's New in IDLE 3.0a1? be cleared before IDLE exits. -What's New in IDLE 2.6 final? -============================= - +What's New in IDLE 2.6 +====================== *Release date: 01-Oct-2008*, merged into 3.0 releases detailed above (3.0rc2) - Issue #2665: On Windows, an IDLE installation upgraded from an old version @@ -338,15 +476,8 @@ What's New in IDLE 2.6 final? What's New in IDLE 1.2? ======================= - *Release date: 19-SEP-2006* - -What's New in IDLE 1.2c1? -========================= - -*Release date: 17-AUG-2006* - - File menu hotkeys: there were three 'p' assignments. Reassign the 'Save Copy As' and 'Print' hotkeys to 'y' and 't'. Change the Shell hotkey from 's' to 'l'. @@ -367,11 +498,6 @@ What's New in IDLE 1.2c1? - When used w/o subprocess, all exceptions were preceded by an error message claiming they were IDLE internal errors (since 1.2a1). -What's New in IDLE 1.2b3? -========================= - -*Release date: 03-AUG-2006* - - Bug #1525817: Don't truncate short lines in IDLE's tool tips. - Bug #1517990: IDLE keybindings on MacOS X now work correctly @@ -395,26 +521,6 @@ What's New in IDLE 1.2b3? 'as' keyword in comment directly following import command. Closes 1325071. Patch 1479219 Tal Einat -What's New in IDLE 1.2b2? -========================= - -*Release date: 11-JUL-2006* - -What's New in IDLE 1.2b1? -========================= - -*Release date: 20-JUN-2006* - -What's New in IDLE 1.2a2? -========================= - -*Release date: 27-APR-2006* - -What's New in IDLE 1.2a1? -========================= - -*Release date: 05-APR-2006* - - Patch #1162825: Support non-ASCII characters in IDLE window titles. - Source file f.flush() after writing; trying to avoid lossage if user @@ -494,19 +600,14 @@ What's New in IDLE 1.2a1? - The remote procedure call module rpc.py can now access data attributes of remote registered objects. Changes to these attributes are local, however. + What's New in IDLE 1.1? ======================= - *Release date: 30-NOV-2004* - On OpenBSD, terminating IDLE with ctrl-c from the command line caused a stuck subprocess MainThread because only the SocketThread was exiting. -What's New in IDLE 1.1b3/rc1? -============================= - -*Release date: 18-NOV-2004* - - Saving a Keyset w/o making changes (by using the "Save as New Custom Key Set" button) caused IDLE to fail on restart (no new keyset was created in config-keys.cfg). Also true for Theme/highlights. Python Bug 1064535. @@ -514,28 +615,12 @@ What's New in IDLE 1.1b3/rc1? - A change to the linecache.py API caused IDLE to exit when an exception was raised while running without the subprocess (-n switch). Python Bug 1063840. -What's New in IDLE 1.1b2? -========================= - -*Release date: 03-NOV-2004* - - When paragraph reformat width was made configurable, a bug was introduced that caused reformatting of comment blocks to ignore how far the block was indented, effectively adding the indentation width to the reformat width. This has been repaired, and the reformat width is again a bound on the total width of reformatted lines. -What's New in IDLE 1.1b1? -========================= - -*Release date: 15-OCT-2004* - - -What's New in IDLE 1.1a3? -========================= - -*Release date: 02-SEP-2004* - - Improve keyboard focus binding, especially in Windows menu. Improve window raising, especially in the Windows menu and in the debugger. IDLEfork 763524. @@ -543,24 +628,12 @@ What's New in IDLE 1.1a3? - If user passes a non-existent filename on the commandline, just open a new file, don't raise a dialog. IDLEfork 854928. - -What's New in IDLE 1.1a2? -========================= - -*Release date: 05-AUG-2004* - - EditorWindow.py was not finding the .chm help file on Windows. Typo at Rev 1.54. Python Bug 990954 - checking sys.platform for substring 'win' was breaking IDLE docs on Mac (darwin). Also, Mac Safari browser requires full file:// URIs. SF 900580. - -What's New in IDLE 1.1a1? -========================= - -*Release date: 08-JUL-2004* - - Redirect the warning stream to the shell during the ScriptBinding check of user code and format the warning similarly to an exception for both that check and for runtime warnings raised in the subprocess. @@ -623,26 +696,13 @@ What's New in IDLE 1.1a1? What's New in IDLE 1.0? ======================= - *Release date: 29-Jul-2003* - Added a banner to the shell discussing warnings possibly raised by personal firewall software. Added same comment to README.txt. - -What's New in IDLE 1.0 release candidate 2? -=========================================== - -*Release date: 24-Jul-2003* - - Calltip error when docstring was None Python Bug 775541 - -What's New in IDLE 1.0 release candidate 1? -=========================================== - -*Release date: 18-Jul-2003* - - Updated extend.txt, help.txt, and config-extensions.def to correctly reflect the current status of the configuration system. Python Bug 768469 @@ -658,12 +718,6 @@ What's New in IDLE 1.0 release candidate 1? sys.std{in|out|err}.encoding, for both the local and the subprocess case. SF IDLEfork patch 682347. - -What's New in IDLE 1.0b2? -========================= - -*Release date: 29-Jun-2003* - - Extend AboutDialog.ViewFile() to support file encodings. Make the CREDITS file Latin-1. @@ -702,7 +756,6 @@ What's New in IDLE 1.0b2? What's New in IDLEfork 0.9b1? ============================= - *Release date: 02-Jun-2003* - The current working directory of the execution environment (and shell @@ -804,10 +857,8 @@ What's New in IDLEfork 0.9b1? exception formatting to the subprocess. - What's New in IDLEfork 0.9 Alpha 2? =================================== - *Release date: 27-Jan-2003* - Updated INSTALL.txt to claify use of the python2 rpm. @@ -911,7 +962,6 @@ What's New in IDLEfork 0.9 Alpha 2? What's New in IDLEfork 0.9 Alpha 1? =================================== - *Release date: 31-Dec-2002* - First release of major new functionality. For further details refer to diff --git a/Lib/idlelib/ObjectBrowser.py b/Lib/idlelib/ObjectBrowser.py index b359efc1b4..7b57aa4c68 100644 --- a/Lib/idlelib/ObjectBrowser.py +++ b/Lib/idlelib/ObjectBrowser.py @@ -9,6 +9,8 @@ # XXX TO DO: # - for classes/modules, add "open source" to object browser +import re + from idlelib.TreeWidget import TreeItem, TreeNode, ScrolledCanvas from reprlib import Repr @@ -119,12 +121,14 @@ def make_objecttreeitem(labeltext, object, setfunction=None): c = ObjectTreeItem return c(labeltext, object, setfunction) -# Test script -def _test(): +def _object_browser(parent): import sys from tkinter import Tk root = Tk() + root.title("Test ObjectBrowser") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) root.configure(bd=0, bg="yellow") root.focus_set() sc = ScrolledCanvas(root, bg="white", highlightthickness=0, takefocus=1) @@ -135,4 +139,5 @@ def _test(): root.mainloop() if __name__ == '__main__': - _test() + from idlelib.idle_test.htest import run + run(_object_browser) diff --git a/Lib/idlelib/OutputWindow.py b/Lib/idlelib/OutputWindow.py index 9dacc492b5..e614f9b2bb 100644 --- a/Lib/idlelib/OutputWindow.py +++ b/Lib/idlelib/OutputWindow.py @@ -91,7 +91,7 @@ class OutputWindow(EditorWindow): "No special line", "The line you point at doesn't look like " "a valid file name followed by a line number.", - master=self.text) + parent=self.text) return filename, lineno = result edit = self.flist.open(filename) diff --git a/Lib/idlelib/ParenMatch.py b/Lib/idlelib/ParenMatch.py index 6d91b390d1..19bad8ce38 100644 --- a/Lib/idlelib/ParenMatch.py +++ b/Lib/idlelib/ParenMatch.py @@ -90,7 +90,8 @@ class ParenMatch: self.set_timeout = self.set_timeout_none def flash_paren_event(self, event): - indices = HyperParser(self.editwin, "insert").get_surrounding_brackets() + indices = (HyperParser(self.editwin, "insert") + .get_surrounding_brackets()) if indices is None: self.warn_mismatched() return @@ -167,6 +168,11 @@ class ParenMatch: # associate a counter with an event; only disable the "paren" # tag if the event is for the most recent timer. self.counter += 1 - self.editwin.text_frame.after(self.FLASH_DELAY, - lambda self=self, c=self.counter: \ - self.handle_restore_timer(c)) + self.editwin.text_frame.after( + self.FLASH_DELAY, + lambda self=self, c=self.counter: self.handle_restore_timer(c)) + + +if __name__ == '__main__': + import unittest + unittest.main('idlelib.idle_test.test_parenmatch', verbosity=2) diff --git a/Lib/idlelib/PathBrowser.py b/Lib/idlelib/PathBrowser.py index ba40719084..9ab7632f4d 100644 --- a/Lib/idlelib/PathBrowser.py +++ b/Lib/idlelib/PathBrowser.py @@ -4,13 +4,20 @@ import importlib.machinery from idlelib.TreeWidget import TreeItem from idlelib.ClassBrowser import ClassBrowser, ModuleBrowserTreeItem +from idlelib.PyShell import PyShellFileList + class PathBrowser(ClassBrowser): - def __init__(self, flist): + def __init__(self, flist, _htest=False): + """ + _htest - bool, change box location when running htest + """ + self._htest = _htest self.init(flist) def settitle(self): + "Set window titles." self.top.wm_title("Path Browser") self.top.wm_iconname("Path Browser") @@ -44,7 +51,7 @@ class DirBrowserTreeItem(TreeItem): def GetSubList(self): try: names = os.listdir(self.dir or os.curdir) - except os.error: + except OSError: return [] packages = [] for name in names: @@ -63,16 +70,17 @@ class DirBrowserTreeItem(TreeItem): return sublist def ispackagedir(self, file): + " Return true for directories that are packages." if not os.path.isdir(file): - return 0 + return False init = os.path.join(file, "__init__.py") return os.path.exists(init) def listmodules(self, allnames): modules = {} suffixes = importlib.machinery.EXTENSION_SUFFIXES[:] - suffixes += importlib.machinery.SOURCE_SUFFIXES[:] - suffixes += importlib.machinery.BYTECODE_SUFFIXES[:] + suffixes += importlib.machinery.SOURCE_SUFFIXES + suffixes += importlib.machinery.BYTECODE_SUFFIXES sorted = [] for suff in suffixes: i = -len(suff) @@ -87,12 +95,14 @@ class DirBrowserTreeItem(TreeItem): sorted.sort() return sorted -def main(): - from idlelib import PyShell - PathBrowser(PyShell.flist) - if sys.stdin is sys.__stdin__: - mainloop() +def _path_browser(parent): # htest # + flist = PyShellFileList(parent) + PathBrowser(flist, _htest=True) + parent.mainloop() if __name__ == "__main__": from unittest import main main('idlelib.idle_test.test_pathbrowser', verbosity=2, exit=False) + + from idlelib.idle_test.htest import run + run(_path_browser) diff --git a/Lib/idlelib/Percolator.py b/Lib/idlelib/Percolator.py index c91de38129..9e9331940f 100644 --- a/Lib/idlelib/Percolator.py +++ b/Lib/idlelib/Percolator.py @@ -51,8 +51,9 @@ class Percolator: f.setdelegate(filter.delegate) filter.setdelegate(None) -def main(): - import tkinter as Tk +def _percolator(parent): + import tkinter as tk + import re class Tracer(Delegator): def __init__(self, name): self.name = name @@ -63,22 +64,41 @@ def main(): def delete(self, *args): print(self.name, ": delete", args) self.delegate.delete(*args) - root = Tk.Tk() - root.wm_protocol("WM_DELETE_WINDOW", root.quit) - text = Tk.Text() - text.pack() - text.focus_set() + root = tk.Tk() + root.title("Test Percolator") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) + text = tk.Text(root) p = Percolator(text) t1 = Tracer("t1") t2 = Tracer("t2") - p.insertfilter(t1) - p.insertfilter(t2) - root.mainloop() # click close widget to continue... - p.removefilter(t2) - root.mainloop() - p.insertfilter(t2) - p.removefilter(t1) + + def toggle1(): + if var1.get() == 0: + var1.set(1) + p.insertfilter(t1) + elif var1.get() == 1: + var1.set(0) + p.removefilter(t1) + + def toggle2(): + if var2.get() == 0: + var2.set(1) + p.insertfilter(t2) + elif var2.get() == 1: + var2.set(0) + p.removefilter(t2) + + text.pack() + var1 = tk.IntVar() + cb1 = tk.Checkbutton(root, text="Tracer1", command=toggle1, variable=var1) + cb1.pack() + var2 = tk.IntVar() + cb2 = tk.Checkbutton(root, text="Tracer2", command=toggle2, variable=var2) + cb2.pack() + root.mainloop() if __name__ == "__main__": - main() + from idlelib.idle_test.htest import run + run(_percolator) diff --git a/Lib/idlelib/PyParse.py b/Lib/idlelib/PyParse.py index 61a0003ce5..9ccbb25076 100644 --- a/Lib/idlelib/PyParse.py +++ b/Lib/idlelib/PyParse.py @@ -1,5 +1,6 @@ import re import sys +from collections import Mapping # Reason last stmt is continued (or C_NONE if it's not). (C_NONE, C_BACKSLASH, C_STRING_FIRST_LINE, @@ -91,19 +92,48 @@ _chew_ordinaryre = re.compile(r""" [^[\](){}#'"\\]+ """, re.VERBOSE).match -# Build translation table to map uninteresting chars to "x", open -# brackets to "(", and close brackets to ")". -_tran = {} -for i in range(256): - _tran[i] = 'x' -for ch in "({[": - _tran[ord(ch)] = '(' -for ch in ")}]": - _tran[ord(ch)] = ')' -for ch in "\"'\\\n#": - _tran[ord(ch)] = ch -del i, ch +class StringTranslatePseudoMapping(Mapping): + r"""Utility class to be used with str.translate() + + This Mapping class wraps a given dict. When a value for a key is + requested via __getitem__() or get(), the key is looked up in the + given dict. If found there, the value from the dict is returned. + Otherwise, the default value given upon initialization is returned. + + This allows using str.translate() to make some replacements, and to + replace all characters for which no replacement was specified with + a given character instead of leaving them as-is. + + For example, to replace everything except whitespace with 'x': + + >>> whitespace_chars = ' \t\n\r' + >>> preserve_dict = {ord(c): ord(c) for c in whitespace_chars} + >>> mapping = StringTranslatePseudoMapping(preserve_dict, ord('x')) + >>> text = "a + b\tc\nd" + >>> text.translate(mapping) + 'x x x\tx\nx' + """ + def __init__(self, non_defaults, default_value): + self._non_defaults = non_defaults + self._default_value = default_value + + def _get(key, _get=non_defaults.get, _default=default_value): + return _get(key, _default) + self._get = _get + + def __getitem__(self, item): + return self._get(item) + + def __len__(self): + return len(self._non_defaults) + + def __iter__(self): + return iter(self._non_defaults) + + def get(self, key, default=None): + return self._get(key) + class Parser: @@ -113,19 +143,6 @@ class Parser: def set_str(self, s): assert len(s) == 0 or s[-1] == '\n' - if isinstance(s, str): - # The parse functions have no idea what to do with Unicode, so - # replace all Unicode characters with "x". This is "safe" - # so long as the only characters germane to parsing the structure - # of Python are 7-bit ASCII. It's *necessary* because Unicode - # strings don't have a .translate() method that supports - # deletechars. - uniphooey = s - s = [] - push = s.append - for raw in map(ord, uniphooey): - push(raw < 127 and chr(raw) or "x") - s = "".join(s) self.str = s self.study_level = 0 @@ -197,6 +214,16 @@ class Parser: if lo > 0: self.str = self.str[lo:] + # Build a translation table to map uninteresting chars to 'x', open + # brackets to '(', close brackets to ')' while preserving quotes, + # backslashes, newlines and hashes. This is to be passed to + # str.translate() in _study1(). + _tran = {} + _tran.update((ord(c), ord('(')) for c in "({[") + _tran.update((ord(c), ord(')')) for c in ")}]") + _tran.update((ord(c), ord(c)) for c in "\"'\\\n#") + _tran = StringTranslatePseudoMapping(_tran, default_value=ord('x')) + # As quickly as humanly possible <wink>, find the line numbers (0- # based) of the non-continuation lines. # Creates self.{goodlines, continuation}. @@ -211,7 +238,7 @@ class Parser: # uninteresting characters. This can cut the number of chars # by a factor of 10-40, and so greatly speed the following loop. str = self.str - str = str.translate(_tran) + str = str.translate(self._tran) str = str.replace('xxxxxxxx', 'x') str = str.replace('xxxx', 'x') str = str.replace('xx', 'x') diff --git a/Lib/idlelib/PyShell.py b/Lib/idlelib/PyShell.py index 2e5ebb233b..1bcc9b6814 100755 --- a/Lib/idlelib/PyShell.py +++ b/Lib/idlelib/PyShell.py @@ -10,8 +10,6 @@ import sys import threading import time import tokenize -import traceback -import types import io import linecache @@ -21,7 +19,7 @@ from platform import python_version, system try: from tkinter import * except ImportError: - print("** IDLE can't import Tkinter. " \ + print("** IDLE can't import Tkinter.\n" "Your Python may not be configured for Tk. **", file=sys.__stderr__) sys.exit(1) import tkinter.messagebox as tkMessageBox @@ -32,7 +30,6 @@ from idlelib.ColorDelegator import ColorDelegator from idlelib.UndoDelegator import UndoDelegator from idlelib.OutputWindow import OutputWindow from idlelib.configHandler import idleConf -from idlelib import idlever from idlelib import rpc from idlelib import Debugger from idlelib import RemoteDebugger @@ -138,6 +135,7 @@ class PyShellEditorWindow(EditorWindow): self.io.set_filename_change_hook(filename_changed_hook) if self.io.filename: self.restore_file_breaks() + self.color_breakpoint_text() rmenu_specs = [ ("Cut", "<<cut>>", "rmenu_check_cut"), @@ -148,12 +146,24 @@ class PyShellEditorWindow(EditorWindow): ("Clear Breakpoint", "<<clear-breakpoint-here>>", None) ] + def color_breakpoint_text(self, color=True): + "Turn colorizing of breakpoint text on or off" + if self.io is None: + # possible due to update in restore_file_breaks + return + if color: + theme = idleConf.CurrentTheme() + cfg = idleConf.GetHighlight(theme, "break") + else: + cfg = {'foreground': '', 'background': ''} + self.text.tag_config('BREAK', cfg) + def set_breakpoint(self, lineno): text = self.text filename = self.io.filename text.tag_add("BREAK", "%d.0" % lineno, "%d.0" % (lineno+1)) try: - i = self.breakpoints.index(lineno) + self.breakpoints.index(lineno) except ValueError: # only add if missing, i.e. do once self.breakpoints.append(lineno) try: # update the subprocess debugger @@ -217,13 +227,8 @@ class PyShellEditorWindow(EditorWindow): # This is necessary to keep the saved breaks synched with the # saved file. # - # Breakpoints are set as tagged ranges in the text. Certain - # kinds of edits cause these ranges to be deleted: Inserting - # or deleting a line just before a breakpoint, and certain - # deletions prior to a breakpoint. These issues need to be - # investigated and understood. It's not clear if they are - # Tk issues or IDLE issues, or whether they can actually - # be fixed. Since a modified file has to be saved before it is + # Breakpoints are set as tagged ranges in the text. + # Since a modified file has to be saved before it is # run, and since self.breakpoints (from which the subprocess # debugger is loaded) is updated during the save, the visible # breaks stay synched with the subprocess even if one of these @@ -333,7 +338,7 @@ class ModifiedColorDelegator(ColorDelegator): def LoadTagDefs(self): ColorDelegator.LoadTagDefs(self) - theme = idleConf.GetOption('main','Theme','name') + theme = idleConf.CurrentTheme() self.tagdefs.update({ "stdin": {'background':None,'foreground':None}, "stdout": idleConf.GetHighlight(theme, "stdout"), @@ -419,7 +424,7 @@ class ModifiedInterpreter(InteractiveInterpreter): try: self.rpcclt = MyRPCClient(addr) break - except socket.error as err: + except OSError: pass else: self.display_port_binding_error() @@ -440,7 +445,7 @@ class ModifiedInterpreter(InteractiveInterpreter): self.rpcclt.listening_sock.settimeout(10) try: self.rpcclt.accept() - except socket.timeout as err: + except socket.timeout: self.display_no_subprocess_error() return None self.rpcclt.register("console", self.tkconsole) @@ -454,7 +459,7 @@ class ModifiedInterpreter(InteractiveInterpreter): self.poll_subprocess() return self.rpcclt - def restart_subprocess(self, with_cwd=False): + def restart_subprocess(self, with_cwd=False, filename=''): if self.restarting: return self.rpcclt self.restarting = True @@ -475,25 +480,24 @@ class ModifiedInterpreter(InteractiveInterpreter): self.spawn_subprocess() try: self.rpcclt.accept() - except socket.timeout as err: + except socket.timeout: self.display_no_subprocess_error() return None self.transfer_path(with_cwd=with_cwd) console.stop_readline() # annotate restart in shell window and mark it console.text.delete("iomark", "end-1c") - if was_executing: - console.write('\n') - console.showprompt() - halfbar = ((int(console.width) - 16) // 2) * '=' - console.write(halfbar + ' RESTART ' + halfbar) + tag = 'RESTART: ' + (filename if filename else 'Shell') + halfbar = ((int(console.width) -len(tag) - 4) // 2) * '=' + console.write("\n{0} {1} {0}".format(halfbar, tag)) console.text.mark_set("restart", "end-1c") console.text.mark_gravity("restart", "left") - console.showprompt() + if not filename: + console.showprompt() # restart subprocess debugger if debug: # Restarted debugger connects to current instance of debug GUI - gui = RemoteDebugger.restart_subprocess_debugger(self.rpcclt) + RemoteDebugger.restart_subprocess_debugger(self.rpcclt) # reload remote debugger breakpoints for all PyShellEditWindows debug.load_breakpoints() self.compile.compiler.flags = self.original_compiler_flags @@ -617,7 +621,7 @@ class ModifiedInterpreter(InteractiveInterpreter): item = RemoteObjectBrowser.StubObjectTreeItem(self.rpcclt, oid) from idlelib.TreeWidget import ScrolledCanvas, TreeNode top = Toplevel(self.tkconsole.root) - theme = idleConf.GetOption('main','Theme','name') + theme = idleConf.CurrentTheme() background = idleConf.GetHighlight(theme, 'normal')['background'] sc = ScrolledCanvas(top, bg=background, highlightthickness=0) sc.frame.pack(expand=1, fill="both") @@ -641,9 +645,9 @@ class ModifiedInterpreter(InteractiveInterpreter): code = compile(source, filename, "exec") except (OverflowError, SyntaxError): self.tkconsole.resetoutput() - tkerr = self.tkconsole.stderr - print('*** Error in script or command!\n', file=tkerr) - print('Traceback (most recent call last):', file=tkerr) + print('*** Error in script or command!\n' + 'Traceback (most recent call last):', + file=self.tkconsole.stderr) InteractiveInterpreter.showsyntaxerror(self, filename) self.tkconsole.showprompt() else: @@ -770,7 +774,7 @@ class ModifiedInterpreter(InteractiveInterpreter): "Exit?", "Do you want to exit altogether?", default="yes", - master=self.tkconsole.text): + parent=self.tkconsole.text): raise else: self.showtraceback() @@ -808,7 +812,7 @@ class ModifiedInterpreter(InteractiveInterpreter): "Run IDLE with the -n command line switch to start without a " "subprocess and refer to Help/IDLE Help 'Running without a " "subprocess' for further details.", - master=self.tkconsole.text) + parent=self.tkconsole.text) def display_no_subprocess_error(self): tkMessageBox.showerror( @@ -816,14 +820,14 @@ class ModifiedInterpreter(InteractiveInterpreter): "IDLE's subprocess didn't make connection. Either IDLE can't " "start a subprocess or personal firewall software is blocking " "the connection.", - master=self.tkconsole.text) + parent=self.tkconsole.text) def display_executing_dialog(self): tkMessageBox.showerror( "Already executing", "The Python Shell window is already executing a command; " "please wait until it is finished.", - master=self.tkconsole.text) + parent=self.tkconsole.text) class PyShell(OutputWindow): @@ -840,13 +844,10 @@ class PyShell(OutputWindow): ("edit", "_Edit"), ("debug", "_Debug"), ("options", "_Options"), - ("windows", "_Windows"), + ("windows", "_Window"), ("help", "_Help"), ] - if macosxSupport.runningAsOSXApp(): - menu_specs[-2] = ("windows", "_Window") - # New classes from idlelib.IdleHistory import History @@ -930,7 +931,7 @@ class PyShell(OutputWindow): if self.executing: tkMessageBox.showerror("Don't debug now", "You can only toggle the debugger when idle", - master=self.text) + parent=self.text) self.set_debugger_indicator() return "break" else: @@ -988,7 +989,7 @@ class PyShell(OutputWindow): if self.executing: response = tkMessageBox.askokcancel( "Kill?", - "The program is still running!\n Do you want to kill it?", + "Your program is still running!\n Do you want to kill it?", default="ok", parent=self.text) if response is False: @@ -1034,11 +1035,15 @@ class PyShell(OutputWindow): self.close() return False else: - nosub = "==== No Subprocess ====" + nosub = ("==== No Subprocess ====\n\n" + + "WARNING: Running IDLE without a Subprocess is deprecated\n" + + "and will be removed in a later version. See Help/IDLE Help\n" + + "for details.\n\n") sys.displayhook = rpc.displayhook self.write("Python %s on %s\n%s\n%s" % (sys.version, sys.platform, self.COPYRIGHT, nosub)) + self.text.focus_force() self.showprompt() import tkinter tkinter._default_root = None # 03Jan04 KBK What's this? @@ -1223,7 +1228,7 @@ class PyShell(OutputWindow): while i > 0 and line[i-1] in " \t": i = i-1 line = line[:i] - more = self.interp.runsource(line) + self.interp.runsource(line) def open_stack_viewer(self, event=None): if self.interp.rpcclt: @@ -1234,10 +1239,10 @@ class PyShell(OutputWindow): tkMessageBox.showerror("No stack trace", "There is no stack trace yet.\n" "(sys.last_traceback is not defined)", - master=self.text) + parent=self.text) return from idlelib.StackViewer import StackBrowser - sv = StackBrowser(self.root, self.flist) + StackBrowser(self.root, self.flist) def view_restart_mark(self, event=None): self.text.see("iomark") @@ -1398,7 +1403,8 @@ USAGE: idle [-deins] [-t title] [file]* idle [-dns] [-t title] - [arg]* -h print this help message and exit - -n run IDLE without a subprocess (see Help/IDLE Help for details) + -n run IDLE without a subprocess (DEPRECATED, + see Help/IDLE Help for details) The following options will override the IDLE 'settings' configuration: @@ -1458,8 +1464,7 @@ def main(): try: opts, args = getopt.getopt(sys.argv[1:], "c:deihnr:st:") except getopt.error as msg: - sys.stderr.write("Error: %s\n" % str(msg)) - sys.stderr.write(usage_msg) + print("Error: %s\n%s" % (msg, usage_msg), file=sys.stderr) sys.exit(2) for o, a in opts: if o == '-c': @@ -1476,6 +1481,8 @@ def main(): if o == '-i': enable_shell = True if o == '-n': + print(" Warning: running IDLE without a subprocess is deprecated.", + file=sys.stderr) use_subprocess = False if o == '-r': script = a @@ -1541,6 +1548,14 @@ def main(): flist = PyShellFileList(root) macosxSupport.setupApp(root, flist) + if macosxSupport.isAquaTk(): + # There are some screwed up <2> class bindings for text + # widgets defined in Tk which we need to do away with. + # See issue #24801. + root.unbind_class('Text', '<B2>') + root.unbind_class('Text', '<B2-Motion>') + root.unbind_class('Text', '<<PasteSelection>>') + if enable_edit: if not (cmd or script): for filename in args[:]: @@ -1554,7 +1569,7 @@ def main(): shell = flist.open_shell() if not shell: return # couldn't open shell - if macosxSupport.runningAsOSXApp() and flist.dict: + if macosxSupport.isAquaTk() and flist.dict: # On OSX: when the user has double-clicked on a file that causes # IDLE to be launched the shell window will open just in front of # the file she wants to see. Lower the interpreter window when diff --git a/Lib/idlelib/README.txt b/Lib/idlelib/README.txt index b2bb73b065..7bf74c0fc4 100644 --- a/Lib/idlelib/README.txt +++ b/Lib/idlelib/README.txt @@ -1,60 +1,229 @@ -IDLE is Python's Tkinter-based Integrated DeveLopment Environment. - -IDLE emphasizes a lightweight, clean design with a simple user interface. -Although it is suitable for beginners, even advanced users will find that -IDLE has everything they really need to develop pure Python code. - -IDLE features a multi-window text editor with multiple undo, Python colorizing, -and many other capabilities, e.g. smart indent, call tips, and autocompletion. - -The editor has comprehensive search functions, including searching through -multiple files. Class browsers and path browsers provide fast access to -code objects from a top level viewpoint without dealing with code folding. - -There is a Python Shell window which features colorizing and command recall. - -IDLE executes Python code in a separate process, which is restarted for each -Run (F5) initiated from an editor window. The environment can also be -restarted from the Shell window without restarting IDLE. - -This enhancement has often been requested, and is now finally available. The -magic "reload/import *" incantations are no longer required when editing and -testing a module two or three steps down the import chain. - -(Personal firewall software may warn about the connection IDLE makes to its -subprocess using this computer's internal loopback interface. This connection -is not visible on any external interface and no data is sent to or received -from the Internet.) - -It is possible to interrupt tightly looping user code, even on Windows. - -Applications which cannot support subprocesses and/or sockets can still run -IDLE in a single process. - -IDLE has an integrated debugger with stepping, persistent breakpoints, and call -stack visibility. - -There is a GUI configuration manager which makes it easy to select fonts, -colors, keybindings, and startup options. This facility includes a feature -which allows the user to specify additional help sources, either locally or on -the web. - -IDLE is coded in 100% pure Python, using the Tkinter GUI toolkit (Tk/Tcl) -and is cross-platform, working on Unix, Mac, and Windows. - -IDLE accepts command line arguments. Try idle -h to see the options. - - -If you find bugs or have suggestions or patches, let us know about -them by using the Python issue tracker: - -http://bugs.python.org - -For further details and links, read the Help files and check the IDLE home -page at - -http://www.python.org/idle/ - -There is a mail list for IDLE: idle-dev@python.org. You can join at - -http://mail.python.org/mailman/listinfo/idle-dev +README.txt: an index to idlelib files and the IDLE menu. + +IDLE is Python's Integrated Development and Learning +Environment. The user documentation is part of the Library Reference and +is available in IDLE by selecting Help => IDLE Help. This README documents +idlelib for IDLE developers and curious users. + +IDLELIB FILES lists files alphabetically by category, +with a short description of each. + +IDLE MENU show the menu tree, annotated with the module +or module object that implements the corresponding function. + +This file is descriptive, not prescriptive, and may have errors +and omissions and lag behind changes in idlelib. + + +IDLELIB FILES +Implemetation files not in IDLE MENU are marked (nim). +Deprecated files and objects are listed separately as the end. + +Startup +------- +__init__.py # import, does nothing +__main__.py # -m, starts IDLE +idle.bat +idle.py +idle.pyw + +Implementation +-------------- +AutoComplete.py # Complete attribute names or filenames. +AutoCompleteWindow.py # Display completions. +AutoExpand.py # Expand word with previous word in file. +Bindings.py # Define most of IDLE menu. +CallTipWindow.py # Display calltip. +CallTips.py # Create calltip text. +ClassBrowser.py # Create module browser window. +CodeContext.py # Show compound statement headers otherwise not visible. +ColorDelegator.py # Colorize text (nim). +Debugger.py # Debug code run from editor; show window. +Delegator.py # Define base class for delegators (nim). +EditorWindow.py # Define most of editor and utility functions. +FileList.py # Open files and manage list of open windows (nim). +FormatParagraph.py# Re-wrap multiline strings and comments. +GrepDialog.py # Find all occurrences of pattern in multiple files. +HyperParser.py # Parse code around a given index. +IOBinding.py # Open, read, and write files +IdleHistory.py # Get previous or next user input in shell (nim) +MultiCall.py # Wrap tk widget to allow multiple calls per event (nim). +MultiStatusBar.py # Define status bar for windows (nim). +ObjectBrowser.py # Define class used in StackViewer (nim). +OutputWindow.py # Create window for grep output. +ParenMatch.py # Match fenceposts: (), [], and {}. +PathBrowser.py # Create path browser window. +Percolator.py # Manage delegator stack (nim). +PyParse.py # Give information on code indentation +PyShell.py # Start IDLE, manage shell, complete editor window +RemoteDebugger.py # Debug code run in remote process. +RemoteObjectBrowser.py # Communicate objects between processes with rpc (nim). +ReplaceDialog.py # Search and replace pattern in text. +RstripExtension.py# Strip trailing whitespace +ScriptBinding.py # Check and run user code. +ScrolledList.py # Define ScrolledList widget for IDLE (nim). +SearchDialog.py # Search for pattern in text. +SearchDialogBase.py # Define base for search, replace, and grep dialogs. +SearchEngine.py # Define engine for all 3 search dialogs. +StackViewer.py # View stack after exception. +TreeWidget.py # Define tree widger, used in browsers (nim). +UndoDelegator.py # Manage undo stack. +WidgetRedirector.py # Intercept widget subcommands (for percolator) (nim). +WindowList.py # Manage window list and define listed top level. +ZoomHeight.py # Zoom window to full height of screen. +aboutDialog.py # Display About IDLE dialog. +configDialog.py # Display user configuration dialogs. +configHandler.py # Load, fetch, and save configuration (nim). +configHelpSourceEdit.py # Specify help source. +configSectionNameDialog.py # Spefify user config section name +dynOptionMenuWidget.py # define mutable OptionMenu widget (nim). +help.py # Display IDLE's html doc. +keybindingDialog.py # Change keybindings. +macosxSupport.py # Help IDLE run on Macs (nim). +rpc.py # Commuicate between idle and user processes (nim). +run.py # Manage user code execution subprocess. +tabbedpages.py # Define tabbed pages widget (nim). +textView.py # Define read-only text widget (nim). + +Configuration +------------- +config-extensions.def # Defaults for extensions +config-highlight.def # Defaults for colorizing +config-keys.def # Defaults for key bindings +config-main.def # Defai;ts fpr font and geneal + +Text +---- +CREDITS.txt # not maintained, displayed by About IDLE +HISTORY.txt # NEWS up to July 2001 +NEWS.txt # commits, displayed by About IDLE +README.txt # this file, displeyed by About IDLE +TODO.txt # needs review +extend.txt # about writing extensions +help.html # copy of idle.html in docs, displayed by IDLE Help + +Subdirectories +-------------- +Icons # small image files +idle_test # files for human test and automated unit tests + +Unused and Deprecated files and objects (nim) +--------------------------------------------- +EditorWindow.py: Helpdialog and helpDialog +ToolTip.py: unused. +help.txt +idlever.py + + +IDLE MENUS +Top level items and most submenu items are defined in Bindings. +Extenstions add submenu items when active. The names given are +found, quoted, in one of these modules, paired with a '<<pseudoevent>>'. +Each pseudoevent is bound to an event handler. Some event handlers +call another function that does the actual work. The annotations below +are intended to at least give the module where the actual work is done. + +File # IOBindig except as noted + New File + Open... # IOBinding.open + Open Module + Recent Files + Class Browser # Class Browser + Path Browser # Path Browser + --- + Save # IDBinding.save + Save As... # IOBinding.save_as + Save Copy As... # IOBindling.save_a_copy + --- + Print Window # IOBinding.print_window + --- + Close + Exit + +Edit + Undo # undoDelegator + Redo # undoDelegator + --- + Cut + Copy + Paste + Select All + --- # Next 5 items use SearchEngine; dialogs use SearchDialogBase + Find # Search Dialog + Find Again + Find Selection + Find in Files... # GrepDialog + Replace... # ReplaceDialog + Go to Line + Show Completions # AutoComplete extension and AutoCompleteWidow (&HP) + Expand Word # AutoExpand extension + Show call tip # Calltips extension and CalltipWindow (& Hyperparser) + Show surrounding parens # ParenMatch (& Hyperparser) + +Shell # PyShell + View Last Restart # PyShell.? + Restart Shell # PyShell.? + +Debug (Shell only) + Go to File/Line + Debugger # Debugger, RemoteDebugger + Stack Viewer # StackViewer + Auto-open Stack Viewer # StackViewer + +Format (Editor only) + Indent Region + Dedent Region + Comment Out Region + Uncomment Region + Tabify Region + Untabify Region + Toggle Tabs + New Indent Width + Format Paragraph # FormatParagraph extension + --- + Strip tailing whitespace # RstripExtension extension + +Run (Editor only) + Python Shell # PyShell + --- + Check Module # ScriptBinding + Run Module # ScriptBinding + +Options + Configure IDLE # configDialog + (tabs in the dialog) + Font tab # onfig-main.def + Highlight tab # configSectionNameDialog, config-highlight.def + Keys tab # keybindingDialog, configSectionNameDialog, onfig-keus.def + General tab # configHelpSourceEdit, config-main.def + Configure Extensions # configDialog + Xyz tab # xyz.py, config-extensions.def + --- + Code Context (editor only) # CodeContext extension + +Window + Zoomheight # ZoomHeight extension + --- + <open windows> # WindowList + +Help + About IDLE # aboutDialog + --- + IDLE Help # help + Python Doc + Turtle Demo + --- + <other help sources> + +<Context Menu> (right click) +Defined in EditorWindow, PyShell, Output + Cut + Copy + Paste + --- + Go to file/line (shell and output only) + Set Breakpoint (editor only) + Clear Breakpoint (editor only) + Defined in Debugger + Go to source line + Show stack frame diff --git a/Lib/idlelib/RemoteDebugger.py b/Lib/idlelib/RemoteDebugger.py index d8662bbd96..be2262f080 100644 --- a/Lib/idlelib/RemoteDebugger.py +++ b/Lib/idlelib/RemoteDebugger.py @@ -21,7 +21,6 @@ barrier, in particular frame and traceback objects. """ import types -from idlelib import rpc from idlelib import Debugger debugging = 0 @@ -99,7 +98,7 @@ class IdbAdapter: else: tb = tracebacktable[tbid] stack, i = self.idb.get_stack(frame, tb) - stack = [(wrap_frame(frame), k) for frame, k in stack] + stack = [(wrap_frame(frame2), k) for frame2, k in stack] return stack, i def run(self, cmd): diff --git a/Lib/idlelib/ReplaceDialog.py b/Lib/idlelib/ReplaceDialog.py index e73f2c5039..2665a1c630 100644 --- a/Lib/idlelib/ReplaceDialog.py +++ b/Lib/idlelib/ReplaceDialog.py @@ -40,7 +40,7 @@ class ReplaceDialog(SearchDialogBase): def create_entries(self): SearchDialogBase.create_entries(self) - self.replent = self.make_entry("Replace with:", self.replvar) + self.replent = self.make_entry("Replace with:", self.replvar)[0] def create_command_buttons(self): SearchDialogBase.create_command_buttons(self) @@ -59,7 +59,7 @@ class ReplaceDialog(SearchDialogBase): def default_command(self, event=None): if self.do_find(self.ok): if self.do_replace(): # Only find next match if replace succeeded. - # A bad re can cause a it to fail. + # A bad re can cause it to fail. self.do_find(0) def _replace_expand(self, m, repl): @@ -188,3 +188,34 @@ class ReplaceDialog(SearchDialogBase): def close(self, event=None): SearchDialogBase.close(self, event) self.text.tag_remove("hit", "1.0", "end") + +def _replace_dialog(parent): + root = Tk() + root.title("Test ReplaceDialog") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) + + # mock undo delegator methods + def undo_block_start(): + pass + + def undo_block_stop(): + pass + + text = Text(root) + text.undo_block_start = undo_block_start + text.undo_block_stop = undo_block_stop + text.pack() + text.insert("insert","This is a sample string.\n"*10) + + def show_replace(): + text.tag_add(SEL, "1.0", END) + replace(text) + text.tag_remove(SEL, "1.0", END) + + button = Button(root, text="Replace", command=show_replace) + button.pack() + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(_replace_dialog) diff --git a/Lib/idlelib/ScriptBinding.py b/Lib/idlelib/ScriptBinding.py index 6bfe128e3c..5cb818d833 100644 --- a/Lib/idlelib/ScriptBinding.py +++ b/Lib/idlelib/ScriptBinding.py @@ -18,13 +18,10 @@ XXX GvR Redesign this interface (yet again) as follows: """ import os -import re -import string import tabnanny import tokenize import tkinter.messagebox as tkMessageBox -from idlelib.EditorWindow import EditorWindow -from idlelib import PyShell, IOBinding +from idlelib import PyShell from idlelib.configHandler import idleConf from idlelib import macosxSupport @@ -39,6 +36,7 @@ To fix case 2, change all tabs to spaces by using Edit->Select All followed \ by Format->Untabify Region and specify the number of columns used by each tab. """ + class ScriptBinding: menudefs = [ @@ -53,7 +51,7 @@ class ScriptBinding: self.flist = self.editwin.flist self.root = self.editwin.root - if macosxSupport.runningAsOSXApp(): + if macosxSupport.isCocoaTk(): self.editwin.text_frame.bind('<<run-module-event-2>>', self._run_module_event) def check_module_event(self, event): @@ -71,7 +69,7 @@ class ScriptBinding: try: tabnanny.process_tokens(tokenize.generate_tokens(f.readline)) except tokenize.TokenError as msg: - msgtxt, (lineno, start) = msg + msgtxt, (lineno, start) = msg.args self.editwin.gotoline(lineno) self.errorbox("Tabnanny Tokenizing Error", "Token Error: %s" % msgtxt) @@ -114,7 +112,7 @@ class ScriptBinding: shell.set_warning_stream(saved_stream) def run_module_event(self, event): - if macosxSupport.runningAsOSXApp(): + if macosxSupport.isCocoaTk(): # Tk-Cocoa in MacOSX is broken until at least # Tk 8.5.9, and without this rather # crude workaround IDLE would hang when a user @@ -145,7 +143,8 @@ class ScriptBinding: return 'break' interp = self.shell.interp if PyShell.use_subprocess: - interp.restart_subprocess(with_cwd=False) + interp.restart_subprocess(with_cwd=False, filename= + self.editwin._filename_to_unicode(filename)) dirname = os.path.dirname(filename) # XXX Too often this discards arguments the user just set... interp.runcommand("""if 1: @@ -198,10 +197,10 @@ class ScriptBinding: confirm = tkMessageBox.askokcancel(title="Save Before Run or Check", message=msg, default=tkMessageBox.OK, - master=self.editwin.text) + parent=self.editwin.text) return confirm def errorbox(self, title, message): # XXX This should really be a function of EditorWindow... - tkMessageBox.showerror(title, message, master=self.editwin.text) + tkMessageBox.showerror(title, message, parent=self.editwin.text) self.editwin.text.focus_set() diff --git a/Lib/idlelib/ScrolledList.py b/Lib/idlelib/ScrolledList.py index 0255a0a23f..53576b5f82 100644 --- a/Lib/idlelib/ScrolledList.py +++ b/Lib/idlelib/ScrolledList.py @@ -1,4 +1,5 @@ from tkinter import * +from idlelib import macosxSupport class ScrolledList: @@ -22,7 +23,11 @@ class ScrolledList: # Bind events to the list box listbox.bind("<ButtonRelease-1>", self.click_event) listbox.bind("<Double-ButtonRelease-1>", self.double_click_event) - listbox.bind("<ButtonPress-3>", self.popup_event) + if macosxSupport.isAquaTk(): + listbox.bind("<ButtonPress-2>", self.popup_event) + listbox.bind("<Control-Button-1>", self.popup_event) + else: + listbox.bind("<ButtonPress-3>", self.popup_event) listbox.bind("<Key-Up>", self.up_event) listbox.bind("<Key-Down>", self.down_event) # Mark as empty @@ -119,21 +124,22 @@ class ScrolledList: pass -def test(): +def _scrolled_list(parent): root = Tk() - root.protocol("WM_DELETE_WINDOW", root.destroy) + root.title("Test ScrolledList") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) class MyScrolledList(ScrolledList): - def fill_menu(self): self.menu.add_command(label="pass") + def fill_menu(self): self.menu.add_command(label="right click") def on_select(self, index): print("select", self.get(index)) def on_double(self, index): print("double", self.get(index)) - s = MyScrolledList(root) + + scrolled_list = MyScrolledList(root) for i in range(30): - s.append("item %02d" % i) - return root + scrolled_list.append("Item %02d" % i) -def main(): - root = test() root.mainloop() if __name__ == '__main__': - main() + from idlelib.idle_test.htest import run + run(_scrolled_list) diff --git a/Lib/idlelib/SearchDialog.py b/Lib/idlelib/SearchDialog.py index bf76c419ac..77ef7b9a82 100644 --- a/Lib/idlelib/SearchDialog.py +++ b/Lib/idlelib/SearchDialog.py @@ -23,7 +23,7 @@ def find_selection(text): class SearchDialog(SearchDialogBase): def create_widgets(self): - f = SearchDialogBase.create_widgets(self) + SearchDialogBase.create_widgets(self) self.make_button("Find Next", self.default_command, 1) def default_command(self, event=None): @@ -65,3 +65,25 @@ class SearchDialog(SearchDialogBase): if pat: self.engine.setcookedpat(pat) return self.find_again(text) + +def _search_dialog(parent): + root = Tk() + root.title("Test SearchDialog") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) + text = Text(root) + text.pack() + text.insert("insert","This is a sample string.\n"*10) + + def show_find(): + text.tag_add(SEL, "1.0", END) + s = _setup(text) + s.open(text) + text.tag_remove(SEL, "1.0", END) + + button = Button(root, text="Search", command=show_find) + button.pack() + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(_search_dialog) diff --git a/Lib/idlelib/SearchDialogBase.py b/Lib/idlelib/SearchDialogBase.py index b8b49b2a1e..5fa84e238b 100644 --- a/Lib/idlelib/SearchDialogBase.py +++ b/Lib/idlelib/SearchDialogBase.py @@ -1,34 +1,51 @@ '''Define SearchDialogBase used by Search, Replace, and Grep dialogs.''' -from tkinter import * + +from tkinter import (Toplevel, Frame, Entry, Label, Button, + Checkbutton, Radiobutton) class SearchDialogBase: - '''Create most of a modal search dialog (make_frame, create_widgets). + '''Create most of a 3 or 4 row, 3 column search dialog. - The wide left column contains: - 1 or 2 text entry lines (create_entries, make_entry); - a row of standard radiobuttons (create_option_buttons); - a row of dialog specific radiobuttons (create_other_buttons). + The left and wide middle column contain: + 1 or 2 labeled text entry lines (make_entry, create_entries); + a row of standard Checkbuttons (make_frame, create_option_buttons), + each of which corresponds to a search engine Variable; + a row of dialog-specific Check/Radiobuttons (create_other_buttons). The narrow right column contains command buttons - (create_command_buttons, make_button). + (make_button, create_command_buttons). These are bound to functions that execute the command. - Except for command buttons, this base class is not limited to - items common to all three subclasses. Rather, it is the Find dialog - minus the "Find Next" command and its execution function. - The other dialogs override methods to replace and add widgets. + Except for command buttons, this base class is not limited to items + common to all three subclasses. Rather, it is the Find dialog minus + the "Find Next" command, its execution function, and the + default_command attribute needed in create_widgets. The other + dialogs override attributes and methods, the latter to replace and + add widgets. ''' - title = "Search Dialog" + title = "Search Dialog" # replace in subclasses icon = "Search" - needwrapbutton = 1 + needwrapbutton = 1 # not in Find in Files def __init__(self, root, engine): + '''Initialize root, engine, and top attributes. + + top (level widget): set in create_widgets() called from open(). + text (Text searched): set in open(), only used in subclasses(). + ent (ry): created in make_entry() called from create_entry(). + row (of grid): 0 in create_widgets(), +1 in make_entry/frame(). + default_command: set in subclasses, used in create_widgers(). + + title (of dialog): class attribute, override in subclasses. + icon (of dialog): ditto, use unclear if cannot minimize dialog. + ''' self.root = root self.engine = engine self.top = None def open(self, text, searchphrase=None): + "Make dialog visible on top of others and ready to use." self.text = text if not self.top: self.create_widgets() @@ -44,11 +61,17 @@ class SearchDialogBase: self.top.grab_set() def close(self, event=None): + "Put dialog away for later use." if self.top: self.top.grab_release() self.top.withdraw() def create_widgets(self): + '''Create basic 3 row x 3 col search (find) dialog. + + Other dialogs override subsidiary create_x methods as needed. + Replace and Find-in-Files add another entry row. + ''' top = Toplevel(self.root) top.bind("<Return>", self.default_command) top.bind("<Escape>", self.close) @@ -61,29 +84,84 @@ class SearchDialogBase: self.top.grid_columnconfigure(0, pad=2, weight=0) self.top.grid_columnconfigure(1, pad=2, minsize=100, weight=100) - self.create_entries() - self.create_option_buttons() - self.create_other_buttons() - return self.create_command_buttons() - - def make_entry(self, label, var): - l = Label(self.top, text=label) - l.grid(row=self.row, column=0, sticky="nw") - e = Entry(self.top, textvariable=var, exportselection=0) - e.grid(row=self.row, column=1, sticky="nwe") + self.create_entries() # row 0 (and maybe 1), cols 0, 1 + self.create_option_buttons() # next row, cols 0, 1 + self.create_other_buttons() # next row, cols 0, 1 + self.create_command_buttons() # col 2, all rows + + def make_entry(self, label_text, var): + '''Return (entry, label), . + + entry - gridded labeled Entry for text entry. + label - Label widget, returned for testing. + ''' + label = Label(self.top, text=label_text) + label.grid(row=self.row, column=0, sticky="nw") + entry = Entry(self.top, textvariable=var, exportselection=0) + entry.grid(row=self.row, column=1, sticky="nwe") self.row = self.row + 1 - return e + return entry, label + + def create_entries(self): + "Create one or more entry lines with make_entry." + self.ent = self.make_entry("Find:", self.engine.patvar)[0] def make_frame(self,labeltext=None): + '''Return (frame, label). + + frame - gridded labeled Frame for option or other buttons. + label - Label widget, returned for testing. + ''' if labeltext: - l = Label(self.top, text=labeltext) - l.grid(row=self.row, column=0, sticky="nw") - f = Frame(self.top) - f.grid(row=self.row, column=1, columnspan=1, sticky="nwe") + label = Label(self.top, text=labeltext) + label.grid(row=self.row, column=0, sticky="nw") + else: + label = '' + frame = Frame(self.top) + frame.grid(row=self.row, column=1, columnspan=1, sticky="nwe") self.row = self.row + 1 - return f + return frame, label + + def create_option_buttons(self): + '''Return (filled frame, options) for testing. + + Options is a list of SearchEngine booleanvar, label pairs. + A gridded frame from make_frame is filled with a Checkbutton + for each pair, bound to the var, with the corresponding label. + ''' + frame = self.make_frame("Options")[0] + engine = self.engine + options = [(engine.revar, "Regular expression"), + (engine.casevar, "Match case"), + (engine.wordvar, "Whole word")] + if self.needwrapbutton: + options.append((engine.wrapvar, "Wrap around")) + for var, label in options: + btn = Checkbutton(frame, anchor="w", variable=var, text=label) + btn.pack(side="left", fill="both") + if var.get(): + btn.select() + return frame, options + + def create_other_buttons(self): + '''Return (frame, others) for testing. + + Others is a list of value, label pairs. + A gridded frame from make_frame is filled with radio buttons. + ''' + frame = self.make_frame("Direction")[0] + var = self.engine.backvar + others = [(1, 'Up'), (0, 'Down')] + for val, label in others: + btn = Radiobutton(frame, anchor="w", + variable=var, value=val, text=label) + btn.pack(side="left", fill="both") + if var.get() == val: + btn.select() + return frame, others def make_button(self, label, command, isdef=0): + "Return command button gridded in command frame." b = Button(self.buttonframe, text=label, command=command, default=isdef and "active" or "normal") @@ -92,66 +170,15 @@ class SearchDialogBase: self.buttonframe.grid(rowspan=rows+1) return b - def create_entries(self): - self.ent = self.make_entry("Find:", self.engine.patvar) - - def create_option_buttons(self): - f = self.make_frame("Options") - - btn = Checkbutton(f, anchor="w", - variable=self.engine.revar, - text="Regular expression") - btn.pack(side="left", fill="both") - if self.engine.isre(): - btn.select() - - btn = Checkbutton(f, anchor="w", - variable=self.engine.casevar, - text="Match case") - btn.pack(side="left", fill="both") - if self.engine.iscase(): - btn.select() - - btn = Checkbutton(f, anchor="w", - variable=self.engine.wordvar, - text="Whole word") - btn.pack(side="left", fill="both") - if self.engine.isword(): - btn.select() - - if self.needwrapbutton: - btn = Checkbutton(f, anchor="w", - variable=self.engine.wrapvar, - text="Wrap around") - btn.pack(side="left", fill="both") - if self.engine.iswrap(): - btn.select() - - def create_other_buttons(self): - f = self.make_frame("Direction") - - #lbl = Label(f, text="Direction: ") - #lbl.pack(side="left") - - btn = Radiobutton(f, anchor="w", - variable=self.engine.backvar, value=1, - text="Up") - btn.pack(side="left", fill="both") - if self.engine.isback(): - btn.select() - - btn = Radiobutton(f, anchor="w", - variable=self.engine.backvar, value=0, - text="Down") - btn.pack(side="left", fill="both") - if not self.engine.isback(): - btn.select() - def create_command_buttons(self): - # - # place button frame on the right + "Place buttons in vertical command frame gridded on right." f = self.buttonframe = Frame(self.top) f.grid(row=0,column=2,padx=2,pady=2,ipadx=2,ipady=2) b = self.make_button("close", self.close) b.lower() + +if __name__ == '__main__': + import unittest + unittest.main( + 'idlelib.idle_test.test_searchdialogbase', verbosity=2) diff --git a/Lib/idlelib/SearchEngine.py b/Lib/idlelib/SearchEngine.py index 9d3c4cb78a..37883bf687 100644 --- a/Lib/idlelib/SearchEngine.py +++ b/Lib/idlelib/SearchEngine.py @@ -85,7 +85,7 @@ class SearchEngine: except re.error as what: args = what.args msg = args[0] - col = arg[1] if len(args) >= 2 else -1 + col = args[1] if len(args) >= 2 else -1 self.report_error(pat, msg, col) return None return prog @@ -107,7 +107,7 @@ class SearchEngine: It directly return the result of that call. Text is a text widget. Prog is a precompiled pattern. - The ok parameteris a bit complicated as it has two effects. + The ok parameter is a bit complicated as it has two effects. If there is a selection, the search begin at either end, depending on the direction setting and ok, with ok meaning that @@ -191,7 +191,7 @@ def search_reverse(prog, chars, col): This is done by searching forwards until there is no match. Prog: compiled re object with a search method returning a match. - Chars: line of text, without \n. + Chars: line of text, without \\n. Col: stop index for the search; the limit for match.end(). ''' m = prog.search(chars) @@ -229,6 +229,5 @@ def get_line_col(index): return line, col if __name__ == "__main__": - from test import support; support.use_resources = ['gui'] import unittest unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False) diff --git a/Lib/idlelib/StackViewer.py b/Lib/idlelib/StackViewer.py index 4ef2d31699..ccc755ce31 100644 --- a/Lib/idlelib/StackViewer.py +++ b/Lib/idlelib/StackViewer.py @@ -1,14 +1,16 @@ import os import sys import linecache +import re +import tkinter as tk from idlelib.TreeWidget import TreeNode, TreeItem, ScrolledCanvas from idlelib.ObjectBrowser import ObjectTreeItem, make_objecttreeitem +from idlelib.PyShell import PyShellFileList def StackBrowser(root, flist=None, tb=None, top=None): if top is None: - from tkinter import Toplevel - top = Toplevel(root) + top = tk.Toplevel(root) sc = ScrolledCanvas(top, bg="white", highlightthickness=0) sc.frame.pack(expand=1, fill="both") item = StackTreeItem(flist, tb) @@ -105,12 +107,9 @@ class VariablesTreeItem(ObjectTreeItem): def IsExpandable(self): return len(self.object) > 0 - def keys(self): - return list(self.object.keys()) - def GetSubList(self): sublist = [] - for key in self.keys(): + for key in self.object.keys(): try: value = self.object[key] except KeyError: @@ -120,3 +119,33 @@ class VariablesTreeItem(ObjectTreeItem): item = make_objecttreeitem(key + " =", value, setfunction) sublist.append(item) return sublist + + def keys(self): # unused, left for possible 3rd party use + return list(self.object.keys()) + +def _stack_viewer(parent): + root = tk.Tk() + root.title("Test StackViewer") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) + flist = PyShellFileList(root) + try: # to obtain a traceback object + intentional_name_error + except NameError: + exc_type, exc_value, exc_tb = sys.exc_info() + + # inject stack trace to sys + sys.last_type = exc_type + sys.last_value = exc_value + sys.last_traceback = exc_tb + + StackBrowser(root, flist=flist, top=root, tb=exc_tb) + + # restore sys to original state + del sys.last_type + del sys.last_value + del sys.last_traceback + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(_stack_viewer) diff --git a/Lib/idlelib/ToolTip.py b/Lib/idlelib/ToolTip.py index b178803b02..964107e117 100644 --- a/Lib/idlelib/ToolTip.py +++ b/Lib/idlelib/ToolTip.py @@ -76,14 +76,22 @@ class ListboxToolTip(ToolTipBase): for item in self.items: listbox.insert(END, item) -def main(): - # Test code +def _tooltip(parent): root = Tk() - b = Button(root, text="Hello", command=root.destroy) - b.pack() - root.update() - tip = ListboxToolTip(b, ["Hello", "world"]) + root.title("Test tooltip") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) + label = Label(root, text="Place your mouse over buttons") + label.pack() + button1 = Button(root, text="Button 1") + button2 = Button(root, text="Button 2") + button1.pack() + button2.pack() + ToolTip(button1, "This is tooltip text for button1.") + ListboxToolTip(button2, ["This is","multiple line", + "tooltip text","for button2"]) root.mainloop() if __name__ == '__main__': - main() + from idlelib.idle_test.htest import run + run(_tooltip) diff --git a/Lib/idlelib/TreeWidget.py b/Lib/idlelib/TreeWidget.py index 25bae48047..a19578fdcb 100644 --- a/Lib/idlelib/TreeWidget.py +++ b/Lib/idlelib/TreeWidget.py @@ -173,11 +173,12 @@ class TreeNode: def draw(self, x, y): # XXX This hard-codes too many geometry constants! + dy = 20 self.x, self.y = x, y self.drawicon() self.drawtext() if self.state != 'expanded': - return y+17 + return y + dy # draw children if not self.children: sublist = self.item._GetSubList() @@ -188,7 +189,7 @@ class TreeNode: child = self.__class__(self.canvas, self, item) self.children.append(child) cx = x+20 - cy = y+17 + cy = y + dy cylast = 0 for child in self.children: cylast = cy @@ -227,7 +228,7 @@ class TreeNode: def drawtext(self): textx = self.x+20-1 - texty = self.y-1 + texty = self.y-4 labeltext = self.item.GetLabelText() if labeltext: id = self.canvas.create_text(textx, texty, anchor="nw", @@ -244,11 +245,11 @@ class TreeNode: else: self.edit_finish() try: - label = self.label + self.label except AttributeError: # padding carefully selected (on Windows) to match Entry widget: self.label = Label(self.canvas, text=text, bd=0, padx=2, pady=2) - theme = idleConf.GetOption('main','Theme','name') + theme = idleConf.CurrentTheme() if self.selected: self.label.configure(idleConf.GetHighlight(theme, 'hilite')) else: @@ -381,7 +382,7 @@ class FileTreeItem(TreeItem): try: os.rename(self.path, newpath) self.path = newpath - except os.error: + except OSError: pass def GetIconName(self): @@ -394,7 +395,7 @@ class FileTreeItem(TreeItem): def GetSubList(self): try: names = os.listdir(self.path) - except os.error: + except OSError: return [] names.sort(key = os.path.normcase) sublist = [] @@ -448,29 +449,18 @@ class ScrolledCanvas: return "break" -# Testing functions - -def test(): - from idlelib import PyShell - root = Toplevel(PyShell.root) - root.configure(bd=0, bg="yellow") - root.focus_set() +def _tree_widget(parent): + root = Tk() + root.title("Test TreeWidget") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) sc = ScrolledCanvas(root, bg="white", highlightthickness=0, takefocus=1) - sc.frame.pack(expand=1, fill="both") - item = FileTreeItem("C:/windows/desktop") + sc.frame.pack(expand=1, fill="both", side=LEFT) + item = FileTreeItem(os.getcwd()) node = TreeNode(sc.canvas, None, item) node.expand() - -def test2(): - # test w/o scrolling canvas - root = Tk() - root.configure(bd=0) - canvas = Canvas(root, bg="white", highlightthickness=0) - canvas.pack(expand=1, fill="both") - item = FileTreeItem(os.curdir) - node = TreeNode(canvas, None, item) - node.update() - canvas.focus_set() + root.mainloop() if __name__ == '__main__': - test() + from idlelib.idle_test.htest import run + run(_tree_widget) diff --git a/Lib/idlelib/UndoDelegator.py b/Lib/idlelib/UndoDelegator.py index d2ef638ad2..04c1cf5a27 100644 --- a/Lib/idlelib/UndoDelegator.py +++ b/Lib/idlelib/UndoDelegator.py @@ -336,17 +336,30 @@ class CommandSequence(Command): self.depth = self.depth + incr return self.depth -def main(): +def _undo_delegator(parent): from idlelib.Percolator import Percolator root = Tk() - root.wm_protocol("WM_DELETE_WINDOW", root.quit) - text = Text() + root.title("Test UndoDelegator") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) + + text = Text(root) + text.config(height=10) text.pack() text.focus_set() p = Percolator(text) d = UndoDelegator() p.insertfilter(d) + + undo = Button(root, text="Undo", command=lambda:d.undo_event(None)) + undo.pack(side='left') + redo = Button(root, text="Redo", command=lambda:d.redo_event(None)) + redo.pack(side='left') + dump = Button(root, text="Dump", command=lambda:d.dump_event(None)) + dump.pack(side='left') + root.mainloop() if __name__ == "__main__": - main() + from idlelib.idle_test.htest import run + run(_undo_delegator) diff --git a/Lib/idlelib/WidgetRedirector.py b/Lib/idlelib/WidgetRedirector.py index ba5251ff71..b3d7bfa3c4 100644 --- a/Lib/idlelib/WidgetRedirector.py +++ b/Lib/idlelib/WidgetRedirector.py @@ -1,29 +1,40 @@ -from tkinter import * +from tkinter import TclError class WidgetRedirector: - """Support for redirecting arbitrary widget subcommands. - Some Tk operations don't normally pass through Tkinter. For example, if a + Some Tk operations don't normally pass through tkinter. For example, if a character is inserted into a Text widget by pressing a key, a default Tk binding to the widget's 'insert' operation is activated, and the Tk library - processes the insert without calling back into Tkinter. + processes the insert without calling back into tkinter. - Although a binding to <Key> could be made via Tkinter, what we really want - to do is to hook the Tk 'insert' operation itself. + Although a binding to <Key> could be made via tkinter, what we really want + to do is to hook the Tk 'insert' operation itself. For one thing, we want + a text.insert call in idle code to have the same effect as a key press. When a widget is instantiated, a Tcl command is created whose name is the same as the pathname widget._w. This command is used to invoke the various widget operations, e.g. insert (for a Text widget). We are going to hook this command and provide a facility ('register') to intercept the widget - operation. - - In IDLE, the function being registered provides access to the top of a - Percolator chain. At the bottom of the chain is a call to the original - Tk widget operation. + operation. We will also intercept method calls on the tkinter class + instance that represents the tk widget. + In IDLE, WidgetRedirector is used in Percolator to intercept Text + commands. The function being registered provides access to the top + of a Percolator chain. At the bottom of the chain is a call to the + original Tk widget operation. """ def __init__(self, widget): + '''Initialize attributes and setup redirection. + + _operations: dict mapping operation name to new function. + widget: the widget whose tcl command is to be intercepted. + tk: widget.tk, a convenience attribute, probably not needed. + orig: new name of the original tcl command. + + Since renaming to orig fails with TclError when orig already + exists, only one WidgetDirector can exist for a given widget. + ''' self._operations = {} self.widget = widget # widget instance self.tk = tk = widget.tk # widget's root @@ -40,27 +51,45 @@ class WidgetRedirector: self.widget._w) def close(self): + "Unregister operations and revert redirection created by .__init__." for operation in list(self._operations): self.unregister(operation) - widget = self.widget; del self.widget - orig = self.orig; del self.orig + widget = self.widget tk = widget.tk w = widget._w + # Restore the original widget Tcl command. tk.deletecommand(w) - # restore the original widget Tcl command: - tk.call("rename", orig, w) + tk.call("rename", self.orig, w) + del self.widget, self.tk # Should not be needed + # if instance is deleted after close, as in Percolator. def register(self, operation, function): + '''Return OriginalCommand(operation) after registering function. + + Registration adds an operation: function pair to ._operations. + It also adds an widget function attribute that masks the tkinter + class instance method. Method masking operates independently + from command dispatch. + + If a second function is registered for the same operation, the + first function is replaced in both places. + ''' self._operations[operation] = function setattr(self.widget, operation, function) return OriginalCommand(self, operation) def unregister(self, operation): + '''Return the function for the operation, or None. + + Deleting the instance attribute unmasks the class attribute. + ''' if operation in self._operations: function = self._operations[operation] del self._operations[operation] - if hasattr(self.widget, operation): + try: delattr(self.widget, operation) + except AttributeError: + pass return function else: return None @@ -88,14 +117,29 @@ class WidgetRedirector: class OriginalCommand: + '''Callable for original tk command that has been redirected. + + Returned by .register; can be used in the function registered. + redir = WidgetRedirector(text) + def my_insert(*args): + print("insert", args) + original_insert(*args) + original_insert = redir.register("insert", my_insert) + ''' def __init__(self, redir, operation): + '''Create .tk_call and .orig_and_operation for .__call__ method. + + .redir and .operation store the input args for __repr__. + .tk and .orig copy attributes of .redir (probably not needed). + ''' self.redir = redir self.operation = operation - self.tk = redir.tk - self.orig = redir.orig - self.tk_call = self.tk.call - self.orig_and_operation = (self.orig, self.operation) + self.tk = redir.tk # redundant with self.redir + self.orig = redir.orig # redundant with self.redir + # These two could be deleted after checking recipient code. + self.tk_call = redir.tk.call + self.orig_and_operation = (redir.orig, operation) def __repr__(self): return "OriginalCommand(%r, %r)" % (self.redir, self.operation) @@ -104,23 +148,27 @@ class OriginalCommand: return self.tk_call(self.orig_and_operation + args) -def main(): +def _widget_redirector(parent): # htest # + from tkinter import Tk, Text + import re + root = Tk() - root.wm_protocol("WM_DELETE_WINDOW", root.quit) - text = Text() + root.title("Test WidgetRedirector") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) + text = Text(root) text.pack() text.focus_set() redir = WidgetRedirector(text) - global previous_tcl_fcn def my_insert(*args): print("insert", args) - previous_tcl_fcn(*args) - previous_tcl_fcn = redir.register("insert", my_insert) - root.mainloop() - redir.unregister("insert") # runs after first 'close window' - redir.close() + original_insert(*args) + original_insert = redir.register("insert", my_insert) root.mainloop() - root.destroy() if __name__ == "__main__": - main() + import unittest + unittest.main('idlelib.idle_test.test_widgetredir', + verbosity=2, exit=False) + from idlelib.idle_test.htest import run + run(_widget_redirector) diff --git a/Lib/idlelib/ZoomHeight.py b/Lib/idlelib/ZoomHeight.py index e8d1710751..a5d679e499 100644 --- a/Lib/idlelib/ZoomHeight.py +++ b/Lib/idlelib/ZoomHeight.py @@ -32,7 +32,7 @@ def zoom_height(top): newy = 0 newheight = newheight - 72 - elif macosxSupport.runningAsOSXApp(): + elif macosxSupport.isAquaTk(): # The '88' below is a magic number that avoids placing the bottom # of the window below the panel on my machine. I don't know how # to calculate the correct value for this with tkinter. diff --git a/Lib/idlelib/__init__.py b/Lib/idlelib/__init__.py index 7a83ddea76..711f61bb69 100644 --- a/Lib/idlelib/__init__.py +++ b/Lib/idlelib/__init__.py @@ -1 +1,8 @@ -# Dummy file to make this a package. +"""The idlelib package implements the Idle application. + +Idle includes an interactive shell and editor. +Use the files named idle.* to start Idle. + +The other files are private implementations. Their details are subject to +change. See PEP 434 for more. Import them at your own risk. +""" diff --git a/Lib/idlelib/__main__.py b/Lib/idlelib/__main__.py index 0666f2fd1b..2edf5f7dc1 100644 --- a/Lib/idlelib/__main__.py +++ b/Lib/idlelib/__main__.py @@ -3,7 +3,6 @@ IDLE main entry point Run IDLE as python -m idlelib """ - - import idlelib.PyShell idlelib.PyShell.main() +# This file does not work for 2.7; See issue 24212. diff --git a/Lib/idlelib/aboutDialog.py b/Lib/idlelib/aboutDialog.py index 7fe1ab81ee..d876a97115 100644 --- a/Lib/idlelib/aboutDialog.py +++ b/Lib/idlelib/aboutDialog.py @@ -2,21 +2,25 @@ """ -from tkinter import * import os - +from sys import version +from tkinter import * from idlelib import textView -from idlelib import idlever class AboutDialog(Toplevel): """Modal about dialog for idle """ - def __init__(self,parent,title): + def __init__(self, parent, title, _htest=False): + """ + _htest - bool, change box location when running htest + """ Toplevel.__init__(self, parent) self.configure(borderwidth=5) - self.geometry("+%d+%d" % (parent.winfo_rootx()+30, - parent.winfo_rooty()+30)) + # place dialog below parent if running htest + self.geometry("+%d+%d" % ( + parent.winfo_rootx()+30, + parent.winfo_rooty()+(30 if not _htest else 100))) self.bg = "#707070" self.fg = "#ffffff" self.CreateWidgets() @@ -32,6 +36,7 @@ class AboutDialog(Toplevel): self.wait_window() def CreateWidgets(self): + release = version[:version.index(' ')] frameMain = Frame(self, borderwidth=2, relief=SUNKEN) frameButtons = Frame(self) frameButtons.pack(side=BOTTOM, fill=X) @@ -57,14 +62,15 @@ class AboutDialog(Toplevel): justify=LEFT, fg=self.fg, bg=self.bg) labelEmail.grid(row=6, column=0, columnspan=2, sticky=W, padx=10, pady=0) - labelWWW = Label(frameBg, text='www: http://www.python.org/idle/', + labelWWW = Label(frameBg, text='https://docs.python.org/' + + version[:3] + '/library/idle.html', justify=LEFT, fg=self.fg, bg=self.bg) labelWWW.grid(row=7, column=0, columnspan=2, sticky=W, padx=10, pady=0) Frame(frameBg, borderwidth=1, relief=SUNKEN, height=2, bg=self.bg).grid(row=8, column=0, sticky=EW, columnspan=3, padx=5, pady=5) - labelPythonVer = Label(frameBg, text='Python version: ' + \ - sys.version.split()[0], fg=self.fg, bg=self.bg) + labelPythonVer = Label(frameBg, text='Python version: ' + + release, fg=self.fg, bg=self.bg) labelPythonVer.grid(row=9, column=0, sticky=W, padx=10, pady=0) tkVer = self.tk.call('info', 'patchlevel') labelTkVer = Label(frameBg, text='Tk version: '+ @@ -87,7 +93,7 @@ class AboutDialog(Toplevel): Frame(frameBg, borderwidth=1, relief=SUNKEN, height=2, bg=self.bg).grid(row=11, column=0, sticky=EW, columnspan=3, padx=5, pady=5) - idle_v = Label(frameBg, text='IDLE version: ' + idlever.IDLE_VERSION, + idle_v = Label(frameBg, text='IDLE version: ' + release, fg=self.fg, bg=self.bg) idle_v.grid(row=12, column=0, sticky=W, padx=10, pady=0) idle_button_f = Frame(frameBg, bg=self.bg) @@ -136,10 +142,5 @@ class AboutDialog(Toplevel): self.destroy() if __name__ == '__main__': - # test the dialog - root = Tk() - def run(): - from idlelib import aboutDialog - aboutDialog.AboutDialog(root, 'About') - Button(root, text='Dialog', command=run).pack() - root.mainloop() + from idlelib.idle_test.htest import run + run(AboutDialog) diff --git a/Lib/idlelib/config-extensions.def b/Lib/idlelib/config-extensions.def index 39e69ce20d..a24b8c9316 100644 --- a/Lib/idlelib/config-extensions.def +++ b/Lib/idlelib/config-extensions.def @@ -3,94 +3,97 @@ # IDLE reads several config files to determine user preferences. This # file is the default configuration file for IDLE extensions settings. # -# Each extension must have at least one section, named after the extension -# module. This section must contain an 'enable' item (=1 to enable the -# extension, =0 to disable it), it may contain 'enable_editor' or 'enable_shell' -# items, to apply it only to editor/shell windows, and may also contain any -# other general configuration items for the extension. +# Each extension must have at least one section, named after the +# extension module. This section must contain an 'enable' item (=True to +# enable the extension, =False to disable it), it may contain +# 'enable_editor' or 'enable_shell' items, to apply it only to editor ir +# shell windows, and may also contain any other general configuration +# items for the extension. Other True/False values will also be +# recognized as boolean by the Extension Configuration dialog. # -# Each extension must define at least one section named ExtensionName_bindings -# or ExtensionName_cfgBindings. If present, ExtensionName_bindings defines -# virtual event bindings for the extension that are not user re-configurable. -# If present, ExtensionName_cfgBindings defines virtual event bindings for the +# Each extension must define at least one section named +# ExtensionName_bindings or ExtensionName_cfgBindings. If present, +# ExtensionName_bindings defines virtual event bindings for the +# extension that are not user re-configurable. If present, +# ExtensionName_cfgBindings defines virtual event bindings for the # extension that may be sensibly re-configured. # -# If there are no keybindings for a menus' virtual events, include lines like -# <<toggle-code-context>>= (See [CodeContext], below.) +# If there are no keybindings for a menus' virtual events, include lines +# like <<toggle-code-context>>= (See [CodeContext], below.) # -# Currently it is necessary to manually modify this file to change extension -# key bindings and default values. To customize, create +# Currently it is necessary to manually modify this file to change +# extension key bindings and default values. To customize, create # ~/.idlerc/config-extensions.cfg and append the appropriate customized # section(s). Those sections will override the defaults in this file. # -# Note: If a keybinding is already in use when the extension is -# loaded, the extension's virtual event's keybinding will be set to ''. +# Note: If a keybinding is already in use when the extension is loaded, +# the extension's virtual event's keybinding will be set to ''. # # See config-keys.def for notes on specifying keys and extend.txt for # information on creating IDLE extensions. -[FormatParagraph] -enable=1 -[FormatParagraph_cfgBindings] -format-paragraph=<Alt-Key-q> +[AutoComplete] +enable=True +popupwait=2000 +[AutoComplete_cfgBindings] +force-open-completions=<Control-Key-space> +[AutoComplete_bindings] +autocomplete=<Key-Tab> +try-open-completions=<KeyRelease-period> <KeyRelease-slash> <KeyRelease-backslash> [AutoExpand] -enable=1 +enable=True [AutoExpand_cfgBindings] expand-word=<Alt-Key-slash> -[ZoomHeight] -enable=1 -[ZoomHeight_cfgBindings] -zoom-height=<Alt-Key-2> - -[ScriptBinding] -enable=1 -enable_shell=0 -enable_editor=1 -[ScriptBinding_cfgBindings] -run-module=<Key-F5> -check-module=<Alt-Key-x> - [CallTips] -enable=1 +enable=True [CallTips_cfgBindings] force-open-calltip=<Control-Key-backslash> [CallTips_bindings] try-open-calltip=<KeyRelease-parenleft> refresh-calltip=<KeyRelease-parenright> <KeyRelease-0> +[CodeContext] +enable=True +enable_shell=False +numlines=3 +visible=False +bgcolor=LightGray +fgcolor=Black +[CodeContext_bindings] +toggle-code-context= + +[FormatParagraph] +enable=True +max-width=72 +[FormatParagraph_cfgBindings] +format-paragraph=<Alt-Key-q> + [ParenMatch] -enable=1 +enable=True style= expression flash-delay= 500 -bell= 1 +bell=True [ParenMatch_cfgBindings] flash-paren=<Control-Key-0> [ParenMatch_bindings] paren-closed=<KeyRelease-parenright> <KeyRelease-bracketright> <KeyRelease-braceright> -[AutoComplete] -enable=1 -popupwait=2000 -[AutoComplete_cfgBindings] -force-open-completions=<Control-Key-space> -[AutoComplete_bindings] -autocomplete=<Key-Tab> -try-open-completions=<KeyRelease-period> <KeyRelease-slash> <KeyRelease-backslash> - -[CodeContext] -enable=1 -enable_shell=0 -numlines=3 -visible=0 -bgcolor=LightGray -fgcolor=Black -[CodeContext_bindings] -toggle-code-context= - [RstripExtension] -enable=1 -enable_shell=0 -enable_editor=1 +enable=True +enable_shell=False +enable_editor=True +[ScriptBinding] +enable=True +enable_shell=False +enable_editor=True +[ScriptBinding_cfgBindings] +run-module=<Key-F5> +check-module=<Alt-Key-x> + +[ZoomHeight] +enable=True +[ZoomHeight_cfgBindings] +zoom-height=<Alt-Key-2> diff --git a/Lib/idlelib/config-highlight.def b/Lib/idlelib/config-highlight.def index 7d20f78240..4146e28c4e 100644 --- a/Lib/idlelib/config-highlight.def +++ b/Lib/idlelib/config-highlight.def @@ -62,3 +62,32 @@ stderr-foreground= red stderr-background= #ffffff console-foreground= #770000 console-background= #ffffff + +[IDLE Dark] +comment-foreground = #dd0000 +console-foreground = #ff4d4d +error-foreground = #FFFFFF +hilite-background = #7e7e7e +string-foreground = #02ff02 +stderr-background = #002240 +stderr-foreground = #ffb3b3 +console-background = #002240 +hit-background = #fbfbfb +string-background = #002240 +normal-background = #002240 +hilite-foreground = #FFFFFF +keyword-foreground = #ff8000 +error-background = #c86464 +keyword-background = #002240 +builtin-background = #002240 +break-background = #808000 +builtin-foreground = #ff00ff +definition-foreground = #5e5eff +stdout-foreground = #c2d1fa +definition-background = #002240 +normal-foreground = #FFFFFF +cursor-foreground = #ffffff +stdout-background = #002240 +hit-foreground = #002240 +comment-background = #002240 +break-foreground = #FFFFFF diff --git a/Lib/idlelib/config-keys.def b/Lib/idlelib/config-keys.def index fdc35ba7b5..3bfcb69015 100644 --- a/Lib/idlelib/config-keys.def +++ b/Lib/idlelib/config-keys.def @@ -13,37 +13,37 @@ cut=<Control-Key-x> <Control-Key-X> paste=<Control-Key-v> <Control-Key-V> beginning-of-line= <Key-Home> center-insert=<Control-Key-l> <Control-Key-L> -close-all-windows=<Control-Key-q> +close-all-windows=<Control-Key-q> <Control-Key-Q> close-window=<Alt-Key-F4> <Meta-Key-F4> do-nothing=<Control-Key-F12> end-of-file=<Control-Key-d> <Control-Key-D> python-docs=<Key-F1> python-context-help=<Shift-Key-F1> -history-next=<Alt-Key-n> <Meta-Key-n> -history-previous=<Alt-Key-p> <Meta-Key-p> +history-next=<Alt-Key-n> <Meta-Key-n> <Alt-Key-N> <Meta-Key-N> +history-previous=<Alt-Key-p> <Meta-Key-p> <Alt-Key-P> <Meta-Key-P> interrupt-execution=<Control-Key-c> <Control-Key-C> view-restart=<Key-F6> restart-shell=<Control-Key-F6> -open-class-browser=<Alt-Key-c> <Meta-Key-c> <Alt-Key-C> -open-module=<Alt-Key-m> <Meta-Key-m> <Alt-Key-M> +open-class-browser=<Alt-Key-c> <Meta-Key-c> <Alt-Key-C> <Meta-Key-C> +open-module=<Alt-Key-m> <Meta-Key-m> <Alt-Key-M> <Meta-Key-M> open-new-window=<Control-Key-n> <Control-Key-N> open-window-from-file=<Control-Key-o> <Control-Key-O> plain-newline-and-indent=<Control-Key-j> <Control-Key-J> print-window=<Control-Key-p> <Control-Key-P> -redo=<Control-Shift-Key-Z> +redo=<Control-Shift-Key-Z> <Control-Shift-Key-z> remove-selection=<Key-Escape> -save-copy-of-window-as-file=<Alt-Shift-Key-S> -save-window-as-file=<Control-Shift-Key-S> -save-window=<Control-Key-s> -select-all=<Control-Key-a> +save-copy-of-window-as-file=<Alt-Shift-Key-S> <Alt-Shift-Key-s> +save-window-as-file=<Control-Shift-Key-S> <Control-Shift-Key-s> +save-window=<Control-Key-s> <Control-Key-S> +select-all=<Control-Key-a> <Control-Key-A> toggle-auto-coloring=<Control-Key-slash> undo=<Control-Key-z> <Control-Key-Z> find=<Control-Key-f> <Control-Key-F> -find-again=<Control-Key-g> <Key-F3> +find-again=<Control-Key-g> <Key-F3> <Control-Key-G> find-in-files=<Alt-Key-F3> <Meta-Key-F3> find-selection=<Control-Key-F3> replace=<Control-Key-h> <Control-Key-H> -goto-line=<Alt-Key-g> <Meta-Key-g> +goto-line=<Alt-Key-g> <Meta-Key-g> <Alt-Key-G> <Meta-Key-G> smart-backspace=<Key-BackSpace> newline-and-indent=<Key-Return> <Key-KP_Enter> smart-indent=<Key-Tab> @@ -53,8 +53,8 @@ comment-region=<Alt-Key-3> <Meta-Key-3> uncomment-region=<Alt-Key-4> <Meta-Key-4> tabify-region=<Alt-Key-5> <Meta-Key-5> untabify-region=<Alt-Key-6> <Meta-Key-6> -toggle-tabs=<Alt-Key-t> <Meta-Key-t> <Alt-Key-T> -change-indentwidth=<Alt-Key-u> <Meta-Key-u> <Alt-Key-U> +toggle-tabs=<Alt-Key-t> <Meta-Key-t> <Alt-Key-T> <Meta-Key-T> +change-indentwidth=<Alt-Key-u> <Meta-Key-u> <Alt-Key-U> <Meta-Key-U> del-word-left=<Control-Key-BackSpace> del-word-right=<Control-Key-Delete> diff --git a/Lib/idlelib/config-main.def b/Lib/idlelib/config-main.def index 9546e2bf12..8ebbc1b4c2 100644 --- a/Lib/idlelib/config-main.def +++ b/Lib/idlelib/config-main.def @@ -53,14 +53,11 @@ delete-exitfunc= 1 [EditorWindow] width= 80 height= 40 -font= courier +font= TkFixedFont font-size= 10 font-bold= 0 encoding= none -[FormatParagraph] -paragraph=70 - [Indent] use-spaces= 1 num-spaces= 4 @@ -68,6 +65,8 @@ num-spaces= 4 [Theme] default= 1 name= IDLE Classic +name2= +# name2 set in user config-main.cfg for themes added after 2015 Oct 1 [Keys] default= 1 diff --git a/Lib/idlelib/configDialog.py b/Lib/idlelib/configDialog.py index efe5c4326c..9b16459d54 100644 --- a/Lib/idlelib/configDialog.py +++ b/Lib/idlelib/configDialog.py @@ -13,534 +13,576 @@ from tkinter import * import tkinter.messagebox as tkMessageBox import tkinter.colorchooser as tkColorChooser import tkinter.font as tkFont -import copy from idlelib.configHandler import idleConf from idlelib.dynOptionMenuWidget import DynOptionMenu -from idlelib.tabbedpages import TabbedPageSet from idlelib.keybindingDialog import GetKeysDialog from idlelib.configSectionNameDialog import GetCfgSectionNameDialog from idlelib.configHelpSourceEdit import GetHelpSourceDialog +from idlelib.tabbedpages import TabbedPageSet +from idlelib.textView import view_text from idlelib import macosxSupport class ConfigDialog(Toplevel): - def __init__(self,parent,title): + def __init__(self, parent, title='', _htest=False, _utest=False): + """ + _htest - bool, change box location when running htest + _utest - bool, don't wait_window when running unittest + """ Toplevel.__init__(self, parent) + self.parent = parent + if _htest: + parent.instance_dict = {} self.wm_withdraw() self.configure(borderwidth=5) - self.title('IDLE Preferences') - self.geometry("+%d+%d" % (parent.winfo_rootx()+20, - parent.winfo_rooty()+30)) + self.title(title or 'IDLE Preferences') + self.geometry( + "+%d+%d" % (parent.winfo_rootx() + 20, + parent.winfo_rooty() + (30 if not _htest else 150))) #Theme Elements. Each theme element key is its display name. #The first value of the tuple is the sample area tag name. #The second value is the display name list sort index. - self.themeElements={'Normal Text':('normal','00'), - 'Python Keywords':('keyword','01'), - 'Python Definitions':('definition','02'), - 'Python Builtins':('builtin', '03'), - 'Python Comments':('comment','04'), - 'Python Strings':('string','05'), - 'Selected Text':('hilite','06'), - 'Found Text':('hit','07'), - 'Cursor':('cursor','08'), - 'Error Text':('error','09'), - 'Shell Normal Text':('console','10'), - 'Shell Stdout Text':('stdout','11'), - 'Shell Stderr Text':('stderr','12'), + self.themeElements={ + 'Normal Text': ('normal', '00'), + 'Python Keywords': ('keyword', '01'), + 'Python Definitions': ('definition', '02'), + 'Python Builtins': ('builtin', '03'), + 'Python Comments': ('comment', '04'), + 'Python Strings': ('string', '05'), + 'Selected Text': ('hilite', '06'), + 'Found Text': ('hit', '07'), + 'Cursor': ('cursor', '08'), + 'Editor Breakpoint': ('break', '09'), + 'Shell Normal Text': ('console', '10'), + 'Shell Error Text': ('error', '11'), + 'Shell Stdout Text': ('stdout', '12'), + 'Shell Stderr Text': ('stderr', '13'), } self.ResetChangedItems() #load initial values in changed items dict self.CreateWidgets() - self.resizable(height=FALSE,width=FALSE) + self.resizable(height=FALSE, width=FALSE) self.transient(parent) self.grab_set() self.protocol("WM_DELETE_WINDOW", self.Cancel) - self.parent = parent self.tabPages.focus_set() #key bindings for this dialog - #self.bind('<Escape>',self.Cancel) #dismiss dialog, no save - #self.bind('<Alt-a>',self.Apply) #apply changes, save - #self.bind('<F1>',self.Help) #context help + #self.bind('<Escape>', self.Cancel) #dismiss dialog, no save + #self.bind('<Alt-a>', self.Apply) #apply changes, save + #self.bind('<F1>', self.Help) #context help self.LoadConfigs() self.AttachVarCallbacks() #avoid callbacks during LoadConfigs - self.wm_deiconify() - self.wait_window() + if not _utest: + self.wm_deiconify() + self.wait_window() def CreateWidgets(self): self.tabPages = TabbedPageSet(self, - page_names=['Fonts/Tabs','Highlighting','Keys','General']) - frameActionButtons = Frame(self,pady=2) - #action buttons - - if macosxSupport.runningAsOSXApp(): - # Surpress the padx and pady arguments when - # running as IDLE.app, otherwise the text - # on these buttons will not be readable. - extraKwds={} - else: - extraKwds=dict(padx=6, pady=3) - -# Comment out button creation and packing until implement self.Help -## self.buttonHelp = Button(frameActionButtons,text='Help', -## command=self.Help,takefocus=FALSE, -## **extraKwds) - self.buttonOk = Button(frameActionButtons,text='Ok', - command=self.Ok,takefocus=FALSE, - **extraKwds) - self.buttonApply = Button(frameActionButtons,text='Apply', - command=self.Apply,takefocus=FALSE, - **extraKwds) - self.buttonCancel = Button(frameActionButtons,text='Cancel', - command=self.Cancel,takefocus=FALSE, - **extraKwds) + page_names=['Fonts/Tabs', 'Highlighting', 'Keys', 'General', + 'Extensions']) + self.tabPages.pack(side=TOP, expand=TRUE, fill=BOTH) self.CreatePageFontTab() self.CreatePageHighlight() self.CreatePageKeys() self.CreatePageGeneral() -## self.buttonHelp.pack(side=RIGHT,padx=5) - self.buttonOk.pack(side=LEFT,padx=5) - self.buttonApply.pack(side=LEFT,padx=5) - self.buttonCancel.pack(side=LEFT,padx=5) - frameActionButtons.pack(side=BOTTOM) - Frame(self, height=2, borderwidth=0).pack(side=BOTTOM) - self.tabPages.pack(side=TOP,expand=TRUE,fill=BOTH) + self.CreatePageExtensions() + self.create_action_buttons().pack(side=BOTTOM) + + def create_action_buttons(self): + if macosxSupport.isAquaTk(): + # Changing the default padding on OSX results in unreadable + # text in the buttons + paddingArgs = {} + else: + paddingArgs = {'padx':6, 'pady':3} + outer = Frame(self, pady=2) + buttons = Frame(outer, pady=2) + for txt, cmd in ( + ('Ok', self.Ok), + ('Apply', self.Apply), + ('Cancel', self.Cancel), + ('Help', self.Help)): + Button(buttons, text=txt, command=cmd, takefocus=FALSE, + **paddingArgs).pack(side=LEFT, padx=5) + # add space above buttons + Frame(outer, height=2, borderwidth=0).pack(side=TOP) + buttons.pack(side=BOTTOM) + return outer def CreatePageFontTab(self): - #tkVars - self.fontSize=StringVar(self) - self.fontBold=BooleanVar(self) - self.fontName=StringVar(self) - self.spaceNum=IntVar(self) - self.editFont=tkFont.Font(self,('courier',10,'normal')) + parent = self.parent + self.fontSize = StringVar(parent) + self.fontBold = BooleanVar(parent) + self.fontName = StringVar(parent) + self.spaceNum = IntVar(parent) + self.editFont = tkFont.Font(parent, ('courier', 10, 'normal')) + ##widget creation #body frame - frame=self.tabPages.pages['Fonts/Tabs'].frame + frame = self.tabPages.pages['Fonts/Tabs'].frame #body section frames - frameFont=LabelFrame(frame,borderwidth=2,relief=GROOVE, - text=' Base Editor Font ') - frameIndent=LabelFrame(frame,borderwidth=2,relief=GROOVE, - text=' Indentation Width ') + frameFont = LabelFrame( + frame, borderwidth=2, relief=GROOVE, text=' Base Editor Font ') + frameIndent = LabelFrame( + frame, borderwidth=2, relief=GROOVE, text=' Indentation Width ') #frameFont - frameFontName=Frame(frameFont) - frameFontParam=Frame(frameFont) - labelFontNameTitle=Label(frameFontName,justify=LEFT, - text='Font Face :') - self.listFontName=Listbox(frameFontName,height=5,takefocus=FALSE, - exportselection=FALSE) - self.listFontName.bind('<ButtonRelease-1>',self.OnListFontButtonRelease) - scrollFont=Scrollbar(frameFontName) + frameFontName = Frame(frameFont) + frameFontParam = Frame(frameFont) + labelFontNameTitle = Label( + frameFontName, justify=LEFT, text='Font Face :') + self.listFontName = Listbox( + frameFontName, height=5, takefocus=FALSE, exportselection=FALSE) + self.listFontName.bind( + '<ButtonRelease-1>', self.OnListFontButtonRelease) + scrollFont = Scrollbar(frameFontName) scrollFont.config(command=self.listFontName.yview) self.listFontName.config(yscrollcommand=scrollFont.set) - labelFontSizeTitle=Label(frameFontParam,text='Size :') - self.optMenuFontSize=DynOptionMenu(frameFontParam,self.fontSize,None, - command=self.SetFontSample) - checkFontBold=Checkbutton(frameFontParam,variable=self.fontBold, - onvalue=1,offvalue=0,text='Bold',command=self.SetFontSample) - frameFontSample=Frame(frameFont,relief=SOLID,borderwidth=1) - self.labelFontSample=Label(frameFontSample, - text='AaBbCcDdEe\nFfGgHhIiJjK\n1234567890\n#:+=(){}[]', - justify=LEFT,font=self.editFont) + labelFontSizeTitle = Label(frameFontParam, text='Size :') + self.optMenuFontSize = DynOptionMenu( + frameFontParam, self.fontSize, None, command=self.SetFontSample) + checkFontBold = Checkbutton( + frameFontParam, variable=self.fontBold, onvalue=1, + offvalue=0, text='Bold', command=self.SetFontSample) + frameFontSample = Frame(frameFont, relief=SOLID, borderwidth=1) + self.labelFontSample = Label( + frameFontSample, justify=LEFT, font=self.editFont, + text='AaBbCcDdEe\nFfGgHhIiJjK\n1234567890\n#:+=(){}[]') #frameIndent - frameIndentSize=Frame(frameIndent) - labelSpaceNumTitle=Label(frameIndentSize, justify=LEFT, - text='Python Standard: 4 Spaces!') - self.scaleSpaceNum=Scale(frameIndentSize, variable=self.spaceNum, - orient='horizontal', - tickinterval=2, from_=2, to=16) + frameIndentSize = Frame(frameIndent) + labelSpaceNumTitle = Label( + frameIndentSize, justify=LEFT, + text='Python Standard: 4 Spaces!') + self.scaleSpaceNum = Scale( + frameIndentSize, variable=self.spaceNum, + orient='horizontal', tickinterval=2, from_=2, to=16) + #widget packing #body - frameFont.pack(side=LEFT,padx=5,pady=5,expand=TRUE,fill=BOTH) - frameIndent.pack(side=LEFT,padx=5,pady=5,fill=Y) + frameFont.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) + frameIndent.pack(side=LEFT, padx=5, pady=5, fill=Y) #frameFont - frameFontName.pack(side=TOP,padx=5,pady=5,fill=X) - frameFontParam.pack(side=TOP,padx=5,pady=5,fill=X) - labelFontNameTitle.pack(side=TOP,anchor=W) - self.listFontName.pack(side=LEFT,expand=TRUE,fill=X) - scrollFont.pack(side=LEFT,fill=Y) - labelFontSizeTitle.pack(side=LEFT,anchor=W) - self.optMenuFontSize.pack(side=LEFT,anchor=W) - checkFontBold.pack(side=LEFT,anchor=W,padx=20) - frameFontSample.pack(side=TOP,padx=5,pady=5,expand=TRUE,fill=BOTH) - self.labelFontSample.pack(expand=TRUE,fill=BOTH) + frameFontName.pack(side=TOP, padx=5, pady=5, fill=X) + frameFontParam.pack(side=TOP, padx=5, pady=5, fill=X) + labelFontNameTitle.pack(side=TOP, anchor=W) + self.listFontName.pack(side=LEFT, expand=TRUE, fill=X) + scrollFont.pack(side=LEFT, fill=Y) + labelFontSizeTitle.pack(side=LEFT, anchor=W) + self.optMenuFontSize.pack(side=LEFT, anchor=W) + checkFontBold.pack(side=LEFT, anchor=W, padx=20) + frameFontSample.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) + self.labelFontSample.pack(expand=TRUE, fill=BOTH) #frameIndent - frameIndentSize.pack(side=TOP,fill=X) - labelSpaceNumTitle.pack(side=TOP,anchor=W,padx=5) - self.scaleSpaceNum.pack(side=TOP,padx=5,fill=X) + frameIndentSize.pack(side=TOP, fill=X) + labelSpaceNumTitle.pack(side=TOP, anchor=W, padx=5) + self.scaleSpaceNum.pack(side=TOP, padx=5, fill=X) return frame def CreatePageHighlight(self): - self.builtinTheme=StringVar(self) - self.customTheme=StringVar(self) - self.fgHilite=BooleanVar(self) - self.colour=StringVar(self) - self.fontName=StringVar(self) - self.themeIsBuiltin=BooleanVar(self) - self.highlightTarget=StringVar(self) + parent = self.parent + self.builtinTheme = StringVar(parent) + self.customTheme = StringVar(parent) + self.fgHilite = BooleanVar(parent) + self.colour = StringVar(parent) + self.fontName = StringVar(parent) + self.themeIsBuiltin = BooleanVar(parent) + self.highlightTarget = StringVar(parent) + ##widget creation #body frame - frame=self.tabPages.pages['Highlighting'].frame + frame = self.tabPages.pages['Highlighting'].frame #body section frames - frameCustom=LabelFrame(frame,borderwidth=2,relief=GROOVE, - text=' Custom Highlighting ') - frameTheme=LabelFrame(frame,borderwidth=2,relief=GROOVE, - text=' Highlighting Theme ') + frameCustom = LabelFrame(frame, borderwidth=2, relief=GROOVE, + text=' Custom Highlighting ') + frameTheme = LabelFrame(frame, borderwidth=2, relief=GROOVE, + text=' Highlighting Theme ') #frameCustom - self.textHighlightSample=Text(frameCustom,relief=SOLID,borderwidth=1, - font=('courier',12,''),cursor='hand2',width=21,height=11, - takefocus=FALSE,highlightthickness=0,wrap=NONE) + self.textHighlightSample=Text( + frameCustom, relief=SOLID, borderwidth=1, + font=('courier', 12, ''), cursor='hand2', width=21, height=11, + takefocus=FALSE, highlightthickness=0, wrap=NONE) text=self.textHighlightSample - text.bind('<Double-Button-1>',lambda e: 'break') - text.bind('<B1-Motion>',lambda e: 'break') - textAndTags=(('#you can click here','comment'),('\n','normal'), - ('#to choose items','comment'),('\n','normal'),('def','keyword'), - (' ','normal'),('func','definition'),('(param):','normal'), - ('\n ','normal'),('"""string"""','string'),('\n var0 = ','normal'), - ("'string'",'string'),('\n var1 = ','normal'),("'selected'",'hilite'), - ('\n var2 = ','normal'),("'found'",'hit'), - ('\n var3 = ','normal'),('list', 'builtin'), ('(','normal'), - ('None', 'keyword'),(')\n\n','normal'), - (' error ','error'),(' ','normal'),('cursor |','cursor'), - ('\n ','normal'),('shell','console'),(' ','normal'),('stdout','stdout'), - (' ','normal'),('stderr','stderr'),('\n','normal')) + text.bind('<Double-Button-1>', lambda e: 'break') + text.bind('<B1-Motion>', lambda e: 'break') + textAndTags=( + ('#you can click here', 'comment'), ('\n', 'normal'), + ('#to choose items', 'comment'), ('\n', 'normal'), + ('def', 'keyword'), (' ', 'normal'), + ('func', 'definition'), ('(param):\n ', 'normal'), + ('"""string"""', 'string'), ('\n var0 = ', 'normal'), + ("'string'", 'string'), ('\n var1 = ', 'normal'), + ("'selected'", 'hilite'), ('\n var2 = ', 'normal'), + ("'found'", 'hit'), ('\n var3 = ', 'normal'), + ('list', 'builtin'), ('(', 'normal'), + ('None', 'keyword'), (')\n', 'normal'), + (' breakpoint("line")', 'break'), ('\n\n', 'normal'), + (' error ', 'error'), (' ', 'normal'), + ('cursor |', 'cursor'), ('\n ', 'normal'), + ('shell', 'console'), (' ', 'normal'), + ('stdout', 'stdout'), (' ', 'normal'), + ('stderr', 'stderr'), ('\n', 'normal')) for txTa in textAndTags: - text.insert(END,txTa[0],txTa[1]) + text.insert(END, txTa[0], txTa[1]) for element in self.themeElements: - text.tag_bind(self.themeElements[element][0],'<ButtonPress-1>', - lambda event,elem=element: event.widget.winfo_toplevel() - .highlightTarget.set(elem)) + def tem(event, elem=element): + event.widget.winfo_toplevel().highlightTarget.set(elem) + text.tag_bind( + self.themeElements[element][0], '<ButtonPress-1>', tem) text.config(state=DISABLED) - self.frameColourSet=Frame(frameCustom,relief=SOLID,borderwidth=1) - frameFgBg=Frame(frameCustom) - buttonSetColour=Button(self.frameColourSet,text='Choose Colour for :', - command=self.GetColour,highlightthickness=0) - self.optMenuHighlightTarget=DynOptionMenu(self.frameColourSet, - self.highlightTarget,None,highlightthickness=0)#,command=self.SetHighlightTargetBinding - self.radioFg=Radiobutton(frameFgBg,variable=self.fgHilite, - value=1,text='Foreground',command=self.SetColourSampleBinding) - self.radioBg=Radiobutton(frameFgBg,variable=self.fgHilite, - value=0,text='Background',command=self.SetColourSampleBinding) + self.frameColourSet = Frame(frameCustom, relief=SOLID, borderwidth=1) + frameFgBg = Frame(frameCustom) + buttonSetColour = Button( + self.frameColourSet, text='Choose Colour for :', + command=self.GetColour, highlightthickness=0) + self.optMenuHighlightTarget = DynOptionMenu( + self.frameColourSet, self.highlightTarget, None, + highlightthickness=0) #, command=self.SetHighlightTargetBinding + self.radioFg = Radiobutton( + frameFgBg, variable=self.fgHilite, value=1, + text='Foreground', command=self.SetColourSampleBinding) + self.radioBg=Radiobutton( + frameFgBg, variable=self.fgHilite, value=0, + text='Background', command=self.SetColourSampleBinding) self.fgHilite.set(1) - buttonSaveCustomTheme=Button(frameCustom, - text='Save as New Custom Theme',command=self.SaveAsNewTheme) + buttonSaveCustomTheme = Button( + frameCustom, text='Save as New Custom Theme', + command=self.SaveAsNewTheme) #frameTheme - labelTypeTitle=Label(frameTheme,text='Select : ') - self.radioThemeBuiltin=Radiobutton(frameTheme,variable=self.themeIsBuiltin, - value=1,command=self.SetThemeType,text='a Built-in Theme') - self.radioThemeCustom=Radiobutton(frameTheme,variable=self.themeIsBuiltin, - value=0,command=self.SetThemeType,text='a Custom Theme') - self.optMenuThemeBuiltin=DynOptionMenu(frameTheme, - self.builtinTheme,None,command=None) - self.optMenuThemeCustom=DynOptionMenu(frameTheme, - self.customTheme,None,command=None) - self.buttonDeleteCustomTheme=Button(frameTheme,text='Delete Custom Theme', + labelTypeTitle = Label(frameTheme, text='Select : ') + self.radioThemeBuiltin = Radiobutton( + frameTheme, variable=self.themeIsBuiltin, value=1, + command=self.SetThemeType, text='a Built-in Theme') + self.radioThemeCustom = Radiobutton( + frameTheme, variable=self.themeIsBuiltin, value=0, + command=self.SetThemeType, text='a Custom Theme') + self.optMenuThemeBuiltin = DynOptionMenu( + frameTheme, self.builtinTheme, None, command=None) + self.optMenuThemeCustom=DynOptionMenu( + frameTheme, self.customTheme, None, command=None) + self.buttonDeleteCustomTheme=Button( + frameTheme, text='Delete Custom Theme', command=self.DeleteCustomTheme) + self.new_custom_theme = Label(frameTheme, bd=2) + ##widget packing #body - frameCustom.pack(side=LEFT,padx=5,pady=5,expand=TRUE,fill=BOTH) - frameTheme.pack(side=LEFT,padx=5,pady=5,fill=Y) + frameCustom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) + frameTheme.pack(side=LEFT, padx=5, pady=5, fill=Y) #frameCustom - self.frameColourSet.pack(side=TOP,padx=5,pady=5,expand=TRUE,fill=X) - frameFgBg.pack(side=TOP,padx=5,pady=0) - self.textHighlightSample.pack(side=TOP,padx=5,pady=5,expand=TRUE, - fill=BOTH) - buttonSetColour.pack(side=TOP,expand=TRUE,fill=X,padx=8,pady=4) - self.optMenuHighlightTarget.pack(side=TOP,expand=TRUE,fill=X,padx=8,pady=3) - self.radioFg.pack(side=LEFT,anchor=E) - self.radioBg.pack(side=RIGHT,anchor=W) - buttonSaveCustomTheme.pack(side=BOTTOM,fill=X,padx=5,pady=5) + self.frameColourSet.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X) + frameFgBg.pack(side=TOP, padx=5, pady=0) + self.textHighlightSample.pack( + side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) + buttonSetColour.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4) + self.optMenuHighlightTarget.pack( + side=TOP, expand=TRUE, fill=X, padx=8, pady=3) + self.radioFg.pack(side=LEFT, anchor=E) + self.radioBg.pack(side=RIGHT, anchor=W) + buttonSaveCustomTheme.pack(side=BOTTOM, fill=X, padx=5, pady=5) #frameTheme - labelTypeTitle.pack(side=TOP,anchor=W,padx=5,pady=5) - self.radioThemeBuiltin.pack(side=TOP,anchor=W,padx=5) - self.radioThemeCustom.pack(side=TOP,anchor=W,padx=5,pady=2) - self.optMenuThemeBuiltin.pack(side=TOP,fill=X,padx=5,pady=5) - self.optMenuThemeCustom.pack(side=TOP,fill=X,anchor=W,padx=5,pady=5) - self.buttonDeleteCustomTheme.pack(side=TOP,fill=X,padx=5,pady=5) + labelTypeTitle.pack(side=TOP, anchor=W, padx=5, pady=5) + self.radioThemeBuiltin.pack(side=TOP, anchor=W, padx=5) + self.radioThemeCustom.pack(side=TOP, anchor=W, padx=5, pady=2) + self.optMenuThemeBuiltin.pack(side=TOP, fill=X, padx=5, pady=5) + self.optMenuThemeCustom.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5) + self.buttonDeleteCustomTheme.pack(side=TOP, fill=X, padx=5, pady=5) + self.new_custom_theme.pack(side=TOP, fill=X, pady=5) return frame def CreatePageKeys(self): - #tkVars - self.bindingTarget=StringVar(self) - self.builtinKeys=StringVar(self) - self.customKeys=StringVar(self) - self.keysAreBuiltin=BooleanVar(self) - self.keyBinding=StringVar(self) + parent = self.parent + self.bindingTarget = StringVar(parent) + self.builtinKeys = StringVar(parent) + self.customKeys = StringVar(parent) + self.keysAreBuiltin = BooleanVar(parent) + self.keyBinding = StringVar(parent) + ##widget creation #body frame - frame=self.tabPages.pages['Keys'].frame + frame = self.tabPages.pages['Keys'].frame #body section frames - frameCustom=LabelFrame(frame,borderwidth=2,relief=GROOVE, - text=' Custom Key Bindings ') - frameKeySets=LabelFrame(frame,borderwidth=2,relief=GROOVE, - text=' Key Set ') + frameCustom = LabelFrame( + frame, borderwidth=2, relief=GROOVE, + text=' Custom Key Bindings ') + frameKeySets = LabelFrame( + frame, borderwidth=2, relief=GROOVE, text=' Key Set ') #frameCustom - frameTarget=Frame(frameCustom) - labelTargetTitle=Label(frameTarget,text='Action - Key(s)') - scrollTargetY=Scrollbar(frameTarget) - scrollTargetX=Scrollbar(frameTarget,orient=HORIZONTAL) - self.listBindings=Listbox(frameTarget,takefocus=FALSE, - exportselection=FALSE) - self.listBindings.bind('<ButtonRelease-1>',self.KeyBindingSelected) + frameTarget = Frame(frameCustom) + labelTargetTitle = Label(frameTarget, text='Action - Key(s)') + scrollTargetY = Scrollbar(frameTarget) + scrollTargetX = Scrollbar(frameTarget, orient=HORIZONTAL) + self.listBindings = Listbox( + frameTarget, takefocus=FALSE, exportselection=FALSE) + self.listBindings.bind('<ButtonRelease-1>', self.KeyBindingSelected) scrollTargetY.config(command=self.listBindings.yview) scrollTargetX.config(command=self.listBindings.xview) self.listBindings.config(yscrollcommand=scrollTargetY.set) self.listBindings.config(xscrollcommand=scrollTargetX.set) - self.buttonNewKeys=Button(frameCustom,text='Get New Keys for Selection', - command=self.GetNewKeys,state=DISABLED) + self.buttonNewKeys = Button( + frameCustom, text='Get New Keys for Selection', + command=self.GetNewKeys, state=DISABLED) #frameKeySets frames = [Frame(frameKeySets, padx=2, pady=2, borderwidth=0) for i in range(2)] - self.radioKeysBuiltin=Radiobutton(frames[0],variable=self.keysAreBuiltin, - value=1,command=self.SetKeysType,text='Use a Built-in Key Set') - self.radioKeysCustom=Radiobutton(frames[0],variable=self.keysAreBuiltin, - value=0,command=self.SetKeysType,text='Use a Custom Key Set') - self.optMenuKeysBuiltin=DynOptionMenu(frames[0], - self.builtinKeys,None,command=None) - self.optMenuKeysCustom=DynOptionMenu(frames[0], - self.customKeys,None,command=None) - self.buttonDeleteCustomKeys=Button(frames[1],text='Delete Custom Key Set', + self.radioKeysBuiltin = Radiobutton( + frames[0], variable=self.keysAreBuiltin, value=1, + command=self.SetKeysType, text='Use a Built-in Key Set') + self.radioKeysCustom = Radiobutton( + frames[0], variable=self.keysAreBuiltin, value=0, + command=self.SetKeysType, text='Use a Custom Key Set') + self.optMenuKeysBuiltin = DynOptionMenu( + frames[0], self.builtinKeys, None, command=None) + self.optMenuKeysCustom = DynOptionMenu( + frames[0], self.customKeys, None, command=None) + self.buttonDeleteCustomKeys = Button( + frames[1], text='Delete Custom Key Set', command=self.DeleteCustomKeys) - buttonSaveCustomKeys=Button(frames[1], - text='Save as New Custom Key Set',command=self.SaveAsNewKeySet) + buttonSaveCustomKeys = Button( + frames[1], text='Save as New Custom Key Set', + command=self.SaveAsNewKeySet) + ##widget packing #body - frameCustom.pack(side=BOTTOM,padx=5,pady=5,expand=TRUE,fill=BOTH) - frameKeySets.pack(side=BOTTOM,padx=5,pady=5,fill=BOTH) + frameCustom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH) + frameKeySets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH) #frameCustom - self.buttonNewKeys.pack(side=BOTTOM,fill=X,padx=5,pady=5) - frameTarget.pack(side=LEFT,padx=5,pady=5,expand=TRUE,fill=BOTH) + self.buttonNewKeys.pack(side=BOTTOM, fill=X, padx=5, pady=5) + frameTarget.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) #frame target - frameTarget.columnconfigure(0,weight=1) - frameTarget.rowconfigure(1,weight=1) - labelTargetTitle.grid(row=0,column=0,columnspan=2,sticky=W) - self.listBindings.grid(row=1,column=0,sticky=NSEW) - scrollTargetY.grid(row=1,column=1,sticky=NS) - scrollTargetX.grid(row=2,column=0,sticky=EW) + frameTarget.columnconfigure(0, weight=1) + frameTarget.rowconfigure(1, weight=1) + labelTargetTitle.grid(row=0, column=0, columnspan=2, sticky=W) + self.listBindings.grid(row=1, column=0, sticky=NSEW) + scrollTargetY.grid(row=1, column=1, sticky=NS) + scrollTargetX.grid(row=2, column=0, sticky=EW) #frameKeySets self.radioKeysBuiltin.grid(row=0, column=0, sticky=W+NS) self.radioKeysCustom.grid(row=1, column=0, sticky=W+NS) self.optMenuKeysBuiltin.grid(row=0, column=1, sticky=NSEW) self.optMenuKeysCustom.grid(row=1, column=1, sticky=NSEW) - self.buttonDeleteCustomKeys.pack(side=LEFT,fill=X,expand=True,padx=2) - buttonSaveCustomKeys.pack(side=LEFT,fill=X,expand=True,padx=2) + self.buttonDeleteCustomKeys.pack(side=LEFT, fill=X, expand=True, padx=2) + buttonSaveCustomKeys.pack(side=LEFT, fill=X, expand=True, padx=2) frames[0].pack(side=TOP, fill=BOTH, expand=True) frames[1].pack(side=TOP, fill=X, expand=True, pady=2) return frame def CreatePageGeneral(self): - #tkVars - self.winWidth=StringVar(self) - self.winHeight=StringVar(self) - self.paraWidth=StringVar(self) - self.startupEdit=IntVar(self) - self.autoSave=IntVar(self) - self.encoding=StringVar(self) - self.userHelpBrowser=BooleanVar(self) - self.helpBrowser=StringVar(self) + parent = self.parent + self.winWidth = StringVar(parent) + self.winHeight = StringVar(parent) + self.startupEdit = IntVar(parent) + self.autoSave = IntVar(parent) + self.encoding = StringVar(parent) + self.userHelpBrowser = BooleanVar(parent) + self.helpBrowser = StringVar(parent) + #widget creation #body - frame=self.tabPages.pages['General'].frame + frame = self.tabPages.pages['General'].frame #body section frames - frameRun=LabelFrame(frame,borderwidth=2,relief=GROOVE, - text=' Startup Preferences ') - frameSave=LabelFrame(frame,borderwidth=2,relief=GROOVE, - text=' Autosave Preferences ') - frameWinSize=Frame(frame,borderwidth=2,relief=GROOVE) - frameParaSize=Frame(frame,borderwidth=2,relief=GROOVE) - frameHelp=LabelFrame(frame,borderwidth=2,relief=GROOVE, - text=' Additional Help Sources ') + frameRun = LabelFrame(frame, borderwidth=2, relief=GROOVE, + text=' Startup Preferences ') + frameSave = LabelFrame(frame, borderwidth=2, relief=GROOVE, + text=' Autosave Preferences ') + frameWinSize = Frame(frame, borderwidth=2, relief=GROOVE) + frameHelp = LabelFrame(frame, borderwidth=2, relief=GROOVE, + text=' Additional Help Sources ') #frameRun - labelRunChoiceTitle=Label(frameRun,text='At Startup') - radioStartupEdit=Radiobutton(frameRun,variable=self.startupEdit, - value=1,command=self.SetKeysType,text="Open Edit Window") - radioStartupShell=Radiobutton(frameRun,variable=self.startupEdit, - value=0,command=self.SetKeysType,text='Open Shell Window') + labelRunChoiceTitle = Label(frameRun, text='At Startup') + radioStartupEdit = Radiobutton( + frameRun, variable=self.startupEdit, value=1, + command=self.SetKeysType, text="Open Edit Window") + radioStartupShell = Radiobutton( + frameRun, variable=self.startupEdit, value=0, + command=self.SetKeysType, text='Open Shell Window') #frameSave - labelRunSaveTitle=Label(frameSave,text='At Start of Run (F5) ') - radioSaveAsk=Radiobutton(frameSave,variable=self.autoSave, - value=0,command=self.SetKeysType,text="Prompt to Save") - radioSaveAuto=Radiobutton(frameSave,variable=self.autoSave, - value=1,command=self.SetKeysType,text='No Prompt') + labelRunSaveTitle = Label(frameSave, text='At Start of Run (F5) ') + radioSaveAsk = Radiobutton( + frameSave, variable=self.autoSave, value=0, + command=self.SetKeysType, text="Prompt to Save") + radioSaveAuto = Radiobutton( + frameSave, variable=self.autoSave, value=1, + command=self.SetKeysType, text='No Prompt') #frameWinSize - labelWinSizeTitle=Label(frameWinSize,text='Initial Window Size'+ - ' (in characters)') - labelWinWidthTitle=Label(frameWinSize,text='Width') - entryWinWidth=Entry(frameWinSize,textvariable=self.winWidth, - width=3) - labelWinHeightTitle=Label(frameWinSize,text='Height') - entryWinHeight=Entry(frameWinSize,textvariable=self.winHeight, - width=3) - #paragraphFormatWidth - labelParaWidthTitle=Label(frameParaSize,text='Paragraph reformat'+ - ' width (in characters)') - entryParaWidth=Entry(frameParaSize,textvariable=self.paraWidth, - width=3) + labelWinSizeTitle = Label( + frameWinSize, text='Initial Window Size (in characters)') + labelWinWidthTitle = Label(frameWinSize, text='Width') + entryWinWidth = Entry( + frameWinSize, textvariable=self.winWidth, width=3) + labelWinHeightTitle = Label(frameWinSize, text='Height') + entryWinHeight = Entry( + frameWinSize, textvariable=self.winHeight, width=3) #frameHelp - frameHelpList=Frame(frameHelp) - frameHelpListButtons=Frame(frameHelpList) - scrollHelpList=Scrollbar(frameHelpList) - self.listHelp=Listbox(frameHelpList,height=5,takefocus=FALSE, + frameHelpList = Frame(frameHelp) + frameHelpListButtons = Frame(frameHelpList) + scrollHelpList = Scrollbar(frameHelpList) + self.listHelp = Listbox( + frameHelpList, height=5, takefocus=FALSE, exportselection=FALSE) scrollHelpList.config(command=self.listHelp.yview) self.listHelp.config(yscrollcommand=scrollHelpList.set) - self.listHelp.bind('<ButtonRelease-1>',self.HelpSourceSelected) - self.buttonHelpListEdit=Button(frameHelpListButtons,text='Edit', - state=DISABLED,width=8,command=self.HelpListItemEdit) - self.buttonHelpListAdd=Button(frameHelpListButtons,text='Add', - width=8,command=self.HelpListItemAdd) - self.buttonHelpListRemove=Button(frameHelpListButtons,text='Remove', - state=DISABLED,width=8,command=self.HelpListItemRemove) + self.listHelp.bind('<ButtonRelease-1>', self.HelpSourceSelected) + self.buttonHelpListEdit = Button( + frameHelpListButtons, text='Edit', state=DISABLED, + width=8, command=self.HelpListItemEdit) + self.buttonHelpListAdd = Button( + frameHelpListButtons, text='Add', + width=8, command=self.HelpListItemAdd) + self.buttonHelpListRemove = Button( + frameHelpListButtons, text='Remove', state=DISABLED, + width=8, command=self.HelpListItemRemove) + #widget packing #body - frameRun.pack(side=TOP,padx=5,pady=5,fill=X) - frameSave.pack(side=TOP,padx=5,pady=5,fill=X) - frameWinSize.pack(side=TOP,padx=5,pady=5,fill=X) - frameParaSize.pack(side=TOP,padx=5,pady=5,fill=X) - frameHelp.pack(side=TOP,padx=5,pady=5,expand=TRUE,fill=BOTH) + frameRun.pack(side=TOP, padx=5, pady=5, fill=X) + frameSave.pack(side=TOP, padx=5, pady=5, fill=X) + frameWinSize.pack(side=TOP, padx=5, pady=5, fill=X) + frameHelp.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) #frameRun - labelRunChoiceTitle.pack(side=LEFT,anchor=W,padx=5,pady=5) - radioStartupShell.pack(side=RIGHT,anchor=W,padx=5,pady=5) - radioStartupEdit.pack(side=RIGHT,anchor=W,padx=5,pady=5) + labelRunChoiceTitle.pack(side=LEFT, anchor=W, padx=5, pady=5) + radioStartupShell.pack(side=RIGHT, anchor=W, padx=5, pady=5) + radioStartupEdit.pack(side=RIGHT, anchor=W, padx=5, pady=5) #frameSave - labelRunSaveTitle.pack(side=LEFT,anchor=W,padx=5,pady=5) - radioSaveAuto.pack(side=RIGHT,anchor=W,padx=5,pady=5) - radioSaveAsk.pack(side=RIGHT,anchor=W,padx=5,pady=5) + labelRunSaveTitle.pack(side=LEFT, anchor=W, padx=5, pady=5) + radioSaveAuto.pack(side=RIGHT, anchor=W, padx=5, pady=5) + radioSaveAsk.pack(side=RIGHT, anchor=W, padx=5, pady=5) #frameWinSize - labelWinSizeTitle.pack(side=LEFT,anchor=W,padx=5,pady=5) - entryWinHeight.pack(side=RIGHT,anchor=E,padx=10,pady=5) - labelWinHeightTitle.pack(side=RIGHT,anchor=E,pady=5) - entryWinWidth.pack(side=RIGHT,anchor=E,padx=10,pady=5) - labelWinWidthTitle.pack(side=RIGHT,anchor=E,pady=5) - #paragraphFormatWidth - labelParaWidthTitle.pack(side=LEFT,anchor=W,padx=5,pady=5) - entryParaWidth.pack(side=RIGHT,anchor=E,padx=10,pady=5) + labelWinSizeTitle.pack(side=LEFT, anchor=W, padx=5, pady=5) + entryWinHeight.pack(side=RIGHT, anchor=E, padx=10, pady=5) + labelWinHeightTitle.pack(side=RIGHT, anchor=E, pady=5) + entryWinWidth.pack(side=RIGHT, anchor=E, padx=10, pady=5) + labelWinWidthTitle.pack(side=RIGHT, anchor=E, pady=5) #frameHelp - frameHelpListButtons.pack(side=RIGHT,padx=5,pady=5,fill=Y) - frameHelpList.pack(side=TOP,padx=5,pady=5,expand=TRUE,fill=BOTH) - scrollHelpList.pack(side=RIGHT,anchor=W,fill=Y) - self.listHelp.pack(side=LEFT,anchor=E,expand=TRUE,fill=BOTH) - self.buttonHelpListEdit.pack(side=TOP,anchor=W,pady=5) - self.buttonHelpListAdd.pack(side=TOP,anchor=W) - self.buttonHelpListRemove.pack(side=TOP,anchor=W,pady=5) + frameHelpListButtons.pack(side=RIGHT, padx=5, pady=5, fill=Y) + frameHelpList.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) + scrollHelpList.pack(side=RIGHT, anchor=W, fill=Y) + self.listHelp.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH) + self.buttonHelpListEdit.pack(side=TOP, anchor=W, pady=5) + self.buttonHelpListAdd.pack(side=TOP, anchor=W) + self.buttonHelpListRemove.pack(side=TOP, anchor=W, pady=5) return frame def AttachVarCallbacks(self): - self.fontSize.trace_variable('w',self.VarChanged_fontSize) - self.fontName.trace_variable('w',self.VarChanged_fontName) - self.fontBold.trace_variable('w',self.VarChanged_fontBold) - self.spaceNum.trace_variable('w',self.VarChanged_spaceNum) - self.colour.trace_variable('w',self.VarChanged_colour) - self.builtinTheme.trace_variable('w',self.VarChanged_builtinTheme) - self.customTheme.trace_variable('w',self.VarChanged_customTheme) - self.themeIsBuiltin.trace_variable('w',self.VarChanged_themeIsBuiltin) - self.highlightTarget.trace_variable('w',self.VarChanged_highlightTarget) - self.keyBinding.trace_variable('w',self.VarChanged_keyBinding) - self.builtinKeys.trace_variable('w',self.VarChanged_builtinKeys) - self.customKeys.trace_variable('w',self.VarChanged_customKeys) - self.keysAreBuiltin.trace_variable('w',self.VarChanged_keysAreBuiltin) - self.winWidth.trace_variable('w',self.VarChanged_winWidth) - self.winHeight.trace_variable('w',self.VarChanged_winHeight) - self.paraWidth.trace_variable('w',self.VarChanged_paraWidth) - self.startupEdit.trace_variable('w',self.VarChanged_startupEdit) - self.autoSave.trace_variable('w',self.VarChanged_autoSave) - self.encoding.trace_variable('w',self.VarChanged_encoding) - - def VarChanged_fontSize(self,*params): - value=self.fontSize.get() - self.AddChangedItem('main','EditorWindow','font-size',value) - - def VarChanged_fontName(self,*params): - value=self.fontName.get() - self.AddChangedItem('main','EditorWindow','font',value) - - def VarChanged_fontBold(self,*params): - value=self.fontBold.get() - self.AddChangedItem('main','EditorWindow','font-bold',value) - - def VarChanged_spaceNum(self,*params): - value=self.spaceNum.get() - self.AddChangedItem('main','Indent','num-spaces',value) - - def VarChanged_colour(self,*params): + self.fontSize.trace_variable('w', self.VarChanged_font) + self.fontName.trace_variable('w', self.VarChanged_font) + self.fontBold.trace_variable('w', self.VarChanged_font) + self.spaceNum.trace_variable('w', self.VarChanged_spaceNum) + self.colour.trace_variable('w', self.VarChanged_colour) + self.builtinTheme.trace_variable('w', self.VarChanged_builtinTheme) + self.customTheme.trace_variable('w', self.VarChanged_customTheme) + self.themeIsBuiltin.trace_variable('w', self.VarChanged_themeIsBuiltin) + self.highlightTarget.trace_variable('w', self.VarChanged_highlightTarget) + self.keyBinding.trace_variable('w', self.VarChanged_keyBinding) + self.builtinKeys.trace_variable('w', self.VarChanged_builtinKeys) + self.customKeys.trace_variable('w', self.VarChanged_customKeys) + self.keysAreBuiltin.trace_variable('w', self.VarChanged_keysAreBuiltin) + self.winWidth.trace_variable('w', self.VarChanged_winWidth) + self.winHeight.trace_variable('w', self.VarChanged_winHeight) + self.startupEdit.trace_variable('w', self.VarChanged_startupEdit) + self.autoSave.trace_variable('w', self.VarChanged_autoSave) + self.encoding.trace_variable('w', self.VarChanged_encoding) + + def VarChanged_font(self, *params): + '''When one font attribute changes, save them all, as they are + not independent from each other. In particular, when we are + overriding the default font, we need to write out everything. + ''' + value = self.fontName.get() + self.AddChangedItem('main', 'EditorWindow', 'font', value) + value = self.fontSize.get() + self.AddChangedItem('main', 'EditorWindow', 'font-size', value) + value = self.fontBold.get() + self.AddChangedItem('main', 'EditorWindow', 'font-bold', value) + + def VarChanged_spaceNum(self, *params): + value = self.spaceNum.get() + self.AddChangedItem('main', 'Indent', 'num-spaces', value) + + def VarChanged_colour(self, *params): self.OnNewColourSet() - def VarChanged_builtinTheme(self,*params): - value=self.builtinTheme.get() - self.AddChangedItem('main','Theme','name',value) + def VarChanged_builtinTheme(self, *params): + value = self.builtinTheme.get() + if value == 'IDLE Dark': + if idleConf.GetOption('main', 'Theme', 'name') != 'IDLE New': + self.AddChangedItem('main', 'Theme', 'name', 'IDLE Classic') + self.AddChangedItem('main', 'Theme', 'name2', value) + self.new_custom_theme.config(text='New theme, see Help', + fg='#500000') + else: + self.AddChangedItem('main', 'Theme', 'name', value) + self.AddChangedItem('main', 'Theme', 'name2', '') + self.new_custom_theme.config(text='', fg='black') self.PaintThemeSample() - def VarChanged_customTheme(self,*params): - value=self.customTheme.get() + def VarChanged_customTheme(self, *params): + value = self.customTheme.get() if value != '- no custom themes -': - self.AddChangedItem('main','Theme','name',value) + self.AddChangedItem('main', 'Theme', 'name', value) self.PaintThemeSample() - def VarChanged_themeIsBuiltin(self,*params): - value=self.themeIsBuiltin.get() - self.AddChangedItem('main','Theme','default',value) + def VarChanged_themeIsBuiltin(self, *params): + value = self.themeIsBuiltin.get() + self.AddChangedItem('main', 'Theme', 'default', value) if value: self.VarChanged_builtinTheme() else: self.VarChanged_customTheme() - def VarChanged_highlightTarget(self,*params): + def VarChanged_highlightTarget(self, *params): self.SetHighlightTarget() - def VarChanged_keyBinding(self,*params): - value=self.keyBinding.get() - keySet=self.customKeys.get() - event=self.listBindings.get(ANCHOR).split()[0] + def VarChanged_keyBinding(self, *params): + value = self.keyBinding.get() + keySet = self.customKeys.get() + event = self.listBindings.get(ANCHOR).split()[0] if idleConf.IsCoreBinding(event): #this is a core keybinding - self.AddChangedItem('keys',keySet,event,value) + self.AddChangedItem('keys', keySet, event, value) else: #this is an extension key binding - extName=idleConf.GetExtnNameForEvent(event) - extKeybindSection=extName+'_cfgBindings' - self.AddChangedItem('extensions',extKeybindSection,event,value) + extName = idleConf.GetExtnNameForEvent(event) + extKeybindSection = extName + '_cfgBindings' + self.AddChangedItem('extensions', extKeybindSection, event, value) - def VarChanged_builtinKeys(self,*params): - value=self.builtinKeys.get() - self.AddChangedItem('main','Keys','name',value) + def VarChanged_builtinKeys(self, *params): + value = self.builtinKeys.get() + self.AddChangedItem('main', 'Keys', 'name', value) self.LoadKeysList(value) - def VarChanged_customKeys(self,*params): - value=self.customKeys.get() + def VarChanged_customKeys(self, *params): + value = self.customKeys.get() if value != '- no custom keys -': - self.AddChangedItem('main','Keys','name',value) + self.AddChangedItem('main', 'Keys', 'name', value) self.LoadKeysList(value) - def VarChanged_keysAreBuiltin(self,*params): - value=self.keysAreBuiltin.get() - self.AddChangedItem('main','Keys','default',value) + def VarChanged_keysAreBuiltin(self, *params): + value = self.keysAreBuiltin.get() + self.AddChangedItem('main', 'Keys', 'default', value) if value: self.VarChanged_builtinKeys() else: self.VarChanged_customKeys() - def VarChanged_winWidth(self,*params): - value=self.winWidth.get() - self.AddChangedItem('main','EditorWindow','width',value) + def VarChanged_winWidth(self, *params): + value = self.winWidth.get() + self.AddChangedItem('main', 'EditorWindow', 'width', value) - def VarChanged_winHeight(self,*params): - value=self.winHeight.get() - self.AddChangedItem('main','EditorWindow','height',value) + def VarChanged_winHeight(self, *params): + value = self.winHeight.get() + self.AddChangedItem('main', 'EditorWindow', 'height', value) - def VarChanged_paraWidth(self,*params): - value=self.paraWidth.get() - self.AddChangedItem('main','FormatParagraph','paragraph',value) + def VarChanged_startupEdit(self, *params): + value = self.startupEdit.get() + self.AddChangedItem('main', 'General', 'editor-on-startup', value) - def VarChanged_startupEdit(self,*params): - value=self.startupEdit.get() - self.AddChangedItem('main','General','editor-on-startup',value) + def VarChanged_autoSave(self, *params): + value = self.autoSave.get() + self.AddChangedItem('main', 'General', 'autosave', value) - def VarChanged_autoSave(self,*params): - value=self.autoSave.get() - self.AddChangedItem('main','General','autosave',value) - - def VarChanged_encoding(self,*params): - value=self.encoding.get() - self.AddChangedItem('main','EditorWindow','encoding',value) + def VarChanged_encoding(self, *params): + value = self.encoding.get() + self.AddChangedItem('main', 'EditorWindow', 'encoding', value) def ResetChangedItems(self): #When any config item is changed in this dialog, an entry @@ -548,24 +590,25 @@ class ConfigDialog(Toplevel): #dictionary. The key should be the config file section name and the #value a dictionary, whose key:value pairs are item=value pairs for #that config file section. - self.changedItems={'main':{},'highlight':{},'keys':{},'extensions':{}} + self.changedItems = {'main':{}, 'highlight':{}, 'keys':{}, + 'extensions':{}} - def AddChangedItem(self,type,section,item,value): - value=str(value) #make sure we use a string - if section not in self.changedItems[type]: - self.changedItems[type][section]={} - self.changedItems[type][section][item]=value + def AddChangedItem(self, typ, section, item, value): + value = str(value) #make sure we use a string + if section not in self.changedItems[typ]: + self.changedItems[typ][section] = {} + self.changedItems[typ][section][item] = value def GetDefaultItems(self): - dItems={'main':{},'highlight':{},'keys':{},'extensions':{}} + dItems={'main':{}, 'highlight':{}, 'keys':{}, 'extensions':{}} for configType in dItems: - sections=idleConf.GetSectionList('default',configType) + sections = idleConf.GetSectionList('default', configType) for section in sections: - dItems[configType][section]={} - options=idleConf.defaultCfg[configType].GetOptionList(section) + dItems[configType][section] = {} + options = idleConf.defaultCfg[configType].GetOptionList(section) for option in options: - dItems[configType][section][option]=( - idleConf.defaultCfg[configType].Get(section,option)) + dItems[configType][section][option] = ( + idleConf.defaultCfg[configType].Get(section, option)) return dItems def SetThemeType(self): @@ -591,26 +634,26 @@ class ConfigDialog(Toplevel): self.buttonDeleteCustomKeys.config(state=NORMAL) def GetNewKeys(self): - listIndex=self.listBindings.index(ANCHOR) - binding=self.listBindings.get(listIndex) - bindName=binding.split()[0] #first part, up to first space + listIndex = self.listBindings.index(ANCHOR) + binding = self.listBindings.get(listIndex) + bindName = binding.split()[0] #first part, up to first space if self.keysAreBuiltin.get(): - currentKeySetName=self.builtinKeys.get() + currentKeySetName = self.builtinKeys.get() else: - currentKeySetName=self.customKeys.get() - currentBindings=idleConf.GetCurrentKeySet() + currentKeySetName = self.customKeys.get() + currentBindings = idleConf.GetCurrentKeySet() if currentKeySetName in self.changedItems['keys']: #unsaved changes - keySetChanges=self.changedItems['keys'][currentKeySetName] + keySetChanges = self.changedItems['keys'][currentKeySetName] for event in keySetChanges: - currentBindings[event]=keySetChanges[event].split() + currentBindings[event] = keySetChanges[event].split() currentKeySequences = list(currentBindings.values()) - newKeys=GetKeysDialog(self,'Get New Keys',bindName, + newKeys = GetKeysDialog(self, 'Get New Keys', bindName, currentKeySequences).result if newKeys: #new keys were specified if self.keysAreBuiltin.get(): #current key set is a built-in - message=('Your changes will be saved as a new Custom Key Set. '+ - 'Enter a name for your new Custom Key Set below.') - newKeySet=self.GetNewKeysName(message) + message = ('Your changes will be saved as a new Custom Key Set.' + ' Enter a name for your new Custom Key Set below.') + newKeySet = self.GetNewKeysName(message) if not newKeySet: #user cancelled custom key set creation self.listBindings.select_set(listIndex) self.listBindings.select_anchor(listIndex) @@ -618,7 +661,7 @@ class ConfigDialog(Toplevel): else: #create new custom key set based on previously active key set self.CreateNewKeySet(newKeySet) self.listBindings.delete(listIndex) - self.listBindings.insert(listIndex,bindName+' - '+newKeys) + self.listBindings.insert(listIndex, bindName+' - '+newKeys) self.listBindings.select_set(listIndex) self.listBindings.select_anchor(listIndex) self.keyBinding.set(newKeys) @@ -626,65 +669,65 @@ class ConfigDialog(Toplevel): self.listBindings.select_set(listIndex) self.listBindings.select_anchor(listIndex) - def GetNewKeysName(self,message): - usedNames=(idleConf.GetSectionList('user','keys')+ - idleConf.GetSectionList('default','keys')) - newKeySet=GetCfgSectionNameDialog(self,'New Custom Key Set', - message,usedNames).result + def GetNewKeysName(self, message): + usedNames = (idleConf.GetSectionList('user', 'keys') + + idleConf.GetSectionList('default', 'keys')) + newKeySet = GetCfgSectionNameDialog( + self, 'New Custom Key Set', message, usedNames).result return newKeySet def SaveAsNewKeySet(self): - newKeysName=self.GetNewKeysName('New Key Set Name:') + newKeysName = self.GetNewKeysName('New Key Set Name:') if newKeysName: self.CreateNewKeySet(newKeysName) - def KeyBindingSelected(self,event): + def KeyBindingSelected(self, event): self.buttonNewKeys.config(state=NORMAL) - def CreateNewKeySet(self,newKeySetName): + def CreateNewKeySet(self, newKeySetName): #creates new custom key set based on the previously active key set, #and makes the new key set active if self.keysAreBuiltin.get(): - prevKeySetName=self.builtinKeys.get() + prevKeySetName = self.builtinKeys.get() else: - prevKeySetName=self.customKeys.get() - prevKeys=idleConf.GetCoreKeys(prevKeySetName) - newKeys={} + prevKeySetName = self.customKeys.get() + prevKeys = idleConf.GetCoreKeys(prevKeySetName) + newKeys = {} for event in prevKeys: #add key set to changed items - eventName=event[2:-2] #trim off the angle brackets - binding=' '.join(prevKeys[event]) - newKeys[eventName]=binding + eventName = event[2:-2] #trim off the angle brackets + binding = ' '.join(prevKeys[event]) + newKeys[eventName] = binding #handle any unsaved changes to prev key set if prevKeySetName in self.changedItems['keys']: - keySetChanges=self.changedItems['keys'][prevKeySetName] + keySetChanges = self.changedItems['keys'][prevKeySetName] for event in keySetChanges: - newKeys[event]=keySetChanges[event] + newKeys[event] = keySetChanges[event] #save the new theme - self.SaveNewKeySet(newKeySetName,newKeys) + self.SaveNewKeySet(newKeySetName, newKeys) #change gui over to the new key set - customKeyList=idleConf.GetSectionList('user','keys') + customKeyList = idleConf.GetSectionList('user', 'keys') customKeyList.sort() - self.optMenuKeysCustom.SetMenu(customKeyList,newKeySetName) + self.optMenuKeysCustom.SetMenu(customKeyList, newKeySetName) self.keysAreBuiltin.set(0) self.SetKeysType() - def LoadKeysList(self,keySetName): - reselect=0 - newKeySet=0 + def LoadKeysList(self, keySetName): + reselect = 0 + newKeySet = 0 if self.listBindings.curselection(): - reselect=1 - listIndex=self.listBindings.index(ANCHOR) - keySet=idleConf.GetKeySet(keySetName) + reselect = 1 + listIndex = self.listBindings.index(ANCHOR) + keySet = idleConf.GetKeySet(keySetName) bindNames = list(keySet.keys()) bindNames.sort() - self.listBindings.delete(0,END) + self.listBindings.delete(0, END) for bindName in bindNames: - key=' '.join(keySet[bindName]) #make key(s) into a string - bindName=bindName[2:-2] #trim off the angle brackets + key = ' '.join(keySet[bindName]) #make key(s) into a string + bindName = bindName[2:-2] #trim off the angle brackets if keySetName in self.changedItems['keys']: #handle any unsaved changes to this key set if bindName in self.changedItems['keys'][keySetName]: - key=self.changedItems['keys'][keySetName][bindName] + key = self.changedItems['keys'][keySetName][bindName] self.listBindings.insert(END, bindName+' - '+key) if reselect: self.listBindings.see(listIndex) @@ -693,9 +736,9 @@ class ConfigDialog(Toplevel): def DeleteCustomKeys(self): keySetName=self.customKeys.get() - if not tkMessageBox.askyesno('Delete Key Set','Are you sure you wish '+ - 'to delete the key set %r ?' % (keySetName), - parent=self): + delmsg = 'Are you sure you wish to delete the key set %r ?' + if not tkMessageBox.askyesno( + 'Delete Key Set', delmsg % keySetName, parent=self): return #remove key set from config idleConf.userCfg['keys'].remove_section(keySetName) @@ -704,25 +747,25 @@ class ConfigDialog(Toplevel): #write changes idleConf.userCfg['keys'].Save() #reload user key set list - itemList=idleConf.GetSectionList('user','keys') + itemList = idleConf.GetSectionList('user', 'keys') itemList.sort() if not itemList: self.radioKeysCustom.config(state=DISABLED) - self.optMenuKeysCustom.SetMenu(itemList,'- no custom keys -') + self.optMenuKeysCustom.SetMenu(itemList, '- no custom keys -') else: - self.optMenuKeysCustom.SetMenu(itemList,itemList[0]) + self.optMenuKeysCustom.SetMenu(itemList, itemList[0]) #revert to default key set - self.keysAreBuiltin.set(idleConf.defaultCfg['main'].Get('Keys','default')) - self.builtinKeys.set(idleConf.defaultCfg['main'].Get('Keys','name')) + self.keysAreBuiltin.set(idleConf.defaultCfg['main'].Get('Keys', 'default')) + self.builtinKeys.set(idleConf.defaultCfg['main'].Get('Keys', 'name')) #user can't back out of these changes, they must be applied now self.Apply() self.SetKeysType() def DeleteCustomTheme(self): - themeName=self.customTheme.get() - if not tkMessageBox.askyesno('Delete Theme','Are you sure you wish '+ - 'to delete the theme %r ?' % (themeName,), - parent=self): + themeName = self.customTheme.get() + delmsg = 'Are you sure you wish to delete the theme %r ?' + if not tkMessageBox.askyesno( + 'Delete Theme', delmsg % themeName, parent=self): return #remove theme from config idleConf.userCfg['highlight'].remove_section(themeName) @@ -731,153 +774,149 @@ class ConfigDialog(Toplevel): #write changes idleConf.userCfg['highlight'].Save() #reload user theme list - itemList=idleConf.GetSectionList('user','highlight') + itemList = idleConf.GetSectionList('user', 'highlight') itemList.sort() if not itemList: self.radioThemeCustom.config(state=DISABLED) - self.optMenuThemeCustom.SetMenu(itemList,'- no custom themes -') + self.optMenuThemeCustom.SetMenu(itemList, '- no custom themes -') else: - self.optMenuThemeCustom.SetMenu(itemList,itemList[0]) + self.optMenuThemeCustom.SetMenu(itemList, itemList[0]) #revert to default theme - self.themeIsBuiltin.set(idleConf.defaultCfg['main'].Get('Theme','default')) - self.builtinTheme.set(idleConf.defaultCfg['main'].Get('Theme','name')) + self.themeIsBuiltin.set(idleConf.defaultCfg['main'].Get('Theme', 'default')) + self.builtinTheme.set(idleConf.defaultCfg['main'].Get('Theme', 'name')) #user can't back out of these changes, they must be applied now self.Apply() self.SetThemeType() def GetColour(self): - target=self.highlightTarget.get() - prevColour=self.frameColourSet.cget('bg') - rgbTuplet, colourString = tkColorChooser.askcolor(parent=self, - title='Pick new colour for : '+target,initialcolor=prevColour) - if colourString and (colourString!=prevColour): + target = self.highlightTarget.get() + prevColour = self.frameColourSet.cget('bg') + rgbTuplet, colourString = tkColorChooser.askcolor( + parent=self, title='Pick new colour for : '+target, + initialcolor=prevColour) + if colourString and (colourString != prevColour): #user didn't cancel, and they chose a new colour - if self.themeIsBuiltin.get(): #current theme is a built-in - message=('Your changes will be saved as a new Custom Theme. '+ - 'Enter a name for your new Custom Theme below.') - newTheme=self.GetNewThemeName(message) - if not newTheme: #user cancelled custom theme creation + if self.themeIsBuiltin.get(): #current theme is a built-in + message = ('Your changes will be saved as a new Custom Theme. ' + 'Enter a name for your new Custom Theme below.') + newTheme = self.GetNewThemeName(message) + if not newTheme: #user cancelled custom theme creation return - else: #create new custom theme based on previously active theme + else: #create new custom theme based on previously active theme self.CreateNewTheme(newTheme) self.colour.set(colourString) - else: #current theme is user defined + else: #current theme is user defined self.colour.set(colourString) def OnNewColourSet(self): newColour=self.colour.get() - self.frameColourSet.config(bg=newColour)#set sample - if self.fgHilite.get(): plane='foreground' - else: plane='background' - sampleElement=self.themeElements[self.highlightTarget.get()][0] + self.frameColourSet.config(bg=newColour) #set sample + plane ='foreground' if self.fgHilite.get() else 'background' + sampleElement = self.themeElements[self.highlightTarget.get()][0] self.textHighlightSample.tag_config(sampleElement, **{plane:newColour}) - theme=self.customTheme.get() - themeElement=sampleElement+'-'+plane - self.AddChangedItem('highlight',theme,themeElement,newColour) - - def GetNewThemeName(self,message): - usedNames=(idleConf.GetSectionList('user','highlight')+ - idleConf.GetSectionList('default','highlight')) - newTheme=GetCfgSectionNameDialog(self,'New Custom Theme', - message,usedNames).result + theme = self.customTheme.get() + themeElement = sampleElement + '-' + plane + self.AddChangedItem('highlight', theme, themeElement, newColour) + + def GetNewThemeName(self, message): + usedNames = (idleConf.GetSectionList('user', 'highlight') + + idleConf.GetSectionList('default', 'highlight')) + newTheme = GetCfgSectionNameDialog( + self, 'New Custom Theme', message, usedNames).result return newTheme def SaveAsNewTheme(self): - newThemeName=self.GetNewThemeName('New Theme Name:') + newThemeName = self.GetNewThemeName('New Theme Name:') if newThemeName: self.CreateNewTheme(newThemeName) - def CreateNewTheme(self,newThemeName): + def CreateNewTheme(self, newThemeName): #creates new custom theme based on the previously active theme, #and makes the new theme active if self.themeIsBuiltin.get(): - themeType='default' - themeName=self.builtinTheme.get() + themeType = 'default' + themeName = self.builtinTheme.get() else: - themeType='user' - themeName=self.customTheme.get() - newTheme=idleConf.GetThemeDict(themeType,themeName) + themeType = 'user' + themeName = self.customTheme.get() + newTheme = idleConf.GetThemeDict(themeType, themeName) #apply any of the old theme's unsaved changes to the new theme if themeName in self.changedItems['highlight']: - themeChanges=self.changedItems['highlight'][themeName] + themeChanges = self.changedItems['highlight'][themeName] for element in themeChanges: - newTheme[element]=themeChanges[element] + newTheme[element] = themeChanges[element] #save the new theme - self.SaveNewTheme(newThemeName,newTheme) + self.SaveNewTheme(newThemeName, newTheme) #change gui over to the new theme - customThemeList=idleConf.GetSectionList('user','highlight') + customThemeList = idleConf.GetSectionList('user', 'highlight') customThemeList.sort() - self.optMenuThemeCustom.SetMenu(customThemeList,newThemeName) + self.optMenuThemeCustom.SetMenu(customThemeList, newThemeName) self.themeIsBuiltin.set(0) self.SetThemeType() - def OnListFontButtonRelease(self,event): + def OnListFontButtonRelease(self, event): font = self.listFontName.get(ANCHOR) self.fontName.set(font.lower()) self.SetFontSample() - def SetFontSample(self,event=None): - fontName=self.fontName.get() - if self.fontBold.get(): - fontWeight=tkFont.BOLD - else: - fontWeight=tkFont.NORMAL + def SetFontSample(self, event=None): + fontName = self.fontName.get() + fontWeight = tkFont.BOLD if self.fontBold.get() else tkFont.NORMAL newFont = (fontName, self.fontSize.get(), fontWeight) self.labelFontSample.config(font=newFont) self.textHighlightSample.configure(font=newFont) def SetHighlightTarget(self): - if self.highlightTarget.get()=='Cursor': #bg not possible + if self.highlightTarget.get() == 'Cursor': #bg not possible self.radioFg.config(state=DISABLED) self.radioBg.config(state=DISABLED) self.fgHilite.set(1) - else: #both fg and bg can be set + else: #both fg and bg can be set self.radioFg.config(state=NORMAL) self.radioBg.config(state=NORMAL) self.fgHilite.set(1) self.SetColourSample() - def SetColourSampleBinding(self,*args): + def SetColourSampleBinding(self, *args): self.SetColourSample() def SetColourSample(self): #set the colour smaple area - tag=self.themeElements[self.highlightTarget.get()][0] - if self.fgHilite.get(): plane='foreground' - else: plane='background' - colour=self.textHighlightSample.tag_cget(tag,plane) + tag = self.themeElements[self.highlightTarget.get()][0] + plane = 'foreground' if self.fgHilite.get() else 'background' + colour = self.textHighlightSample.tag_cget(tag, plane) self.frameColourSet.config(bg=colour) def PaintThemeSample(self): - if self.themeIsBuiltin.get(): #a default theme - theme=self.builtinTheme.get() - else: #a user theme - theme=self.customTheme.get() + if self.themeIsBuiltin.get(): #a default theme + theme = self.builtinTheme.get() + else: #a user theme + theme = self.customTheme.get() for elementTitle in self.themeElements: - element=self.themeElements[elementTitle][0] - colours=idleConf.GetHighlight(theme,element) - if element=='cursor': #cursor sample needs special painting - colours['background']=idleConf.GetHighlight(theme, - 'normal', fgBg='bg') + element = self.themeElements[elementTitle][0] + colours = idleConf.GetHighlight(theme, element) + if element == 'cursor': #cursor sample needs special painting + colours['background'] = idleConf.GetHighlight( + theme, 'normal', fgBg='bg') #handle any unsaved changes to this theme if theme in self.changedItems['highlight']: - themeDict=self.changedItems['highlight'][theme] - if element+'-foreground' in themeDict: - colours['foreground']=themeDict[element+'-foreground'] - if element+'-background' in themeDict: - colours['background']=themeDict[element+'-background'] + themeDict = self.changedItems['highlight'][theme] + if element + '-foreground' in themeDict: + colours['foreground'] = themeDict[element + '-foreground'] + if element + '-background' in themeDict: + colours['background'] = themeDict[element + '-background'] self.textHighlightSample.tag_config(element, **colours) self.SetColourSample() - def HelpSourceSelected(self,event): + def HelpSourceSelected(self, event): self.SetHelpListButtonStates() def SetHelpListButtonStates(self): - if self.listHelp.size()<1: #no entries in list + if self.listHelp.size() < 1: #no entries in list self.buttonHelpListEdit.config(state=DISABLED) self.buttonHelpListRemove.config(state=DISABLED) else: #there are some entries - if self.listHelp.curselection(): #there currently is a selection + if self.listHelp.curselection(): #there currently is a selection self.buttonHelpListEdit.config(state=NORMAL) self.buttonHelpListRemove.config(state=NORMAL) else: #there currently is not a selection @@ -885,28 +924,29 @@ class ConfigDialog(Toplevel): self.buttonHelpListRemove.config(state=DISABLED) def HelpListItemAdd(self): - helpSource=GetHelpSourceDialog(self,'New Help Source').result + helpSource = GetHelpSourceDialog(self, 'New Help Source').result if helpSource: - self.userHelpList.append( (helpSource[0],helpSource[1]) ) - self.listHelp.insert(END,helpSource[0]) + self.userHelpList.append((helpSource[0], helpSource[1])) + self.listHelp.insert(END, helpSource[0]) self.UpdateUserHelpChangedItems() self.SetHelpListButtonStates() def HelpListItemEdit(self): - itemIndex=self.listHelp.index(ANCHOR) - helpSource=self.userHelpList[itemIndex] - newHelpSource=GetHelpSourceDialog(self,'Edit Help Source', - menuItem=helpSource[0],filePath=helpSource[1]).result - if (not newHelpSource) or (newHelpSource==helpSource): + itemIndex = self.listHelp.index(ANCHOR) + helpSource = self.userHelpList[itemIndex] + newHelpSource = GetHelpSourceDialog( + self, 'Edit Help Source', menuItem=helpSource[0], + filePath=helpSource[1]).result + if (not newHelpSource) or (newHelpSource == helpSource): return #no changes - self.userHelpList[itemIndex]=newHelpSource + self.userHelpList[itemIndex] = newHelpSource self.listHelp.delete(itemIndex) - self.listHelp.insert(itemIndex,newHelpSource[0]) + self.listHelp.insert(itemIndex, newHelpSource[0]) self.UpdateUserHelpChangedItems() self.SetHelpListButtonStates() def HelpListItemRemove(self): - itemIndex=self.listHelp.index(ANCHOR) + itemIndex = self.listHelp.index(ANCHOR) del(self.userHelpList[itemIndex]) self.listHelp.delete(itemIndex) self.UpdateUserHelpChangedItems() @@ -915,128 +955,126 @@ class ConfigDialog(Toplevel): def UpdateUserHelpChangedItems(self): "Clear and rebuild the HelpFiles section in self.changedItems" self.changedItems['main']['HelpFiles'] = {} - for num in range(1,len(self.userHelpList)+1): - self.AddChangedItem('main','HelpFiles',str(num), + for num in range(1, len(self.userHelpList) + 1): + self.AddChangedItem( + 'main', 'HelpFiles', str(num), ';'.join(self.userHelpList[num-1][:2])) def LoadFontCfg(self): ##base editor font selection list - fonts=list(tkFont.families(self)) + fonts = list(tkFont.families(self)) fonts.sort() for font in fonts: - self.listFontName.insert(END,font) - configuredFont=idleConf.GetOption('main','EditorWindow','font', - default='courier') - lc_configuredFont = configuredFont.lower() - self.fontName.set(lc_configuredFont) + self.listFontName.insert(END, font) + configuredFont = idleConf.GetFont(self, 'main', 'EditorWindow') + fontName = configuredFont[0].lower() + fontSize = configuredFont[1] + fontBold = configuredFont[2]=='bold' + self.fontName.set(fontName) lc_fonts = [s.lower() for s in fonts] - if lc_configuredFont in lc_fonts: - currentFontIndex = lc_fonts.index(lc_configuredFont) + try: + currentFontIndex = lc_fonts.index(fontName) self.listFontName.see(currentFontIndex) self.listFontName.select_set(currentFontIndex) self.listFontName.select_anchor(currentFontIndex) + except ValueError: + pass ##font size dropdown - fontSize=idleConf.GetOption('main', 'EditorWindow', 'font-size', - type='int', default='10') - self.optMenuFontSize.SetMenu(('7','8','9','10','11','12','13','14', - '16','18','20','22'), fontSize ) + self.optMenuFontSize.SetMenu(('7', '8', '9', '10', '11', '12', '13', + '14', '16', '18', '20', '22'), fontSize ) ##fontWeight - self.fontBold.set(idleConf.GetOption('main','EditorWindow', - 'font-bold',default=0,type='bool')) + self.fontBold.set(fontBold) ##font sample self.SetFontSample() def LoadTabCfg(self): ##indent sizes - spaceNum=idleConf.GetOption('main','Indent','num-spaces', - default=4,type='int') + spaceNum = idleConf.GetOption( + 'main', 'Indent', 'num-spaces', default=4, type='int') self.spaceNum.set(spaceNum) def LoadThemeCfg(self): ##current theme type radiobutton - self.themeIsBuiltin.set(idleConf.GetOption('main','Theme','default', - type='bool',default=1)) + self.themeIsBuiltin.set(idleConf.GetOption( + 'main', 'Theme', 'default', type='bool', default=1)) ##currently set theme - currentOption=idleConf.CurrentTheme() + currentOption = idleConf.CurrentTheme() ##load available theme option menus if self.themeIsBuiltin.get(): #default theme selected - itemList=idleConf.GetSectionList('default','highlight') + itemList = idleConf.GetSectionList('default', 'highlight') itemList.sort() - self.optMenuThemeBuiltin.SetMenu(itemList,currentOption) - itemList=idleConf.GetSectionList('user','highlight') + self.optMenuThemeBuiltin.SetMenu(itemList, currentOption) + itemList = idleConf.GetSectionList('user', 'highlight') itemList.sort() if not itemList: self.radioThemeCustom.config(state=DISABLED) self.customTheme.set('- no custom themes -') else: - self.optMenuThemeCustom.SetMenu(itemList,itemList[0]) + self.optMenuThemeCustom.SetMenu(itemList, itemList[0]) else: #user theme selected - itemList=idleConf.GetSectionList('user','highlight') + itemList = idleConf.GetSectionList('user', 'highlight') itemList.sort() - self.optMenuThemeCustom.SetMenu(itemList,currentOption) - itemList=idleConf.GetSectionList('default','highlight') + self.optMenuThemeCustom.SetMenu(itemList, currentOption) + itemList = idleConf.GetSectionList('default', 'highlight') itemList.sort() - self.optMenuThemeBuiltin.SetMenu(itemList,itemList[0]) + self.optMenuThemeBuiltin.SetMenu(itemList, itemList[0]) self.SetThemeType() ##load theme element option menu themeNames = list(self.themeElements.keys()) themeNames.sort(key=lambda x: self.themeElements[x][1]) - self.optMenuHighlightTarget.SetMenu(themeNames,themeNames[0]) + self.optMenuHighlightTarget.SetMenu(themeNames, themeNames[0]) self.PaintThemeSample() self.SetHighlightTarget() def LoadKeyCfg(self): ##current keys type radiobutton - self.keysAreBuiltin.set(idleConf.GetOption('main','Keys','default', - type='bool',default=1)) + self.keysAreBuiltin.set(idleConf.GetOption( + 'main', 'Keys', 'default', type='bool', default=1)) ##currently set keys - currentOption=idleConf.CurrentKeys() + currentOption = idleConf.CurrentKeys() ##load available keyset option menus if self.keysAreBuiltin.get(): #default theme selected - itemList=idleConf.GetSectionList('default','keys') + itemList = idleConf.GetSectionList('default', 'keys') itemList.sort() - self.optMenuKeysBuiltin.SetMenu(itemList,currentOption) - itemList=idleConf.GetSectionList('user','keys') + self.optMenuKeysBuiltin.SetMenu(itemList, currentOption) + itemList = idleConf.GetSectionList('user', 'keys') itemList.sort() if not itemList: self.radioKeysCustom.config(state=DISABLED) self.customKeys.set('- no custom keys -') else: - self.optMenuKeysCustom.SetMenu(itemList,itemList[0]) + self.optMenuKeysCustom.SetMenu(itemList, itemList[0]) else: #user key set selected - itemList=idleConf.GetSectionList('user','keys') + itemList = idleConf.GetSectionList('user', 'keys') itemList.sort() - self.optMenuKeysCustom.SetMenu(itemList,currentOption) - itemList=idleConf.GetSectionList('default','keys') + self.optMenuKeysCustom.SetMenu(itemList, currentOption) + itemList = idleConf.GetSectionList('default', 'keys') itemList.sort() - self.optMenuKeysBuiltin.SetMenu(itemList,itemList[0]) + self.optMenuKeysBuiltin.SetMenu(itemList, itemList[0]) self.SetKeysType() ##load keyset element list - keySetName=idleConf.CurrentKeys() + keySetName = idleConf.CurrentKeys() self.LoadKeysList(keySetName) def LoadGeneralCfg(self): #startup state - self.startupEdit.set(idleConf.GetOption('main','General', - 'editor-on-startup',default=1,type='bool')) + self.startupEdit.set(idleConf.GetOption( + 'main', 'General', 'editor-on-startup', default=1, type='bool')) #autosave state - self.autoSave.set(idleConf.GetOption('main', 'General', 'autosave', - default=0, type='bool')) + self.autoSave.set(idleConf.GetOption( + 'main', 'General', 'autosave', default=0, type='bool')) #initial window size - self.winWidth.set(idleConf.GetOption('main','EditorWindow','width', - type='int')) - self.winHeight.set(idleConf.GetOption('main','EditorWindow','height', - type='int')) - #initial paragraph reformat size - self.paraWidth.set(idleConf.GetOption('main','FormatParagraph','paragraph', - type='int')) + self.winWidth.set(idleConf.GetOption( + 'main', 'EditorWindow', 'width', type='int')) + self.winHeight.set(idleConf.GetOption( + 'main', 'EditorWindow', 'height', type='int')) # default source encoding - self.encoding.set(idleConf.GetOption('main', 'EditorWindow', - 'encoding', default='none')) + self.encoding.set(idleConf.GetOption( + 'main', 'EditorWindow', 'encoding', default='none')) # additional help sources self.userHelpList = idleConf.GetAllExtraHelpSourcesList() for helpItem in self.userHelpList: - self.listHelp.insert(END,helpItem[0]) + self.listHelp.insert(END, helpItem[0]) self.SetHelpListButtonStates() def LoadConfigs(self): @@ -1053,8 +1091,9 @@ class ConfigDialog(Toplevel): self.LoadKeyCfg() ### general page self.LoadGeneralCfg() + # note: extension page handled separately - def SaveNewKeySet(self,keySetName,keySet): + def SaveNewKeySet(self, keySetName, keySet): """ save a newly created core key set. keySetName - string, the name of the new key set @@ -1063,10 +1102,10 @@ class ConfigDialog(Toplevel): if not idleConf.userCfg['keys'].has_section(keySetName): idleConf.userCfg['keys'].add_section(keySetName) for event in keySet: - value=keySet[event] - idleConf.userCfg['keys'].SetOption(keySetName,event,value) + value = keySet[event] + idleConf.userCfg['keys'].SetOption(keySetName, event, value) - def SaveNewTheme(self,themeName,theme): + def SaveNewTheme(self, themeName, theme): """ save a newly created theme. themeName - string, the name of the new theme @@ -1075,16 +1114,16 @@ class ConfigDialog(Toplevel): if not idleConf.userCfg['highlight'].has_section(themeName): idleConf.userCfg['highlight'].add_section(themeName) for element in theme: - value=theme[element] - idleConf.userCfg['highlight'].SetOption(themeName,element,value) + value = theme[element] + idleConf.userCfg['highlight'].SetOption(themeName, element, value) - def SetUserValue(self,configType,section,item,value): - if idleConf.defaultCfg[configType].has_option(section,item): - if idleConf.defaultCfg[configType].Get(section,item)==value: + def SetUserValue(self, configType, section, item, value): + if idleConf.defaultCfg[configType].has_option(section, item): + if idleConf.defaultCfg[configType].Get(section, item) == value: #the setting equals a default setting, remove it from user cfg - return idleConf.userCfg[configType].RemoveOption(section,item) + return idleConf.userCfg[configType].RemoveOption(section, item) #if we got here set the option - return idleConf.userCfg[configType].SetOption(section,item,value) + return idleConf.userCfg[configType].SetOption(section, item, value) def SaveAllChangedConfigs(self): "Save configuration changes to the user config file." @@ -1098,7 +1137,7 @@ class ConfigDialog(Toplevel): cfgTypeHasChanges = True for item in self.changedItems[configType][section]: value = self.changedItems[configType][section][item] - if self.SetUserValue(configType,section,item,value): + if self.SetUserValue(configType, section, item, value): cfgTypeHasChanges = True if cfgTypeHasChanges: idleConf.userCfg[configType].Save() @@ -1106,6 +1145,7 @@ class ConfigDialog(Toplevel): # save these even if unchanged! idleConf.userCfg[configType].Save() self.ResetChangedItems() #clear the changed items dict + self.save_all_changed_extensions() # uses a different mechanism def DeactivateCurrentConfig(self): #Before a config is saved, some cleanup of current @@ -1137,12 +1177,247 @@ class ConfigDialog(Toplevel): self.ActivateConfigChanges() def Help(self): - pass + page = self.tabPages._current_page + view_text(self, title='Help for IDLE preferences', + text=help_common+help_pages.get(page, '')) + + def CreatePageExtensions(self): + """Part of the config dialog used for configuring IDLE extensions. + + This code is generic - it works for any and all IDLE extensions. + + IDLE extensions save their configuration options using idleConf. + This code reads the current configuration using idleConf, supplies a + GUI interface to change the configuration values, and saves the + changes using idleConf. + + Not all changes take effect immediately - some may require restarting IDLE. + This depends on each extension's implementation. + + All values are treated as text, and it is up to the user to supply + reasonable values. The only exception to this are the 'enable*' options, + which are boolean, and can be toggled with an True/False button. + """ + parent = self.parent + frame = self.tabPages.pages['Extensions'].frame + self.ext_defaultCfg = idleConf.defaultCfg['extensions'] + self.ext_userCfg = idleConf.userCfg['extensions'] + self.is_int = self.register(is_int) + self.load_extensions() + # create widgets - a listbox shows all available extensions, with the + # controls for the extension selected in the listbox to the right + self.extension_names = StringVar(self) + frame.rowconfigure(0, weight=1) + frame.columnconfigure(2, weight=1) + self.extension_list = Listbox(frame, listvariable=self.extension_names, + selectmode='browse') + self.extension_list.bind('<<ListboxSelect>>', self.extension_selected) + scroll = Scrollbar(frame, command=self.extension_list.yview) + self.extension_list.yscrollcommand=scroll.set + self.details_frame = LabelFrame(frame, width=250, height=250) + self.extension_list.grid(column=0, row=0, sticky='nws') + scroll.grid(column=1, row=0, sticky='ns') + self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0]) + frame.configure(padx=10, pady=10) + self.config_frame = {} + self.current_extension = None + + self.outerframe = self # TEMPORARY + self.tabbed_page_set = self.extension_list # TEMPORARY + + # create the frame holding controls for each extension + ext_names = '' + for ext_name in sorted(self.extensions): + self.create_extension_frame(ext_name) + ext_names = ext_names + '{' + ext_name + '} ' + self.extension_names.set(ext_names) + self.extension_list.selection_set(0) + self.extension_selected(None) + + def load_extensions(self): + "Fill self.extensions with data from the default and user configs." + self.extensions = {} + for ext_name in idleConf.GetExtensions(active_only=False): + self.extensions[ext_name] = [] + + for ext_name in self.extensions: + opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name)) + + # bring 'enable' options to the beginning of the list + enables = [opt_name for opt_name in opt_list + if opt_name.startswith('enable')] + for opt_name in enables: + opt_list.remove(opt_name) + opt_list = enables + opt_list + + for opt_name in opt_list: + def_str = self.ext_defaultCfg.Get( + ext_name, opt_name, raw=True) + try: + def_obj = {'True':True, 'False':False}[def_str] + opt_type = 'bool' + except KeyError: + try: + def_obj = int(def_str) + opt_type = 'int' + except ValueError: + def_obj = def_str + opt_type = None + try: + value = self.ext_userCfg.Get( + ext_name, opt_name, type=opt_type, raw=True, + default=def_obj) + except ValueError: # Need this until .Get fixed + value = def_obj # bad values overwritten by entry + var = StringVar(self) + var.set(str(value)) + + self.extensions[ext_name].append({'name': opt_name, + 'type': opt_type, + 'default': def_str, + 'value': value, + 'var': var, + }) + + def extension_selected(self, event): + newsel = self.extension_list.curselection() + if newsel: + newsel = self.extension_list.get(newsel) + if newsel is None or newsel != self.current_extension: + if self.current_extension: + self.details_frame.config(text='') + self.config_frame[self.current_extension].grid_forget() + self.current_extension = None + if newsel: + self.details_frame.config(text=newsel) + self.config_frame[newsel].grid(column=0, row=0, sticky='nsew') + self.current_extension = newsel + + def create_extension_frame(self, ext_name): + """Create a frame holding the widgets to configure one extension""" + f = VerticalScrolledFrame(self.details_frame, height=250, width=250) + self.config_frame[ext_name] = f + entry_area = f.interior + # create an entry for each configuration option + for row, opt in enumerate(self.extensions[ext_name]): + # create a row with a label and entry/checkbutton + label = Label(entry_area, text=opt['name']) + label.grid(row=row, column=0, sticky=NW) + var = opt['var'] + if opt['type'] == 'bool': + Checkbutton(entry_area, textvariable=var, variable=var, + onvalue='True', offvalue='False', + indicatoron=FALSE, selectcolor='', width=8 + ).grid(row=row, column=1, sticky=W, padx=7) + elif opt['type'] == 'int': + Entry(entry_area, textvariable=var, validate='key', + validatecommand=(self.is_int, '%P') + ).grid(row=row, column=1, sticky=NSEW, padx=7) + + else: + Entry(entry_area, textvariable=var + ).grid(row=row, column=1, sticky=NSEW, padx=7) + return + + def set_extension_value(self, section, opt): + name = opt['name'] + default = opt['default'] + value = opt['var'].get().strip() or default + opt['var'].set(value) + # if self.defaultCfg.has_section(section): + # Currently, always true; if not, indent to return + if (value == default): + return self.ext_userCfg.RemoveOption(section, name) + # set the option + return self.ext_userCfg.SetOption(section, name, value) + + def save_all_changed_extensions(self): + """Save configuration changes to the user config file.""" + has_changes = False + for ext_name in self.extensions: + options = self.extensions[ext_name] + for opt in options: + if self.set_extension_value(ext_name, opt): + has_changes = True + if has_changes: + self.ext_userCfg.Save() + + +help_common = '''\ +When you click either the Apply or Ok buttons, settings in this +dialog that are different from IDLE's default are saved in +a .idlerc directory in your home directory. Except as noted, +these changes apply to all versions of IDLE installed on this +machine. Some do not take affect until IDLE is restarted. +[Cancel] only cancels changes made since the last save. +''' +help_pages = { + 'Highlighting':''' +Highlighting: +The IDLE Dark color theme is new in October 2015. It can only +be used with older IDLE releases if it is saved as a custom +theme, with a different name. +''' +} + + +def is_int(s): + "Return 's is blank or represents an int'" + if not s: + return True + try: + int(s) + return True + except ValueError: + return False + + +class VerticalScrolledFrame(Frame): + """A pure Tkinter vertically scrollable frame. + + * Use the 'interior' attribute to place widgets inside the scrollable frame + * Construct and pack/place/grid normally + * This frame only allows vertical scrolling + """ + def __init__(self, parent, *args, **kw): + Frame.__init__(self, parent, *args, **kw) + + # create a canvas object and a vertical scrollbar for scrolling it + vscrollbar = Scrollbar(self, orient=VERTICAL) + vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE) + canvas = Canvas(self, bd=0, highlightthickness=0, + yscrollcommand=vscrollbar.set, width=240) + canvas.pack(side=LEFT, fill=BOTH, expand=TRUE) + vscrollbar.config(command=canvas.yview) + + # reset the view + canvas.xview_moveto(0) + canvas.yview_moveto(0) + + # create a frame inside the canvas which will be scrolled with it + self.interior = interior = Frame(canvas) + interior_id = canvas.create_window(0, 0, window=interior, anchor=NW) + + # track changes to the canvas and frame width and sync them, + # also updating the scrollbar + def _configure_interior(event): + # update the scrollbars to match the size of the inner frame + size = (interior.winfo_reqwidth(), interior.winfo_reqheight()) + canvas.config(scrollregion="0 0 %s %s" % size) + interior.bind('<Configure>', _configure_interior) + + def _configure_canvas(event): + if interior.winfo_reqwidth() != canvas.winfo_width(): + # update the inner frame's width to fill the canvas + canvas.itemconfigure(interior_id, width=canvas.winfo_width()) + canvas.bind('<Configure>', _configure_canvas) + + return + if __name__ == '__main__': - #test the dialog - root=Tk() - Button(root,text='Dialog', - command=lambda:ConfigDialog(root,'Settings')).pack() - root.instance_dict={} - root.mainloop() + import unittest + unittest.main('idlelib.idle_test.test_configdialog', + verbosity=2, exit=False) + from idlelib.idle_test.htest import run + run(ConfigDialog) diff --git a/Lib/idlelib/configHandler.py b/Lib/idlelib/configHandler.py index a974d54bce..531efb4b3e 100644 --- a/Lib/idlelib/configHandler.py +++ b/Lib/idlelib/configHandler.py @@ -15,13 +15,15 @@ idle. This is to allow IDLE to continue to function in spite of errors in the retrieval of config information. When a default is returned instead of a requested config value, a message is printed to stderr to aid in configuration problem notification and resolution. - """ +# TODOs added Oct 2014, tjr + import os import sys -from idlelib import macosxSupport -from configparser import ConfigParser, NoOptionError, NoSectionError +from configparser import ConfigParser +from tkinter import TkVersion +from tkinter.font import Font, nametofont class InvalidConfigType(Exception): pass class InvalidConfigSet(Exception): pass @@ -36,7 +38,7 @@ class IdleConfParser(ConfigParser): """ cfgFile - string, fully specified configuration file name """ - self.file=cfgFile + self.file = cfgFile ConfigParser.__init__(self, defaults=cfgDefaults, strict=False) def Get(self, section, option, type=None, default=None, raw=False): @@ -44,28 +46,27 @@ class IdleConfParser(ConfigParser): Get an option value for given section/option or return default. If type is specified, return as type. """ + # TODO Use default as fallback, at least if not None + # Should also print Warning(file, section, option). + # Currently may raise ValueError if not self.has_option(section, option): return default - if type=='bool': + if type == 'bool': return self.getboolean(section, option) - elif type=='int': + elif type == 'int': return self.getint(section, option) else: return self.get(section, option, raw=raw) - def GetOptionList(self,section): - """ - Get an option list for given section - """ + def GetOptionList(self, section): + "Return a list of options for given section, else []." if self.has_section(section): return self.options(section) else: #return a default value return [] def Load(self): - """ - Load the configuration file from disk - """ + "Load the configuration file from disk." self.read(self.file) class IdleUserConfParser(IdleConfParser): @@ -73,61 +74,50 @@ class IdleUserConfParser(IdleConfParser): IdleConfigParser specialised for user configuration handling. """ - def AddSection(self,section): - """ - if section doesn't exist, add it - """ + def AddSection(self, section): + "If section doesn't exist, add it." if not self.has_section(section): self.add_section(section) def RemoveEmptySections(self): - """ - remove any sections that have no options - """ + "Remove any sections that have no options." for section in self.sections(): if not self.GetOptionList(section): self.remove_section(section) def IsEmpty(self): - """ - Remove empty sections and then return 1 if parser has no sections - left, else return 0. - """ + "Return True if no sections after removing empty sections." self.RemoveEmptySections() - if self.sections(): - return 0 - else: - return 1 + return not self.sections() - def RemoveOption(self,section,option): - """ - If section/option exists, remove it. - Returns 1 if option was removed, 0 otherwise. + def RemoveOption(self, section, option): + """Return True if option is removed from section, else False. + + False if either section does not exist or did not have option. """ if self.has_section(section): - return self.remove_option(section,option) + return self.remove_option(section, option) + return False - def SetOption(self,section,option,value): - """ - Sets option to value, adding section if required. - Returns 1 if option was added or changed, otherwise 0. + def SetOption(self, section, option, value): + """Return True if option is added or changed to value, else False. + + Add section if required. False means option already had value. """ - if self.has_option(section,option): - if self.get(section,option)==value: - return 0 + if self.has_option(section, option): + if self.get(section, option) == value: + return False else: - self.set(section,option,value) - return 1 + self.set(section, option, value) + return True else: if not self.has_section(section): self.add_section(section) - self.set(section,option,value) - return 1 + self.set(section, option, value) + return True def RemoveFile(self): - """ - Removes the user config file from disk if it exists. - """ + "Remove user config file self.file from disk if it exists." if os.path.exists(self.file): os.remove(self.file) @@ -151,62 +141,59 @@ class IdleUserConfParser(IdleConfParser): self.RemoveFile() class IdleConf: - """ - holds config parsers for all idle config files: - default config files - (idle install dir)/config-main.def - (idle install dir)/config-extensions.def - (idle install dir)/config-highlight.def - (idle install dir)/config-keys.def - user config files - (user home dir)/.idlerc/config-main.cfg - (user home dir)/.idlerc/config-extensions.cfg - (user home dir)/.idlerc/config-highlight.cfg - (user home dir)/.idlerc/config-keys.cfg + """Hold config parsers for all idle config files in singleton instance. + + Default config files, self.defaultCfg -- + for config_type in self.config_types: + (idle install dir)/config-{config-type}.def + + User config files, self.userCfg -- + for config_type in self.config_types: + (user home dir)/.idlerc/config-{config-type}.cfg """ def __init__(self): - self.defaultCfg={} - self.userCfg={} - self.cfg={} + self.config_types = ('main', 'extensions', 'highlight', 'keys') + self.defaultCfg = {} + self.userCfg = {} + self.cfg = {} # TODO use to select userCfg vs defaultCfg self.CreateConfigHandlers() self.LoadCfgFiles() - #self.LoadCfg() + def CreateConfigHandlers(self): - """ - set up a dictionary of config parsers for default and user - configurations respectively - """ + "Populate default and user config parser dictionaries." #build idle install path if __name__ != '__main__': # we were imported idleDir=os.path.dirname(__file__) else: # we were exec'ed (for testing only) idleDir=os.path.abspath(sys.path[0]) userDir=self.GetUserCfgDir() - configTypes=('main','extensions','highlight','keys') - defCfgFiles={} - usrCfgFiles={} - for cfgType in configTypes: #build config file names - defCfgFiles[cfgType]=os.path.join(idleDir,'config-'+cfgType+'.def') - usrCfgFiles[cfgType]=os.path.join(userDir,'config-'+cfgType+'.cfg') - for cfgType in configTypes: #create config parsers - self.defaultCfg[cfgType]=IdleConfParser(defCfgFiles[cfgType]) - self.userCfg[cfgType]=IdleUserConfParser(usrCfgFiles[cfgType]) + + defCfgFiles = {} + usrCfgFiles = {} + # TODO eliminate these temporaries by combining loops + for cfgType in self.config_types: #build config file names + defCfgFiles[cfgType] = os.path.join( + idleDir, 'config-' + cfgType + '.def') + usrCfgFiles[cfgType] = os.path.join( + userDir, 'config-' + cfgType + '.cfg') + for cfgType in self.config_types: #create config parsers + self.defaultCfg[cfgType] = IdleConfParser(defCfgFiles[cfgType]) + self.userCfg[cfgType] = IdleUserConfParser(usrCfgFiles[cfgType]) def GetUserCfgDir(self): - """ - Creates (if required) and returns a filesystem directory for storing - user config files. + """Return a filesystem directory for storing user config files. + Creates it if required. """ cfgDir = '.idlerc' userDir = os.path.expanduser('~') if userDir != '~': # expanduser() found user home dir if not os.path.exists(userDir): - warn = ('\n Warning: os.path.expanduser("~") points to\n '+ - userDir+',\n but the path does not exist.\n') + warn = ('\n Warning: os.path.expanduser("~") points to\n ' + + userDir + ',\n but the path does not exist.') try: - sys.stderr.write(warn) + print(warn, file=sys.stderr) except OSError: pass userDir = '~' @@ -218,45 +205,44 @@ class IdleConf: try: os.mkdir(userDir) except OSError: - warn = ('\n Warning: unable to create user config directory\n'+ - userDir+'\n Check path and permissions.\n Exiting!\n\n') - sys.stderr.write(warn) + warn = ('\n Warning: unable to create user config directory\n' + + userDir + '\n Check path and permissions.\n Exiting!\n') + print(warn, file=sys.stderr) raise SystemExit + # TODO continue without userDIr instead of exit return userDir def GetOption(self, configType, section, option, default=None, type=None, warn_on_default=True, raw=False): - """ - Get an option value for given config type and given general - configuration section/option or return a default. If type is specified, - return as type. Firstly the user configuration is checked, with a - fallback to the default configuration, and a final 'catch all' - fallback to a useable passed-in default if the option isn't present in - either the user or the default configuration. - configType must be one of ('main','extensions','highlight','keys') - If a default is returned, and warn_on_default is True, a warning is - printed to stderr. + """Return a value for configType section option, or default. + If type is not None, return a value of that type. Also pass raw + to the config parser. First try to return a valid value + (including type) from a user configuration. If that fails, try + the default configuration. If that fails, return default, with a + default of None. + + Warn if either user or default configurations have an invalid value. + Warn if default is returned and warn_on_default is True. """ try: - if self.userCfg[configType].has_option(section,option): + if self.userCfg[configType].has_option(section, option): return self.userCfg[configType].Get(section, option, type=type, raw=raw) except ValueError: warning = ('\n Warning: configHandler.py - IdleConf.GetOption -\n' ' invalid %r value for configuration option %r\n' - ' from section %r: %r\n' % + ' from section %r: %r' % (type, option, section, - self.userCfg[configType].Get(section, option, - raw=raw))) + self.userCfg[configType].Get(section, option, raw=raw))) try: - sys.stderr.write(warning) + print(warning, file=sys.stderr) except OSError: pass try: if self.defaultCfg[configType].has_option(section,option): - return self.defaultCfg[configType].Get(section, option, - type=type, raw=raw) + return self.defaultCfg[configType].Get( + section, option, type=type, raw=raw) except ValueError: pass #returning default, print warning @@ -264,29 +250,28 @@ class IdleConf: warning = ('\n Warning: configHandler.py - IdleConf.GetOption -\n' ' problem retrieving configuration option %r\n' ' from section %r.\n' - ' returning default value: %r\n' % + ' returning default value: %r' % (option, section, default)) try: - sys.stderr.write(warning) + print(warning, file=sys.stderr) except OSError: pass return default + def SetOption(self, configType, section, option, value): - """In user's config file, set section's option to value. - """ + """Set section option to value in user config file.""" self.userCfg[configType].SetOption(section, option, value) def GetSectionList(self, configSet, configType): - """ - Get a list of sections from either the user or default config for - the given config type. + """Return sections for configSet configType configuration. + configSet must be either 'user' or 'default' - configType must be one of ('main','extensions','highlight','keys') + configType must be in self.config_types. """ - if not (configType in ('main','extensions','highlight','keys')): + if not (configType in self.config_types): raise InvalidConfigType('Invalid configType specified') if configSet == 'user': - cfgParser=self.userCfg[configType] + cfgParser = self.userCfg[configType] elif configSet == 'default': cfgParser=self.defaultCfg[configType] else: @@ -294,25 +279,27 @@ class IdleConf: return cfgParser.sections() def GetHighlight(self, theme, element, fgBg=None): - """ - return individual highlighting theme elements. - fgBg - string ('fg'or'bg') or None, if None return a dictionary - containing fg and bg colours (appropriate for passing to Tkinter in, - e.g., a tag_config call), otherwise fg or bg colour only as specified. + """Return individual theme element highlight color(s). + + fgBg - string ('fg' or 'bg') or None. + If None, return a dictionary containing fg and bg colors with + keys 'foreground' and 'background'. Otherwise, only return + fg or bg color, as specified. Colors are intended to be + appropriate for passing to Tkinter in, e.g., a tag_config call). """ if self.defaultCfg['highlight'].has_section(theme): - themeDict=self.GetThemeDict('default',theme) + themeDict = self.GetThemeDict('default', theme) else: - themeDict=self.GetThemeDict('user',theme) - fore=themeDict[element+'-foreground'] - if element=='cursor': #there is no config value for cursor bg - back=themeDict['normal-background'] + themeDict = self.GetThemeDict('user', theme) + fore = themeDict[element + '-foreground'] + if element == 'cursor': # There is no config value for cursor bg + back = themeDict['normal-background'] else: - back=themeDict[element+'-background'] - highlight={"foreground": fore,"background": back} - if not fgBg: #return dict of both colours + back = themeDict[element + '-background'] + highlight = {"foreground": fore, "background": back} + if not fgBg: # Return dict of both colors return highlight - else: #return specified colour only + else: # Return specified color only if fgBg == 'fg': return highlight["foreground"] if fgBg == 'bg': @@ -320,26 +307,26 @@ class IdleConf: else: raise InvalidFgBg('Invalid fgBg specified') - def GetThemeDict(self,type,themeName): - """ + def GetThemeDict(self, type, themeName): + """Return {option:value} dict for elements in themeName. + type - string, 'default' or 'user' theme type themeName - string, theme name - Returns a dictionary which holds {option:value} for each element - in the specified theme. Values are loaded over a set of ultimate last - fallback defaults to guarantee that all theme elements are present in - a newly created theme. + Values are loaded over ultimate fallback defaults to guarantee + that all theme elements are present in a newly created theme. """ if type == 'user': - cfgParser=self.userCfg['highlight'] + cfgParser = self.userCfg['highlight'] elif type == 'default': - cfgParser=self.defaultCfg['highlight'] + cfgParser = self.defaultCfg['highlight'] else: raise InvalidTheme('Invalid theme type specified') - #foreground and background values are provded for each theme element - #(apart from cursor) even though all these values are not yet used - #by idle, to allow for their use in the future. Default values are - #generally black and white. - theme={ 'normal-foreground':'#000000', + # Provide foreground and background colors for each theme + # element (other than cursor) even though some values are not + # yet used by idle, to allow for their use in the future. + # Default values are generally black and white. + # TODO copy theme from a class attribute. + theme ={'normal-foreground':'#000000', 'normal-background':'#ffffff', 'keyword-foreground':'#000000', 'keyword-background':'#ffffff', @@ -369,52 +356,74 @@ class IdleConf: 'console-foreground':'#000000', 'console-background':'#ffffff' } for element in theme: - if not cfgParser.has_option(themeName,element): - #we are going to return a default, print warning - warning=('\n Warning: configHandler.py - IdleConf.GetThemeDict' + if not cfgParser.has_option(themeName, element): + # Print warning that will return a default color + warning = ('\n Warning: configHandler.IdleConf.GetThemeDict' ' -\n problem retrieving theme element %r' '\n from theme %r.\n' - ' returning default value: %r\n' % + ' returning default color: %r' % (element, themeName, theme[element])) try: - sys.stderr.write(warning) + print(warning, file=sys.stderr) except OSError: pass - colour=cfgParser.Get(themeName,element,default=theme[element]) - theme[element]=colour + theme[element] = cfgParser.Get( + themeName, element, default=theme[element]) return theme def CurrentTheme(self): - """ - Returns the name of the currently active theme - """ - return self.GetOption('main','Theme','name',default='') + """Return the name of the currently active text color theme. + + idlelib.config-main.def includes this section + [Theme] + default= 1 + name= IDLE Classic + name2= + # name2 set in user config-main.cfg for themes added after 2015 Oct 1 + + Item name2 is needed because setting name to a new builtin + causes older IDLEs to display multiple error messages or quit. + See https://bugs.python.org/issue25313. + When default = True, name2 takes precedence over name, + while older IDLEs will just use name. + """ + default = self.GetOption('main', 'Theme', 'default', + type='bool', default=True) + if default: + theme = self.GetOption('main', 'Theme', 'name2', default='') + if default and not theme or not default: + theme = self.GetOption('main', 'Theme', 'name', default='') + source = self.defaultCfg if default else self.userCfg + if source['highlight'].has_section(theme): + return theme + else: + return "IDLE Classic" def CurrentKeys(self): - """ - Returns the name of the currently active key set - """ - return self.GetOption('main','Keys','name',default='') + "Return the name of the currently active key set." + return self.GetOption('main', 'Keys', 'name', default='') def GetExtensions(self, active_only=True, editor_only=False, shell_only=False): + """Return extensions in default and user config-extensions files. + + If active_only True, only return active (enabled) extensions + and optionally only editor or shell extensions. + If active_only False, return all extensions. """ - Gets a list of all idle extensions declared in the config files. - active_only - boolean, if true only return active (enabled) extensions - """ - extns=self.RemoveKeyBindNames( - self.GetSectionList('default','extensions')) - userExtns=self.RemoveKeyBindNames( - self.GetSectionList('user','extensions')) + extns = self.RemoveKeyBindNames( + self.GetSectionList('default', 'extensions')) + userExtns = self.RemoveKeyBindNames( + self.GetSectionList('user', 'extensions')) for extn in userExtns: if extn not in extns: #user has added own extension extns.append(extn) if active_only: - activeExtns=[] + activeExtns = [] for extn in extns: if self.GetOption('extensions', extn, 'enable', default=True, type='bool'): #the extension is enabled - if editor_only or shell_only: + if editor_only or shell_only: # TODO if both, contradictory if editor_only: option = "enable_editor" else: @@ -429,106 +438,110 @@ class IdleConf: else: return extns - def RemoveKeyBindNames(self,extnNameList): - #get rid of keybinding section names - names=extnNameList - kbNameIndicies=[] + def RemoveKeyBindNames(self, extnNameList): + "Return extnNameList with keybinding section names removed." + # TODO Easier to return filtered copy with list comp + names = extnNameList + kbNameIndicies = [] for name in names: if name.endswith(('_bindings', '_cfgBindings')): kbNameIndicies.append(names.index(name)) - kbNameIndicies.sort() - kbNameIndicies.reverse() + kbNameIndicies.sort(reverse=True) for index in kbNameIndicies: #delete each keybinding section name del(names[index]) return names - def GetExtnNameForEvent(self,virtualEvent): - """ - Returns the name of the extension that virtualEvent is bound in, or - None if not bound in any extension. - virtualEvent - string, name of the virtual event to test for, without - the enclosing '<< >>' + def GetExtnNameForEvent(self, virtualEvent): + """Return the name of the extension binding virtualEvent, or None. + + virtualEvent - string, name of the virtual event to test for, + without the enclosing '<< >>' """ - extName=None - vEvent='<<'+virtualEvent+'>>' + extName = None + vEvent = '<<' + virtualEvent + '>>' for extn in self.GetExtensions(active_only=0): for event in self.GetExtensionKeys(extn): if event == vEvent: - extName=extn + extName = extn # TODO return here? return extName - def GetExtensionKeys(self,extensionName): - """ - returns a dictionary of the configurable keybindings for a particular - extension,as they exist in the dictionary returned by GetCurrentKeySet; - that is, where previously used bindings are disabled. + def GetExtensionKeys(self, extensionName): + """Return dict: {configurable extensionName event : active keybinding}. + + Events come from default config extension_cfgBindings section. + Keybindings come from GetCurrentKeySet() active key dict, + where previously used bindings are disabled. """ - keysName=extensionName+'_cfgBindings' - activeKeys=self.GetCurrentKeySet() - extKeys={} + keysName = extensionName + '_cfgBindings' + activeKeys = self.GetCurrentKeySet() + extKeys = {} if self.defaultCfg['extensions'].has_section(keysName): - eventNames=self.defaultCfg['extensions'].GetOptionList(keysName) + eventNames = self.defaultCfg['extensions'].GetOptionList(keysName) for eventName in eventNames: - event='<<'+eventName+'>>' - binding=activeKeys[event] - extKeys[event]=binding + event = '<<' + eventName + '>>' + binding = activeKeys[event] + extKeys[event] = binding return extKeys def __GetRawExtensionKeys(self,extensionName): + """Return dict {configurable extensionName event : keybinding list}. + + Events come from default config extension_cfgBindings section. + Keybindings list come from the splitting of GetOption, which + tries user config before default config. """ - returns a dictionary of the configurable keybindings for a particular - extension, as defined in the configuration files, or an empty dictionary - if no bindings are found - """ - keysName=extensionName+'_cfgBindings' - extKeys={} + keysName = extensionName+'_cfgBindings' + extKeys = {} if self.defaultCfg['extensions'].has_section(keysName): - eventNames=self.defaultCfg['extensions'].GetOptionList(keysName) + eventNames = self.defaultCfg['extensions'].GetOptionList(keysName) for eventName in eventNames: - binding=self.GetOption('extensions',keysName, - eventName,default='').split() - event='<<'+eventName+'>>' - extKeys[event]=binding + binding = self.GetOption( + 'extensions', keysName, eventName, default='').split() + event = '<<' + eventName + '>>' + extKeys[event] = binding return extKeys - def GetExtensionBindings(self,extensionName): - """ - Returns a dictionary of all the event bindings for a particular - extension. The configurable keybindings are returned as they exist in - the dictionary returned by GetCurrentKeySet; that is, where re-used - keybindings are disabled. + def GetExtensionBindings(self, extensionName): + """Return dict {extensionName event : active or defined keybinding}. + + Augment self.GetExtensionKeys(extensionName) with mapping of non- + configurable events (from default config) to GetOption splits, + as in self.__GetRawExtensionKeys. """ - bindsName=extensionName+'_bindings' - extBinds=self.GetExtensionKeys(extensionName) + bindsName = extensionName + '_bindings' + extBinds = self.GetExtensionKeys(extensionName) #add the non-configurable bindings if self.defaultCfg['extensions'].has_section(bindsName): - eventNames=self.defaultCfg['extensions'].GetOptionList(bindsName) + eventNames = self.defaultCfg['extensions'].GetOptionList(bindsName) for eventName in eventNames: - binding=self.GetOption('extensions',bindsName, - eventName,default='').split() - event='<<'+eventName+'>>' - extBinds[event]=binding + binding = self.GetOption( + 'extensions', bindsName, eventName, default='').split() + event = '<<' + eventName + '>>' + extBinds[event] = binding return extBinds def GetKeyBinding(self, keySetName, eventStr): + """Return the keybinding list for keySetName eventStr. + + keySetName - name of key binding set (config-keys section). + eventStr - virtual event, including brackets, as in '<<event>>'. """ - returns the keybinding for a specific event. - keySetName - string, name of key binding set - eventStr - string, the virtual event we want the binding for, - represented as a string, eg. '<<event>>' - """ - eventName=eventStr[2:-2] #trim off the angle brackets - binding=self.GetOption('keys',keySetName,eventName,default='').split() + eventName = eventStr[2:-2] #trim off the angle brackets + binding = self.GetOption('keys', keySetName, eventName, default='').split() return binding def GetCurrentKeySet(self): + "Return CurrentKeys with 'darwin' modifications." result = self.GetKeySet(self.CurrentKeys()) - if macosxSupport.runningAsOSXApp(): - # We're using AquaTk, replace all keybingings that use the - # Alt key by ones that use the Option key because the former - # don't work reliably. + if sys.platform == "darwin": + # OS X Tk variants do not support the "Alt" keyboard modifier. + # So replace all keybingings that use "Alt" with ones that + # use the "Option" keyboard modifier. + # TODO (Ned?): the "Option" modifier does not work properly for + # Cocoa Tk and XQuartz Tk so we should not use it + # in default OS X KeySets. for k, v in result.items(): v2 = [ x.replace('<Alt-', '<Option-') for x in v ] if v != v2: @@ -536,40 +549,43 @@ class IdleConf: return result - def GetKeySet(self,keySetName): - """ - Returns a dictionary of: all requested core keybindings, plus the - keybindings for all currently active extensions. If a binding defined - in an extension is already in use, that binding is disabled. + def GetKeySet(self, keySetName): + """Return event-key dict for keySetName core plus active extensions. + + If a binding defined in an extension is already in use, the + extension binding is disabled by being set to '' """ - keySet=self.GetCoreKeys(keySetName) - activeExtns=self.GetExtensions(active_only=1) + keySet = self.GetCoreKeys(keySetName) + activeExtns = self.GetExtensions(active_only=1) for extn in activeExtns: - extKeys=self.__GetRawExtensionKeys(extn) + extKeys = self.__GetRawExtensionKeys(extn) if extKeys: #the extension defines keybindings for event in extKeys: if extKeys[event] in keySet.values(): #the binding is already in use - extKeys[event]='' #disable this binding - keySet[event]=extKeys[event] #add binding + extKeys[event] = '' #disable this binding + keySet[event] = extKeys[event] #add binding return keySet - def IsCoreBinding(self,virtualEvent): - """ - returns true if the virtual event is bound in the core idle keybindings. - virtualEvent - string, name of the virtual event to test for, without - the enclosing '<< >>' + def IsCoreBinding(self, virtualEvent): + """Return True if the virtual event is one of the core idle key events. + + virtualEvent - string, name of the virtual event to test for, + without the enclosing '<< >>' """ return ('<<'+virtualEvent+'>>') in self.GetCoreKeys() +# TODO make keyBindins a file or class attribute used for test above +# and copied in function below + def GetCoreKeys(self, keySetName=None): - """ - returns the requested set of core keybindings, with fallbacks if - required. - Keybindings loaded from the config file(s) are loaded _over_ these - defaults, so if there is a problem getting any core binding there will - be an 'ultimate last resort fallback' to the CUA-ish bindings - defined here. + """Return dict of core virtual-key keybindings for keySetName. + + The default keySetName None corresponds to the keyBindings base + dict. If keySetName is not None, bindings from the config + file(s) are loaded _over_ these defaults, so if there is a + problem getting any core binding there will be an 'ultimate last + resort fallback' to the CUA-ish bindings defined here. """ keyBindings={ '<<copy>>': ['<Control-c>', '<Control-C>'], @@ -624,22 +640,24 @@ class IdleConf: } if keySetName: for event in keyBindings: - binding=self.GetKeyBinding(keySetName,event) + binding = self.GetKeyBinding(keySetName, event) if binding: - keyBindings[event]=binding + keyBindings[event] = binding else: #we are going to return a default, print warning warning=('\n Warning: configHandler.py - IdleConf.GetCoreKeys' ' -\n problem retrieving key binding for event %r' '\n from key set %r.\n' - ' returning default value: %r\n' % + ' returning default value: %r' % (event, keySetName, keyBindings[event])) try: - sys.stderr.write(warning) + print(warning, file=sys.stderr) except OSError: pass return keyBindings - def GetExtraHelpSourceList(self,configSet): - """Fetch list of extra help sources from a given configSet. + + def GetExtraHelpSourceList(self, configSet): + """Return list of extra help sources from a given configSet. + Valid configSets are 'user' or 'default'. Return a list of tuples of the form (menu_item , path_to_help_file , option), or return the empty list. 'option' is the sequence number of the help resource. 'option' @@ -647,19 +665,19 @@ class IdleConf: therefore the returned list must be sorted by 'option'. """ - helpSources=[] - if configSet=='user': - cfgParser=self.userCfg['main'] - elif configSet=='default': - cfgParser=self.defaultCfg['main'] + helpSources = [] + if configSet == 'user': + cfgParser = self.userCfg['main'] + elif configSet == 'default': + cfgParser = self.defaultCfg['main'] else: raise InvalidConfigSet('Invalid configSet specified') options=cfgParser.GetOptionList('HelpFiles') for option in options: - value=cfgParser.Get('HelpFiles',option,default=';') - if value.find(';')==-1: #malformed config entry with no ';' - menuItem='' #make these empty - helpPath='' #so value won't be added to list + value=cfgParser.Get('HelpFiles', option, default=';') + if value.find(';') == -1: #malformed config entry with no ';' + menuItem = '' #make these empty + helpPath = '' #so value won't be added to list else: #config entry contains ';' as expected value=value.split(';') menuItem=value[0].strip() @@ -670,47 +688,73 @@ class IdleConf: return helpSources def GetAllExtraHelpSourcesList(self): + """Return a list of the details of all additional help sources. + + Tuples in the list are those of GetExtraHelpSourceList. """ - Returns a list of tuples containing the details of all additional help - sources configured, or an empty list if there are none. Tuples are of - the format returned by GetExtraHelpSourceList. - """ - allHelpSources=( self.GetExtraHelpSourceList('default')+ + allHelpSources = (self.GetExtraHelpSourceList('default') + self.GetExtraHelpSourceList('user') ) return allHelpSources + def GetFont(self, root, configType, section): + """Retrieve a font from configuration (font, font-size, font-bold) + Intercept the special value 'TkFixedFont' and substitute + the actual font, factoring in some tweaks if needed for + appearance sakes. + + The 'root' parameter can normally be any valid Tkinter widget. + + Return a tuple (family, size, weight) suitable for passing + to tkinter.Font + """ + family = self.GetOption(configType, section, 'font', default='courier') + size = self.GetOption(configType, section, 'font-size', type='int', + default='10') + bold = self.GetOption(configType, section, 'font-bold', default=0, + type='bool') + if (family == 'TkFixedFont'): + if TkVersion < 8.5: + family = 'Courier' + else: + f = Font(name='TkFixedFont', exists=True, root=root) + actualFont = Font.actual(f) + family = actualFont['family'] + size = actualFont['size'] + if size < 0: + size = 10 # if font in pixels, ignore actual size + bold = actualFont['weight']=='bold' + return (family, size, 'bold' if bold else 'normal') + def LoadCfgFiles(self): - """ - load all configuration files. - """ + "Load all configuration files." for key in self.defaultCfg: self.defaultCfg[key].Load() self.userCfg[key].Load() #same keys def SaveUserCfgFiles(self): - """ - write all loaded user configuration files back to disk - """ + "Write all loaded user configuration files to disk." for key in self.userCfg: self.userCfg[key].Save() -idleConf=IdleConf() +idleConf = IdleConf() + +# TODO Revise test output, write expanded unittest ### module test if __name__ == '__main__': def dumpCfg(cfg): - print('\n',cfg,'\n') + print('\n', cfg, '\n') for key in cfg: - sections=cfg[key].sections() + sections = cfg[key].sections() print(key) print(sections) for section in sections: - options=cfg[key].options(section) + options = cfg[key].options(section) print(section) print(options) for option in options: - print(option, '=', cfg[key].Get(section,option)) + print(option, '=', cfg[key].Get(section, option)) dumpCfg(idleConf.defaultCfg) dumpCfg(idleConf.userCfg) - print(idleConf.userCfg['main'].Get('Theme','name')) + print(idleConf.userCfg['main'].Get('Theme', 'name')) #print idleConf.userCfg['highlight'].GetDefHighlight('Foo','normal') diff --git a/Lib/idlelib/configHelpSourceEdit.py b/Lib/idlelib/configHelpSourceEdit.py index 2ccb400575..242b08db56 100644 --- a/Lib/idlelib/configHelpSourceEdit.py +++ b/Lib/idlelib/configHelpSourceEdit.py @@ -8,13 +8,14 @@ import tkinter.messagebox as tkMessageBox import tkinter.filedialog as tkFileDialog class GetHelpSourceDialog(Toplevel): - def __init__(self, parent, title, menuItem='', filePath=''): + def __init__(self, parent, title, menuItem='', filePath='', _htest=False): """Get menu entry and url/ local file location for Additional Help User selects a name for the Help resource and provides a web url or a local file as its source. The user can enter a url or browse for the file. + _htest - bool, change box location when running htest """ Toplevel.__init__(self, parent) self.configure(borderwidth=5) @@ -31,12 +32,14 @@ class GetHelpSourceDialog(Toplevel): self.withdraw() #hide while setting geometry #needs to be done here so that the winfo_reqwidth is valid self.update_idletasks() - #centre dialog over parent: - self.geometry("+%d+%d" % - ((parent.winfo_rootx() + ((parent.winfo_width()/2) - -(self.winfo_reqwidth()/2)), - parent.winfo_rooty() + ((parent.winfo_height()/2) - -(self.winfo_reqheight()/2))))) + #centre dialog over parent. below parent if running htest. + self.geometry( + "+%d+%d" % ( + parent.winfo_rootx() + + (parent.winfo_width()/2 - self.winfo_reqwidth()/2), + parent.winfo_rooty() + + ((parent.winfo_height()/2 - self.winfo_reqheight()/2) + if not _htest else 150))) self.deiconify() #geometry set, unhide self.bind('<Return>', self.Ok) self.wait_window() @@ -159,11 +162,5 @@ class GetHelpSourceDialog(Toplevel): self.destroy() if __name__ == '__main__': - #test the dialog - root = Tk() - def run(): - keySeq = '' - dlg = GetHelpSourceDialog(root, 'Get Help Source') - print(dlg.result) - Button(root,text='Dialog', command=run).pack() - root.mainloop() + from idlelib.idle_test.htest import run + run(GetHelpSourceDialog) diff --git a/Lib/idlelib/configSectionNameDialog.py b/Lib/idlelib/configSectionNameDialog.py index b05e38e9d3..5137836981 100644 --- a/Lib/idlelib/configSectionNameDialog.py +++ b/Lib/idlelib/configSectionNameDialog.py @@ -8,10 +8,11 @@ from tkinter import * import tkinter.messagebox as tkMessageBox class GetCfgSectionNameDialog(Toplevel): - def __init__(self, parent, title, message, used_names): + def __init__(self, parent, title, message, used_names, _htest=False): """ message - string, informational message to display used_names - string collection, names already in use for validity check + _htest - bool, change box location when running htest """ Toplevel.__init__(self, parent) self.configure(borderwidth=5) @@ -30,11 +31,12 @@ class GetCfgSectionNameDialog(Toplevel): self.messageInfo.config(width=self.frameMain.winfo_reqwidth()) self.geometry( "+%d+%d" % ( - parent.winfo_rootx() + - (parent.winfo_width()/2 - self.winfo_reqwidth()/2), - parent.winfo_rooty() + - (parent.winfo_height()/2 - self.winfo_reqheight()/2) - ) ) #centre dialog over parent + parent.winfo_rootx() + + (parent.winfo_width()/2 - self.winfo_reqwidth()/2), + parent.winfo_rooty() + + ((parent.winfo_height()/2 - self.winfo_reqheight()/2) + if not _htest else 100) + ) ) #centre dialog over parent (or below htest box) self.deiconify() #geometry set, unhide self.wait_window() @@ -92,15 +94,5 @@ if __name__ == '__main__': import unittest unittest.main('idlelib.idle_test.test_config_name', verbosity=2, exit=False) - # also human test the dialog - root = Tk() - def run(): - dlg=GetCfgSectionNameDialog(root,'Get Name', - "After the text entered with [Ok] is stripped, <nothing>, " - "'abc', or more that 30 chars are errors. " - "Close with a valid entry (printed), [Cancel], or [X]", - {'abc'}) - print(dlg.result) - Message(root, text='').pack() # will be needed for oher dialog tests - Button(root, text='Click to begin dialog test', command=run).pack() - root.mainloop() + from idlelib.idle_test.htest import run + run(GetCfgSectionNameDialog) diff --git a/Lib/idlelib/dynOptionMenuWidget.py b/Lib/idlelib/dynOptionMenuWidget.py index 922de96cea..515b4bafc2 100644 --- a/Lib/idlelib/dynOptionMenuWidget.py +++ b/Lib/idlelib/dynOptionMenuWidget.py @@ -2,16 +2,15 @@ OptionMenu widget modified to allow dynamic menu reconfiguration and setting of highlightthickness """ -from tkinter import OptionMenu -from tkinter import _setit import copy +from tkinter import OptionMenu, _setit, StringVar, Button class DynOptionMenu(OptionMenu): """ unlike OptionMenu, our kwargs can include highlightthickness """ def __init__(self, master, variable, value, *values, **kwargs): - #get a copy of kwargs before OptionMenu.__init__ munges them + # TODO copy value instead of whole dict kwargsCopy=copy.copy(kwargs) if 'highlightthickness' in list(kwargs.keys()): del(kwargs['highlightthickness']) @@ -33,3 +32,26 @@ class DynOptionMenu(OptionMenu): command=_setit(self.variable,item,self.command)) if value: self.variable.set(value) + +def _dyn_option_menu(parent): # htest # + from tkinter import Toplevel + + top = Toplevel() + top.title("Tets dynamic option menu") + top.geometry("200x100+%d+%d" % (parent.winfo_rootx() + 200, + parent.winfo_rooty() + 150)) + top.focus_set() + + var = StringVar(top) + var.set("Old option set") #Set the default value + dyn = DynOptionMenu(top,var, "old1","old2","old3","old4") + dyn.pack() + + def update(): + dyn.SetMenu(["new1","new2","new3","new4"], value="new option set") + button = Button(top, text="Change option set", command=update) + button.pack() + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(_dyn_option_menu) diff --git a/Lib/idlelib/help.html b/Lib/idlelib/help.html new file mode 100644 index 0000000000..2189fd4cf0 --- /dev/null +++ b/Lib/idlelib/help.html @@ -0,0 +1,709 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + + <title>25.5. IDLE — Python 3.4.3 documentation</title> + + <link rel="stylesheet" href="../_static/pydoctheme.css" type="text/css" /> + <link rel="stylesheet" href="../_static/pygments.css" type="text/css" /> + + <script type="text/javascript"> + var DOCUMENTATION_OPTIONS = { + URL_ROOT: '../', + VERSION: '3.4.3', + COLLAPSE_INDEX: false, + FILE_SUFFIX: '.html', + HAS_SOURCE: true + }; + </script> + <script type="text/javascript" src="../_static/jquery.js"></script> + <script type="text/javascript" src="../_static/underscore.js"></script> + <script type="text/javascript" src="../_static/doctools.js"></script> + <script type="text/javascript" src="../_static/sidebar.js"></script> + <link rel="search" type="application/opensearchdescription+xml" + title="Search within Python 3.4.3 documentation" + href="../_static/opensearch.xml"/> + <link rel="author" title="About these documents" href="../about.html" /> + <link rel="copyright" title="Copyright" href="../copyright.html" /> + <link rel="top" title="Python 3.4.3 documentation" href="../index.html" /> + <link rel="up" title="25. Graphical User Interfaces with Tk" href="tk.html" /> + <link rel="next" title="25.6. Other Graphical User Interface Packages" href="othergui.html" /> + <link rel="prev" title="25.4. tkinter.scrolledtext — Scrolled Text Widget" href="tkinter.scrolledtext.html" /> + <link rel="shortcut icon" type="image/png" href="../_static/py.png" /> + <script type="text/javascript" src="../_static/copybutton.js"></script> + + + + + </head> + <body> + <div class="related"> + <h3>Navigation</h3> + <ul> + <li class="right" style="margin-right: 10px"> + <a href="../genindex.html" title="General Index" + accesskey="I">index</a></li> + <li class="right" > + <a href="../py-modindex.html" title="Python Module Index" + >modules</a> |</li> + <li class="right" > + <a href="othergui.html" title="25.6. Other Graphical User Interface Packages" + accesskey="N">next</a> |</li> + <li class="right" > + <a href="tkinter.scrolledtext.html" title="25.4. tkinter.scrolledtext — Scrolled Text Widget" + accesskey="P">previous</a> |</li> + <li><img src="../_static/py.png" alt="" + style="vertical-align: middle; margin-top: -1px"/></li> + <li><a href="https://www.python.org/">Python</a> »</li> + <li> + <a href="../index.html">3.4.3 Documentation</a> » + </li> + + <li><a href="index.html" >The Python Standard Library</a> »</li> + <li><a href="tk.html" accesskey="U">25. Graphical User Interfaces with Tk</a> »</li> + </ul> + </div> + + <div class="document"> + <div class="documentwrapper"> + <div class="bodywrapper"> + <div class="body"> + + <div class="section" id="idle"> +<span id="id1"></span><h1>25.5. IDLE<a class="headerlink" href="#idle" title="Permalink to this headline">¶</a></h1> +<p id="index-0">IDLE is Python’s Integrated Development and Learning Environment.</p> +<p>IDLE has the following features:</p> +<ul class="simple"> +<li>coded in 100% pure Python, using the <a class="reference internal" href="tkinter.html#module-tkinter" title="tkinter: Interface to Tcl/Tk for graphical user interfaces"><tt class="xref py py-mod docutils literal"><span class="pre">tkinter</span></tt></a> GUI toolkit</li> +<li>cross-platform: works mostly the same on Windows, Unix, and Mac OS X</li> +<li>Python shell window (interactive interpreter) with colorizing +of code input, output, and error messages</li> +<li>multi-window text editor with multiple undo, Python colorizing, +smart indent, call tips, auto completion, and other features</li> +<li>search within any window, replace within editor windows, and search +through multiple files (grep)</li> +<li>debugger with persistent breakpoints, stepping, and viewing +of global and local namespaces</li> +<li>configuration, browsers, and other dialogs</li> +</ul> +<div class="section" id="menus"> +<h2>25.5.1. Menus<a class="headerlink" href="#menus" title="Permalink to this headline">¶</a></h2> +<p>IDLE has two main window types, the Shell window and the Editor window. It is +possible to have multiple editor windows simultaneously. Output windows, such +as used for Edit / Find in Files, are a subtype of edit window. They currently +have the same top menu as Editor windows but a different default title and +context menu.</p> +<p>IDLE’s menus dynamically change based on which window is currently selected. +Each menu documented below indicates which window type it is associated with.</p> +<div class="section" id="file-menu-shell-and-editor"> +<h3>25.5.1.1. File menu (Shell and Editor)<a class="headerlink" href="#file-menu-shell-and-editor" title="Permalink to this headline">¶</a></h3> +<dl class="docutils"> +<dt>New File</dt> +<dd>Create a new file editing window.</dd> +<dt>Open...</dt> +<dd>Open an existing file with an Open dialog.</dd> +<dt>Recent Files</dt> +<dd>Open a list of recent files. Click one to open it.</dd> +<dt>Open Module...</dt> +<dd>Open an existing module (searches sys.path).</dd> +</dl> +<dl class="docutils" id="index-1"> +<dt>Class Browser</dt> +<dd>Show functions, classes, and methods in the current Editor file in a +tree structure. In the shell, open a module first.</dd> +<dt>Path Browser</dt> +<dd>Show sys.path directories, modules, functions, classes and methods in a +tree structure.</dd> +<dt>Save</dt> +<dd>Save the current window to the associated file, if there is one. Windows +that have been changed since being opened or last saved have a * before +and after the window title. If there is no associated file, +do Save As instead.</dd> +<dt>Save As...</dt> +<dd>Save the current window with a Save As dialog. The file saved becomes the +new associated file for the window.</dd> +<dt>Save Copy As...</dt> +<dd>Save the current window to different file without changing the associated +file.</dd> +<dt>Print Window</dt> +<dd>Print the current window to the default printer.</dd> +<dt>Close</dt> +<dd>Close the current window (ask to save if unsaved).</dd> +<dt>Exit</dt> +<dd>Close all windows and quit IDLE (ask to save unsaved windows).</dd> +</dl> +</div> +<div class="section" id="edit-menu-shell-and-editor"> +<h3>25.5.1.2. Edit menu (Shell and Editor)<a class="headerlink" href="#edit-menu-shell-and-editor" title="Permalink to this headline">¶</a></h3> +<dl class="docutils"> +<dt>Undo</dt> +<dd>Undo the last change to the current window. A maximum of 1000 changes may +be undone.</dd> +<dt>Redo</dt> +<dd>Redo the last undone change to the current window.</dd> +<dt>Cut</dt> +<dd>Copy selection into the system-wide clipboard; then delete the selection.</dd> +<dt>Copy</dt> +<dd>Copy selection into the system-wide clipboard.</dd> +<dt>Paste</dt> +<dd>Insert contents of the system-wide clipboard into the current window.</dd> +</dl> +<p>The clipboard functions are also available in context menus.</p> +<dl class="docutils"> +<dt>Select All</dt> +<dd>Select the entire contents of the current window.</dd> +<dt>Find...</dt> +<dd>Open a search dialog with many options</dd> +<dt>Find Again</dt> +<dd>Repeat the last search, if there is one.</dd> +<dt>Find Selection</dt> +<dd>Search for the currently selected string, if there is one.</dd> +<dt>Find in Files...</dt> +<dd>Open a file search dialog. Put results in an new output window.</dd> +<dt>Replace...</dt> +<dd>Open a search-and-replace dialog.</dd> +<dt>Go to Line</dt> +<dd>Move cursor to the line number requested and make that line visible.</dd> +<dt>Show Completions</dt> +<dd>Open a scrollable list allowing selection of keywords and attributes. See +Completions in the Tips sections below.</dd> +<dt>Expand Word</dt> +<dd>Expand a prefix you have typed to match a full word in the same window; +repeat to get a different expansion.</dd> +<dt>Show call tip</dt> +<dd>After an unclosed parenthesis for a function, open a small window with +function parameter hints.</dd> +<dt>Show surrounding parens</dt> +<dd>Highlight the surrounding parenthesis.</dd> +</dl> +</div> +<div class="section" id="format-menu-editor-window-only"> +<h3>25.5.1.3. Format menu (Editor window only)<a class="headerlink" href="#format-menu-editor-window-only" title="Permalink to this headline">¶</a></h3> +<dl class="docutils"> +<dt>Indent Region</dt> +<dd>Shift selected lines right by the indent width (default 4 spaces).</dd> +<dt>Dedent Region</dt> +<dd>Shift selected lines left by the indent width (default 4 spaces).</dd> +<dt>Comment Out Region</dt> +<dd>Insert ## in front of selected lines.</dd> +<dt>Uncomment Region</dt> +<dd>Remove leading # or ## from selected lines.</dd> +<dt>Tabify Region</dt> +<dd>Turn <em>leading</em> stretches of spaces into tabs. (Note: We recommend using +4 space blocks to indent Python code.)</dd> +<dt>Untabify Region</dt> +<dd>Turn <em>all</em> tabs into the correct number of spaces.</dd> +<dt>Toggle Tabs</dt> +<dd>Open a dialog to switch between indenting with spaces and tabs.</dd> +<dt>New Indent Width</dt> +<dd>Open a dialog to change indent width. The accepted default by the Python +community is 4 spaces.</dd> +<dt>Format Paragraph</dt> +<dd>Reformat the current blank-line-delimited paragraph in comment block or +multiline string or selected line in a string. All lines in the +paragraph will be formatted to less than N columns, where N defaults to 72.</dd> +<dt>Strip trailing whitespace</dt> +<dd>Remove any space characters after the last non-space character of a line.</dd> +</dl> +</div> +<div class="section" id="run-menu-editor-window-only"> +<span id="index-2"></span><h3>25.5.1.4. Run menu (Editor window only)<a class="headerlink" href="#run-menu-editor-window-only" title="Permalink to this headline">¶</a></h3> +<dl class="docutils"> +<dt>Python Shell</dt> +<dd>Open or wake up the Python Shell window.</dd> +<dt>Check Module</dt> +<dd>Check the syntax of the module currently open in the Editor window. If the +module has not been saved IDLE will either prompt the user to save or +autosave, as selected in the General tab of the Idle Settings dialog. If +there is a syntax error, the approximate location is indicated in the +Editor window.</dd> +<dt>Run Module</dt> +<dd>Do Check Module (above). If no error, restart the shell to clean the +environment, then execute the module. Output is displayed in the Shell +window. Note that output requires use of <tt class="docutils literal"><span class="pre">print</span></tt> or <tt class="docutils literal"><span class="pre">write</span></tt>. +When execution is complete, the Shell retains focus and displays a prompt. +At this point, one may interactively explore the result of execution. +This is similar to executing a file with <tt class="docutils literal"><span class="pre">python</span> <span class="pre">-i</span> <span class="pre">file</span></tt> at a command +line.</dd> +</dl> +</div> +<div class="section" id="shell-menu-shell-window-only"> +<h3>25.5.1.5. Shell menu (Shell window only)<a class="headerlink" href="#shell-menu-shell-window-only" title="Permalink to this headline">¶</a></h3> +<dl class="docutils"> +<dt>View Last Restart</dt> +<dd>Scroll the shell window to the last Shell restart.</dd> +<dt>Restart Shell</dt> +<dd>Restart the shell to clean the environment.</dd> +</dl> +</div> +<div class="section" id="debug-menu-shell-window-only"> +<h3>25.5.1.6. Debug menu (Shell window only)<a class="headerlink" href="#debug-menu-shell-window-only" title="Permalink to this headline">¶</a></h3> +<dl class="docutils"> +<dt>Go to File/Line</dt> +<dd>Look on the current line. with the cursor, and the line above for a filename +and line number. If found, open the file if not already open, and show the +line. Use this to view source lines referenced in an exception traceback +and lines found by Find in Files. Also available in the context menu of +the Shell window and Output windows.</dd> +</dl> +<dl class="docutils" id="index-3"> +<dt>Debugger (toggle)</dt> +<dd>When actived, code entered in the Shell or run from an Editor will run +under the debugger. In the Editor, breakpoints can be set with the context +menu. This feature is still incomplete and somewhat experimental.</dd> +<dt>Stack Viewer</dt> +<dd>Show the stack traceback of the last exception in a tree widget, with +access to locals and globals.</dd> +<dt>Auto-open Stack Viewer</dt> +<dd>Toggle automatically opening the stack viewer on an unhandled exception.</dd> +</dl> +</div> +<div class="section" id="options-menu-shell-and-editor"> +<h3>25.5.1.7. Options menu (Shell and Editor)<a class="headerlink" href="#options-menu-shell-and-editor" title="Permalink to this headline">¶</a></h3> +<dl class="docutils"> +<dt>Configure IDLE</dt> +<dd><p class="first">Open a configuration dialog and change preferences for the following: +fonts, indentation, keybindings, text color themes, startup windows and +size, additional help sources, and extensions (see below). On OS X, +open the configuration dialog by selecting Preferences in the application +menu. To use a new built-in color theme (IDLE Dark) with older IDLEs, +save it as a new custom theme.</p> +<p class="last">Non-default user settings are saved in a .idlerc directory in the user’s +home directory. Problems caused by bad user configuration files are solved +by editing or deleting one or more of the files in .idlerc.</p> +</dd> +<dt>Code Context (toggle)(Editor Window only)</dt> +<dd>Open a pane at the top of the edit window which shows the block context +of the code which has scrolled above the top of the window.</dd> +</dl> +</div> +<div class="section" id="window-menu-shell-and-editor"> +<h3>25.5.1.8. Window menu (Shell and Editor)<a class="headerlink" href="#window-menu-shell-and-editor" title="Permalink to this headline">¶</a></h3> +<dl class="docutils"> +<dt>Zoom Height</dt> +<dd>Toggles the window between normal size and maximum height. The initial size +defaults to 40 lines by 80 chars unless changed on the General tab of the +Configure IDLE dialog.</dd> +</dl> +<p>The rest of this menu lists the names of all open windows; select one to bring +it to the foreground (deiconifying it if necessary).</p> +</div> +<div class="section" id="help-menu-shell-and-editor"> +<h3>25.5.1.9. Help menu (Shell and Editor)<a class="headerlink" href="#help-menu-shell-and-editor" title="Permalink to this headline">¶</a></h3> +<dl class="docutils"> +<dt>About IDLE</dt> +<dd>Display version, copyright, license, credits, and more.</dd> +<dt>IDLE Help</dt> +<dd>Display a help file for IDLE detailing the menu options, basic editing and +navigation, and other tips.</dd> +<dt>Python Docs</dt> +<dd>Access local Python documentation, if installed, or start a web browser +and open docs.python.org showing the latest Python documentation.</dd> +<dt>Turtle Demo</dt> +<dd>Run the turtledemo module with example python code and turtle drawings.</dd> +</dl> +<p>Additional help sources may be added here with the Configure IDLE dialog under +the General tab.</p> +</div> +<div class="section" id="context-menus"> +<span id="index-4"></span><h3>25.5.1.10. Context Menus<a class="headerlink" href="#context-menus" title="Permalink to this headline">¶</a></h3> +<p>Open a context menu by right-clicking in a window (Control-click on OS X). +Context menus have the standard clipboard functions also on the Edit menu.</p> +<dl class="docutils"> +<dt>Cut</dt> +<dd>Copy selection into the system-wide clipboard; then delete the selection.</dd> +<dt>Copy</dt> +<dd>Copy selection into the system-wide clipboard.</dd> +<dt>Paste</dt> +<dd>Insert contents of the system-wide clipboard into the current window.</dd> +</dl> +<p>Editor windows also have breakpoint functions. Lines with a breakpoint set are +specially marked. Breakpoints only have an effect when running under the +debugger. Breakpoints for a file are saved in the user’s .idlerc directory.</p> +<dl class="docutils"> +<dt>Set Breakpoint</dt> +<dd>Set a breakpoint on the current line.</dd> +<dt>Clear Breakpoint</dt> +<dd>Clear the breakpoint on that line.</dd> +</dl> +<p>Shell and Output windows have the following.</p> +<dl class="docutils"> +<dt>Go to file/line</dt> +<dd>Same as in Debug menu.</dd> +</dl> +</div> +</div> +<div class="section" id="editing-and-navigation"> +<h2>25.5.2. Editing and navigation<a class="headerlink" href="#editing-and-navigation" title="Permalink to this headline">¶</a></h2> +<p>In this section, ‘C’ refers to the <tt class="kbd docutils literal"><span class="pre">Control</span></tt> key on Windows and Unix and +the <tt class="kbd docutils literal"><span class="pre">Command</span></tt> key on Mac OSX.</p> +<ul> +<li><p class="first"><tt class="kbd docutils literal"><span class="pre">Backspace</span></tt> deletes to the left; <tt class="kbd docutils literal"><span class="pre">Del</span></tt> deletes to the right</p> +</li> +<li><p class="first"><tt class="kbd docutils literal"><span class="pre">C-Backspace</span></tt> delete word left; <tt class="kbd docutils literal"><span class="pre">C-Del</span></tt> delete word to the right</p> +</li> +<li><p class="first">Arrow keys and <tt class="kbd docutils literal"><span class="pre">Page</span> <span class="pre">Up</span></tt>/<tt class="kbd docutils literal"><span class="pre">Page</span> <span class="pre">Down</span></tt> to move around</p> +</li> +<li><p class="first"><tt class="kbd docutils literal"><span class="pre">C-LeftArrow</span></tt> and <tt class="kbd docutils literal"><span class="pre">C-RightArrow</span></tt> moves by words</p> +</li> +<li><p class="first"><tt class="kbd docutils literal"><span class="pre">Home</span></tt>/<tt class="kbd docutils literal"><span class="pre">End</span></tt> go to begin/end of line</p> +</li> +<li><p class="first"><tt class="kbd docutils literal"><span class="pre">C-Home</span></tt>/<tt class="kbd docutils literal"><span class="pre">C-End</span></tt> go to begin/end of file</p> +</li> +<li><p class="first">Some useful Emacs bindings are inherited from Tcl/Tk:</p> +<blockquote> +<div><ul class="simple"> +<li><tt class="kbd docutils literal"><span class="pre">C-a</span></tt> beginning of line</li> +<li><tt class="kbd docutils literal"><span class="pre">C-e</span></tt> end of line</li> +<li><tt class="kbd docutils literal"><span class="pre">C-k</span></tt> kill line (but doesn’t put it in clipboard)</li> +<li><tt class="kbd docutils literal"><span class="pre">C-l</span></tt> center window around the insertion point</li> +<li><tt class="kbd docutils literal"><span class="pre">C-b</span></tt> go backwards one character without deleting (usually you can +also use the cursor key for this)</li> +<li><tt class="kbd docutils literal"><span class="pre">C-f</span></tt> go forward one character without deleting (usually you can +also use the cursor key for this)</li> +<li><tt class="kbd docutils literal"><span class="pre">C-p</span></tt> go up one line (usually you can also use the cursor key for +this)</li> +<li><tt class="kbd docutils literal"><span class="pre">C-d</span></tt> delete next character</li> +</ul> +</div></blockquote> +</li> +</ul> +<p>Standard keybindings (like <tt class="kbd docutils literal"><span class="pre">C-c</span></tt> to copy and <tt class="kbd docutils literal"><span class="pre">C-v</span></tt> to paste) +may work. Keybindings are selected in the Configure IDLE dialog.</p> +<div class="section" id="automatic-indentation"> +<h3>25.5.2.1. Automatic indentation<a class="headerlink" href="#automatic-indentation" title="Permalink to this headline">¶</a></h3> +<p>After a block-opening statement, the next line is indented by 4 spaces (in the +Python Shell window by one tab). After certain keywords (break, return etc.) +the next line is dedented. In leading indentation, <tt class="kbd docutils literal"><span class="pre">Backspace</span></tt> deletes up +to 4 spaces if they are there. <tt class="kbd docutils literal"><span class="pre">Tab</span></tt> inserts spaces (in the Python +Shell window one tab), number depends on Indent width. Currently tabs +are restricted to four spaces due to Tcl/Tk limitations.</p> +<p>See also the indent/dedent region commands in the edit menu.</p> +</div> +<div class="section" id="completions"> +<h3>25.5.2.2. Completions<a class="headerlink" href="#completions" title="Permalink to this headline">¶</a></h3> +<p>Completions are supplied for functions, classes, and attributes of classes, +both built-in and user-defined. Completions are also provided for +filenames.</p> +<p>The AutoCompleteWindow (ACW) will open after a predefined delay (default is +two seconds) after a ‘.’ or (in a string) an os.sep is typed. If after one +of those characters (plus zero or more other characters) a tab is typed +the ACW will open immediately if a possible continuation is found.</p> +<p>If there is only one possible completion for the characters entered, a +<tt class="kbd docutils literal"><span class="pre">Tab</span></tt> will supply that completion without opening the ACW.</p> +<p>‘Show Completions’ will force open a completions window, by default the +<tt class="kbd docutils literal"><span class="pre">C-space</span></tt> will open a completions window. In an empty +string, this will contain the files in the current directory. On a +blank line, it will contain the built-in and user-defined functions and +classes in the current name spaces, plus any modules imported. If some +characters have been entered, the ACW will attempt to be more specific.</p> +<p>If a string of characters is typed, the ACW selection will jump to the +entry most closely matching those characters. Entering a <tt class="kbd docutils literal"><span class="pre">tab</span></tt> will +cause the longest non-ambiguous match to be entered in the Editor window or +Shell. Two <tt class="kbd docutils literal"><span class="pre">tab</span></tt> in a row will supply the current ACW selection, as +will return or a double click. Cursor keys, Page Up/Down, mouse selection, +and the scroll wheel all operate on the ACW.</p> +<p>“Hidden” attributes can be accessed by typing the beginning of hidden +name after a ‘.’, e.g. ‘_’. This allows access to modules with +<tt class="docutils literal"><span class="pre">__all__</span></tt> set, or to class-private attributes.</p> +<p>Completions and the ‘Expand Word’ facility can save a lot of typing!</p> +<p>Completions are currently limited to those in the namespaces. Names in +an Editor window which are not via <tt class="docutils literal"><span class="pre">__main__</span></tt> and <a class="reference internal" href="sys.html#sys.modules" title="sys.modules"><tt class="xref py py-data docutils literal"><span class="pre">sys.modules</span></tt></a> will +not be found. Run the module once with your imports to correct this situation. +Note that IDLE itself places quite a few modules in sys.modules, so +much can be found by default, e.g. the re module.</p> +<p>If you don’t like the ACW popping up unbidden, simply make the delay +longer or disable the extension.</p> +</div> +<div class="section" id="calltips"> +<h3>25.5.2.3. Calltips<a class="headerlink" href="#calltips" title="Permalink to this headline">¶</a></h3> +<p>A calltip is shown when one types <tt class="kbd docutils literal"><span class="pre">(</span></tt> after the name of an <em>acccessible</em> +function. A name expression may include dots and subscripts. A calltip +remains until it is clicked, the cursor is moved out of the argument area, +or <tt class="kbd docutils literal"><span class="pre">)</span></tt> is typed. When the cursor is in the argument part of a definition, +the menu or shortcut display a calltip.</p> +<p>A calltip consists of the function signature and the first line of the +docstring. For builtins without an accessible signature, the calltip +consists of all lines up the fifth line or the first blank line. These +details may change.</p> +<p>The set of <em>accessible</em> functions depends on what modules have been imported +into the user process, including those imported by Idle itself, +and what definitions have been run, all since the last restart.</p> +<p>For example, restart the Shell and enter <tt class="docutils literal"><span class="pre">itertools.count(</span></tt>. A calltip +appears because Idle imports itertools into the user process for its own use. +(This could change.) Enter <tt class="docutils literal"><span class="pre">turtle.write(</span></tt> and nothing appears. Idle does +not import turtle. The menu or shortcut do nothing either. Enter +<tt class="docutils literal"><span class="pre">import</span> <span class="pre">turtle</span></tt> and then <tt class="docutils literal"><span class="pre">turtle.write(</span></tt> will work.</p> +<p>In an editor, import statements have no effect until one runs the file. One +might want to run a file after writing the import statements at the top, +or immediately run an existing file before editing.</p> +</div> +<div class="section" id="python-shell-window"> +<h3>25.5.2.4. Python Shell window<a class="headerlink" href="#python-shell-window" title="Permalink to this headline">¶</a></h3> +<ul> +<li><p class="first"><tt class="kbd docutils literal"><span class="pre">C-c</span></tt> interrupts executing command</p> +</li> +<li><p class="first"><tt class="kbd docutils literal"><span class="pre">C-d</span></tt> sends end-of-file; closes window if typed at a <tt class="docutils literal"><span class="pre">>>></span></tt> prompt</p> +</li> +<li><p class="first"><tt class="kbd docutils literal"><span class="pre">Alt-/</span></tt> (Expand word) is also useful to reduce typing</p> +<p>Command history</p> +<ul class="simple"> +<li><tt class="kbd docutils literal"><span class="pre">Alt-p</span></tt> retrieves previous command matching what you have typed. On +OS X use <tt class="kbd docutils literal"><span class="pre">C-p</span></tt>.</li> +<li><tt class="kbd docutils literal"><span class="pre">Alt-n</span></tt> retrieves next. On OS X use <tt class="kbd docutils literal"><span class="pre">C-n</span></tt>.</li> +<li><tt class="kbd docutils literal"><span class="pre">Return</span></tt> while on any previous command retrieves that command</li> +</ul> +</li> +</ul> +</div> +<div class="section" id="text-colors"> +<h3>25.5.2.5. Text colors<a class="headerlink" href="#text-colors" title="Permalink to this headline">¶</a></h3> +<p>Idle defaults to black on white text, but colors text with special meanings. +For the shell, these are shell output, shell error, user output, and +user error. For Python code, at the shell prompt or in an editor, these are +keywords, builtin class and function names, names following <tt class="docutils literal"><span class="pre">class</span></tt> and +<tt class="docutils literal"><span class="pre">def</span></tt>, strings, and comments. For any text window, these are the cursor (when +present), found text (when possible), and selected text.</p> +<p>Text coloring is done in the background, so uncolorized text is occasionally +visible. To change the color scheme, use the Configure IDLE dialog +Highlighting tab. The marking of debugger breakpoint lines in the editor and +text in popups and dialogs is not user-configurable.</p> +</div> +</div> +<div class="section" id="startup-and-code-execution"> +<h2>25.5.3. Startup and code execution<a class="headerlink" href="#startup-and-code-execution" title="Permalink to this headline">¶</a></h2> +<p>Upon startup with the <tt class="docutils literal"><span class="pre">-s</span></tt> option, IDLE will execute the file referenced by +the environment variables <span class="target" id="index-5"></span><tt class="xref std std-envvar docutils literal"><span class="pre">IDLESTARTUP</span></tt> or <span class="target" id="index-6"></span><a class="reference internal" href="../using/cmdline.html#envvar-PYTHONSTARTUP"><tt class="xref std std-envvar docutils literal"><span class="pre">PYTHONSTARTUP</span></tt></a>. +IDLE first checks for <tt class="docutils literal"><span class="pre">IDLESTARTUP</span></tt>; if <tt class="docutils literal"><span class="pre">IDLESTARTUP</span></tt> is present the file +referenced is run. If <tt class="docutils literal"><span class="pre">IDLESTARTUP</span></tt> is not present, IDLE checks for +<tt class="docutils literal"><span class="pre">PYTHONSTARTUP</span></tt>. Files referenced by these environment variables are +convenient places to store functions that are used frequently from the IDLE +shell, or for executing import statements to import common modules.</p> +<p>In addition, <tt class="docutils literal"><span class="pre">Tk</span></tt> also loads a startup file if it is present. Note that the +Tk file is loaded unconditionally. This additional file is <tt class="docutils literal"><span class="pre">.Idle.py</span></tt> and is +looked for in the user’s home directory. Statements in this file will be +executed in the Tk namespace, so this file is not useful for importing +functions to be used from IDLE’s Python shell.</p> +<div class="section" id="command-line-usage"> +<h3>25.5.3.1. Command line usage<a class="headerlink" href="#command-line-usage" title="Permalink to this headline">¶</a></h3> +<div class="highlight-python3"><div class="highlight"><pre>idle.py [-c command] [-d] [-e] [-h] [-i] [-r file] [-s] [-t title] [-] [arg] ... + +-c command run command in the shell window +-d enable debugger and open shell window +-e open editor window +-h print help message with legal combinatios and exit +-i open shell window +-r file run file in shell window +-s run $IDLESTARTUP or $PYTHONSTARTUP first, in shell window +-t title set title of shell window +- run stdin in shell (- must be last option before args) +</pre></div> +</div> +<p>If there are arguments:</p> +<ul class="simple"> +<li>If <tt class="docutils literal"><span class="pre">-</span></tt>, <tt class="docutils literal"><span class="pre">-c</span></tt>, or <tt class="docutils literal"><span class="pre">r</span></tt> is used, all arguments are placed in +<tt class="docutils literal"><span class="pre">sys.argv[1:...]</span></tt> and <tt class="docutils literal"><span class="pre">sys.argv[0]</span></tt> is set to <tt class="docutils literal"><span class="pre">''</span></tt>, <tt class="docutils literal"><span class="pre">'-c'</span></tt>, +or <tt class="docutils literal"><span class="pre">'-r'</span></tt>. No editor window is opened, even if that is the default +set in the Options dialog.</li> +<li>Otherwise, arguments are files opened for editing and +<tt class="docutils literal"><span class="pre">sys.argv</span></tt> reflects the arguments passed to IDLE itself.</li> +</ul> +</div> +<div class="section" id="idle-console-differences"> +<h3>25.5.3.2. IDLE-console differences<a class="headerlink" href="#idle-console-differences" title="Permalink to this headline">¶</a></h3> +<p>As much as possible, the result of executing Python code with IDLE is the +same as executing the same code in a console window. However, the different +interface and operation occasionally affects results.</p> +<p>For instance, IDLE normally executes user code in a separate process from +the IDLE GUI itself. The IDLE versions of sys.stdin, .stdout, and .stderr in the +execution process get input from and send output to the GUI process, +which keeps control of the keyboard and screen. This is normally transparent, +but code that access these object will see different attribute values. +Also, functions that directly access the keyboard and screen will not work.</p> +<p>With IDLE’s Shell, one enters, edits, and recalls complete statements. +Some consoles only work with a single physical line at a time.</p> +</div> +<div class="section" id="running-without-a-subprocess"> +<h3>25.5.3.3. Running without a subprocess<a class="headerlink" href="#running-without-a-subprocess" title="Permalink to this headline">¶</a></h3> +<p>By default, IDLE executes user code in a separate subprocess via a socket, +which uses the internal loopback interface. This connection is not +externally visible and no data is sent to or received from the Internet. +If firewall software complains anyway, you can ignore it.</p> +<p>If the attempt to make the socket connection fails, Idle will notify you. +Such failures are sometimes transient, but if persistent, the problem +may be either a firewall blocking the connecton or misconfiguration of +a particular system. Until the problem is fixed, one can run Idle with +the -n command line switch.</p> +<p>If IDLE is started with the -n command line switch it will run in a +single process and will not create the subprocess which runs the RPC +Python execution server. This can be useful if Python cannot create +the subprocess or the RPC socket interface on your platform. However, +in this mode user code is not isolated from IDLE itself. Also, the +environment is not restarted when Run/Run Module (F5) is selected. If +your code has been modified, you must reload() the affected modules and +re-import any specific items (e.g. from foo import baz) if the changes +are to take effect. For these reasons, it is preferable to run IDLE +with the default subprocess if at all possible.</p> +<div class="deprecated"> +<p><span class="versionmodified">Deprecated since version 3.4.</span></p> +</div> +</div> +</div> +<div class="section" id="help-and-preferences"> +<h2>25.5.4. Help and preferences<a class="headerlink" href="#help-and-preferences" title="Permalink to this headline">¶</a></h2> +<div class="section" id="additional-help-sources"> +<h3>25.5.4.1. Additional help sources<a class="headerlink" href="#additional-help-sources" title="Permalink to this headline">¶</a></h3> +<p>IDLE includes a help menu entry called “Python Docs” that will open the +extensive sources of help, including tutorials, available at docs.python.org. +Selected URLs can be added or removed from the help menu at any time using the +Configure IDLE dialog. See the IDLE help option in the help menu of IDLE for +more information.</p> +</div> +<div class="section" id="setting-preferences"> +<h3>25.5.4.2. Setting preferences<a class="headerlink" href="#setting-preferences" title="Permalink to this headline">¶</a></h3> +<p>The font preferences, highlighting, keys, and general preferences can be +changed via Configure IDLE on the Option menu. Keys can be user defined; +IDLE ships with four built in key sets. In addition a user can create a +custom key set in the Configure IDLE dialog under the keys tab.</p> +</div> +<div class="section" id="extensions"> +<h3>25.5.4.3. Extensions<a class="headerlink" href="#extensions" title="Permalink to this headline">¶</a></h3> +<p>IDLE contains an extension facility. Peferences for extensions can be +changed with Configure Extensions. See the beginning of config-extensions.def +in the idlelib directory for further information. The default extensions +are currently:</p> +<ul class="simple"> +<li>FormatParagraph</li> +<li>AutoExpand</li> +<li>ZoomHeight</li> +<li>ScriptBinding</li> +<li>CallTips</li> +<li>ParenMatch</li> +<li>AutoComplete</li> +<li>CodeContext</li> +<li>RstripExtension</li> +</ul> +</div> +</div> +</div> + + + </div> + </div> + </div> + <div class="sphinxsidebar"> + <div class="sphinxsidebarwrapper"> + <h3><a href="../contents.html">Table Of Contents</a></h3> + <ul> +<li><a class="reference internal" href="#">25.5. IDLE</a><ul> +<li><a class="reference internal" href="#menus">25.5.1. Menus</a><ul> +<li><a class="reference internal" href="#file-menu-shell-and-editor">25.5.1.1. File menu (Shell and Editor)</a></li> +<li><a class="reference internal" href="#edit-menu-shell-and-editor">25.5.1.2. Edit menu (Shell and Editor)</a></li> +<li><a class="reference internal" href="#format-menu-editor-window-only">25.5.1.3. Format menu (Editor window only)</a></li> +<li><a class="reference internal" href="#run-menu-editor-window-only">25.5.1.4. Run menu (Editor window only)</a></li> +<li><a class="reference internal" href="#shell-menu-shell-window-only">25.5.1.5. Shell menu (Shell window only)</a></li> +<li><a class="reference internal" href="#debug-menu-shell-window-only">25.5.1.6. Debug menu (Shell window only)</a></li> +<li><a class="reference internal" href="#options-menu-shell-and-editor">25.5.1.7. Options menu (Shell and Editor)</a></li> +<li><a class="reference internal" href="#window-menu-shell-and-editor">25.5.1.8. Window menu (Shell and Editor)</a></li> +<li><a class="reference internal" href="#help-menu-shell-and-editor">25.5.1.9. Help menu (Shell and Editor)</a></li> +<li><a class="reference internal" href="#context-menus">25.5.1.10. Context Menus</a></li> +</ul> +</li> +<li><a class="reference internal" href="#editing-and-navigation">25.5.2. Editing and navigation</a><ul> +<li><a class="reference internal" href="#automatic-indentation">25.5.2.1. Automatic indentation</a></li> +<li><a class="reference internal" href="#completions">25.5.2.2. Completions</a></li> +<li><a class="reference internal" href="#calltips">25.5.2.3. Calltips</a></li> +<li><a class="reference internal" href="#python-shell-window">25.5.2.4. Python Shell window</a></li> +<li><a class="reference internal" href="#text-colors">25.5.2.5. Text colors</a></li> +</ul> +</li> +<li><a class="reference internal" href="#startup-and-code-execution">25.5.3. Startup and code execution</a><ul> +<li><a class="reference internal" href="#command-line-usage">25.5.3.1. Command line usage</a></li> +<li><a class="reference internal" href="#idle-console-differences">25.5.3.2. IDLE-console differences</a></li> +<li><a class="reference internal" href="#running-without-a-subprocess">25.5.3.3. Running without a subprocess</a></li> +</ul> +</li> +<li><a class="reference internal" href="#help-and-preferences">25.5.4. Help and preferences</a><ul> +<li><a class="reference internal" href="#additional-help-sources">25.5.4.1. Additional help sources</a></li> +<li><a class="reference internal" href="#setting-preferences">25.5.4.2. Setting preferences</a></li> +<li><a class="reference internal" href="#extensions">25.5.4.3. Extensions</a></li> +</ul> +</li> +</ul> +</li> +</ul> + + <h4>Previous topic</h4> + <p class="topless"><a href="tkinter.scrolledtext.html" + title="previous chapter">25.4. <tt class="docutils literal"><span class="pre">tkinter.scrolledtext</span></tt> — Scrolled Text Widget</a></p> + <h4>Next topic</h4> + <p class="topless"><a href="othergui.html" + title="next chapter">25.6. Other Graphical User Interface Packages</a></p> +<h3>This Page</h3> +<ul class="this-page-menu"> + <li><a href="../bugs.html">Report a Bug</a></li> + <li><a href="../_sources/library/idle.txt" + rel="nofollow">Show Source</a></li> +</ul> + +<div id="searchbox" style="display: none"> + <h3>Quick search</h3> + <form class="search" action="../search.html" method="get"> + <input type="text" name="q" /> + <input type="submit" value="Go" /> + <input type="hidden" name="check_keywords" value="yes" /> + <input type="hidden" name="area" value="default" /> + </form> + <p class="searchtip" style="font-size: 90%"> + Enter search terms or a module, class or function name. + </p> +</div> +<script type="text/javascript">$('#searchbox').show(0);</script> + </div> + </div> + <div class="clearer"></div> + </div> + <div class="related"> + <h3>Navigation</h3> + <ul> + <li class="right" style="margin-right: 10px"> + <a href="../genindex.html" title="General Index" + >index</a></li> + <li class="right" > + <a href="../py-modindex.html" title="Python Module Index" + >modules</a> |</li> + <li class="right" > + <a href="othergui.html" title="25.6. Other Graphical User Interface Packages" + >next</a> |</li> + <li class="right" > + <a href="tkinter.scrolledtext.html" title="25.4. tkinter.scrolledtext — Scrolled Text Widget" + >previous</a> |</li> + <li><img src="../_static/py.png" alt="" + style="vertical-align: middle; margin-top: -1px"/></li> + <li><a href="https://www.python.org/">Python</a> »</li> + <li> + <a href="../index.html">3.4.3 Documentation</a> » + </li> + + <li><a href="index.html" >The Python Standard Library</a> »</li> + <li><a href="tk.html" >25. Graphical User Interfaces with Tk</a> »</li> + </ul> + </div> + <div class="footer"> + © <a href="../copyright.html">Copyright</a> 1990-2015, Python Software Foundation. + <br /> + The Python Software Foundation is a non-profit corporation. + <a href="https://www.python.org/psf/donations/">Please donate.</a> + <br /> + Last updated on Oct 13, 2015. + <a href="../bugs.html">Found a bug</a>? + <br /> + Created using <a href="http://sphinx.pocoo.org/">Sphinx</a> 1.2.3. + </div> + + </body> +</html> diff --git a/Lib/idlelib/help.py b/Lib/idlelib/help.py new file mode 100644 index 0000000000..b31596c2eb --- /dev/null +++ b/Lib/idlelib/help.py @@ -0,0 +1,249 @@ +""" help.py: Implement the Idle help menu. +Contents are subject to revision at any time, without notice. + + +Help => About IDLE: diplay About Idle dialog + +<to be moved here from aboutDialog.py> + + +Help => IDLE Help: Display help.html with proper formatting. +Doc/library/idle.rst (Sphinx)=> Doc/build/html/library/idle.html +(help.copy_strip)=> Lib/idlelib/help.html + +HelpParser - Parse help.html and and render to tk Text. + +HelpText - Display formatted help.html. + +HelpFrame - Contain text, scrollbar, and table-of-contents. +(This will be needed for display in a future tabbed window.) + +HelpWindow - Display HelpFrame in a standalone window. + +copy_strip - Copy idle.html to help.html, rstripping each line. + +show_idlehelp - Create HelpWindow. Called in EditorWindow.help_dialog. +""" +from html.parser import HTMLParser +from os.path import abspath, dirname, isdir, isfile, join +from tkinter import Tk, Toplevel, Frame, Text, Scrollbar, Menu, Menubutton +from tkinter import font as tkfont +from idlelib.configHandler import idleConf + +use_ttk = False # until available to import +if use_ttk: + from tkinter.ttk import Menubutton + +## About IDLE ## + + +## IDLE Help ## + +class HelpParser(HTMLParser): + """Render help.html into a text widget. + + The overridden handle_xyz methods handle a subset of html tags. + The supplied text should have the needed tag configurations. + The behavior for unsupported tags, such as table, is undefined. + """ + def __init__(self, text): + HTMLParser.__init__(self, convert_charrefs=True) + self.text = text # text widget we're rendering into + self.tags = '' # current block level text tags to apply + self.chartags = '' # current character level text tags + self.show = False # used so we exclude page navigation + self.hdrlink = False # used so we don't show header links + self.level = 0 # indentation level + self.pre = False # displaying preformatted text + self.hprefix = '' # prefix such as '25.5' to strip from headings + self.nested_dl = False # if we're in a nested <dl> + self.simplelist = False # simple list (no double spacing) + self.toc = [] # pair headers with text indexes for toc + self.header = '' # text within header tags for toc + + def indent(self, amt=1): + self.level += amt + self.tags = '' if self.level == 0 else 'l'+str(self.level) + + def handle_starttag(self, tag, attrs): + "Handle starttags in help.html." + class_ = '' + for a, v in attrs: + if a == 'class': + class_ = v + s = '' + if tag == 'div' and class_ == 'section': + self.show = True # start of main content + elif tag == 'div' and class_ == 'sphinxsidebar': + self.show = False # end of main content + elif tag == 'p' and class_ != 'first': + s = '\n\n' + elif tag == 'span' and class_ == 'pre': + self.chartags = 'pre' + elif tag == 'span' and class_ == 'versionmodified': + self.chartags = 'em' + elif tag == 'em': + self.chartags = 'em' + elif tag in ['ul', 'ol']: + if class_.find('simple') != -1: + s = '\n' + self.simplelist = True + else: + self.simplelist = False + self.indent() + elif tag == 'dl': + if self.level > 0: + self.nested_dl = True + elif tag == 'li': + s = '\n* ' if self.simplelist else '\n\n* ' + elif tag == 'dt': + s = '\n\n' if not self.nested_dl else '\n' # avoid extra line + self.nested_dl = False + elif tag == 'dd': + self.indent() + s = '\n' + elif tag == 'pre': + self.pre = True + if self.show: + self.text.insert('end', '\n\n') + self.tags = 'preblock' + elif tag == 'a' and class_ == 'headerlink': + self.hdrlink = True + elif tag == 'h1': + self.tags = tag + elif tag in ['h2', 'h3']: + if self.show: + self.header = '' + self.text.insert('end', '\n\n') + self.tags = tag + if self.show: + self.text.insert('end', s, (self.tags, self.chartags)) + + def handle_endtag(self, tag): + "Handle endtags in help.html." + if tag in ['h1', 'h2', 'h3']: + self.indent(0) # clear tag, reset indent + if self.show: + self.toc.append((self.header, self.text.index('insert'))) + elif tag in ['span', 'em']: + self.chartags = '' + elif tag == 'a': + self.hdrlink = False + elif tag == 'pre': + self.pre = False + self.tags = '' + elif tag in ['ul', 'dd', 'ol']: + self.indent(amt=-1) + + def handle_data(self, data): + "Handle date segments in help.html." + if self.show and not self.hdrlink: + d = data if self.pre else data.replace('\n', ' ') + if self.tags == 'h1': + self.hprefix = d[0:d.index(' ')] + if self.tags in ['h1', 'h2', 'h3'] and self.hprefix != '': + if d[0:len(self.hprefix)] == self.hprefix: + d = d[len(self.hprefix):].strip() + self.header += d + self.text.insert('end', d, (self.tags, self.chartags)) + + +class HelpText(Text): + "Display help.html." + def __init__(self, parent, filename): + "Configure tags and feed file to parser." + uwide = idleConf.GetOption('main', 'EditorWindow', 'width', type='int') + uhigh = idleConf.GetOption('main', 'EditorWindow', 'height', type='int') + uhigh = 3 * uhigh // 4 # lines average 4/3 of editor line height + Text.__init__(self, parent, wrap='word', highlightthickness=0, + padx=5, borderwidth=0, width=uwide, height=uhigh) + + normalfont = self.findfont(['TkDefaultFont', 'arial', 'helvetica']) + fixedfont = self.findfont(['TkFixedFont', 'monaco', 'courier']) + self['font'] = (normalfont, 12) + self.tag_configure('em', font=(normalfont, 12, 'italic')) + self.tag_configure('h1', font=(normalfont, 20, 'bold')) + self.tag_configure('h2', font=(normalfont, 18, 'bold')) + self.tag_configure('h3', font=(normalfont, 15, 'bold')) + self.tag_configure('pre', font=(fixedfont, 12), background='#f6f6ff') + self.tag_configure('preblock', font=(fixedfont, 10), lmargin1=25, + borderwidth=1, relief='solid', background='#eeffcc') + self.tag_configure('l1', lmargin1=25, lmargin2=25) + self.tag_configure('l2', lmargin1=50, lmargin2=50) + self.tag_configure('l3', lmargin1=75, lmargin2=75) + self.tag_configure('l4', lmargin1=100, lmargin2=100) + + self.parser = HelpParser(self) + with open(filename, encoding='utf-8') as f: + contents = f.read() + self.parser.feed(contents) + self['state'] = 'disabled' + + def findfont(self, names): + "Return name of first font family derived from names." + for name in names: + if name.lower() in (x.lower() for x in tkfont.names(root=self)): + font = tkfont.Font(name=name, exists=True, root=self) + return font.actual()['family'] + elif name.lower() in (x.lower() + for x in tkfont.families(root=self)): + return name + + +class HelpFrame(Frame): + "Display html text, scrollbar, and toc." + def __init__(self, parent, filename): + Frame.__init__(self, parent) + text = HelpText(self, filename) + self['background'] = text['background'] + scroll = Scrollbar(self, command=text.yview) + text['yscrollcommand'] = scroll.set + self.rowconfigure(0, weight=1) + self.columnconfigure(1, weight=1) # text + self.toc_menu(text).grid(column=0, row=0, sticky='nw') + text.grid(column=1, row=0, sticky='nsew') + scroll.grid(column=2, row=0, sticky='ns') + + def toc_menu(self, text): + "Create table of contents as drop-down menu." + toc = Menubutton(self, text='TOC') + drop = Menu(toc, tearoff=False) + for lbl, dex in text.parser.toc: + drop.add_command(label=lbl, command=lambda dex=dex:text.yview(dex)) + toc['menu'] = drop + return toc + + +class HelpWindow(Toplevel): + "Display frame with rendered html." + def __init__(self, parent, filename, title): + Toplevel.__init__(self, parent) + self.wm_title(title) + self.protocol("WM_DELETE_WINDOW", self.destroy) + HelpFrame(self, filename).grid(column=0, row=0, sticky='nsew') + self.grid_columnconfigure(0, weight=1) + self.grid_rowconfigure(0, weight=1) + + +def copy_strip(): + "Copy idle.html to idlelib/help.html, stripping trailing whitespace." + src = join(abspath(dirname(dirname(dirname(__file__)))), + 'Doc', 'build', 'html', 'library', 'idle.html') + dst = join(abspath(dirname(__file__)), 'help.html') + with open(src, 'rb') as inn,\ + open(dst, 'wb') as out: + for line in inn: + out.write(line.rstrip() + b'\n') + print('idle.html copied to help.html') + +def show_idlehelp(parent): + "Create HelpWindow; called from Idle Help event handler." + filename = join(abspath(dirname(__file__)), 'help.html') + if not isfile(filename): + # try copy_strip, present message + return + HelpWindow(parent, filename, 'IDLE Help') + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(show_idlehelp) diff --git a/Lib/idlelib/help.txt b/Lib/idlelib/help.txt index ff786c53f3..89fbe0b41e 100644 --- a/Lib/idlelib/help.txt +++ b/Lib/idlelib/help.txt @@ -1,142 +1,189 @@ -[See the end of this file for ** TIPS ** on using IDLE !!] - -Click on the dotted line at the top of a menu to "tear it off": a -separate window containing the menu is created. - -File Menu: - - New File -- Create a new file editing window - Open... -- Open an existing file - Recent Files... -- Open a list of recent files - Open Module... -- Open an existing module (searches sys.path) - Class Browser -- Show classes and methods in current file - Path Browser -- Show sys.path directories, modules, classes - and methods - --- - Save -- Save current window to the associated file (unsaved - windows have a * before and after the window title) - - Save As... -- Save current window to new file, which becomes - the associated file - Save Copy As... -- Save current window to different file - without changing the associated file - --- - Print Window -- Print the current window - --- - Close -- Close current window (asks to save if unsaved) - Exit -- Close all windows, quit (asks to save if unsaved) - -Edit Menu: - - Undo -- Undo last change to current window - (A maximum of 1000 changes may be undone) - Redo -- Redo last undone change to current window - --- - Cut -- Copy a selection into system-wide clipboard, - then delete the selection - Copy -- Copy selection into system-wide clipboard - Paste -- Insert system-wide clipboard into window - Select All -- Select the entire contents of the edit buffer - --- - Find... -- Open a search dialog box with many options - Find Again -- Repeat last search - Find Selection -- Search for the string in the selection - Find in Files... -- Open a search dialog box for searching files - Replace... -- Open a search-and-replace dialog box - Go to Line -- Ask for a line number and show that line - Show Calltip -- Open a small window with function param hints - Show Completions -- Open a scroll window allowing selection keywords - and attributes. (see '*TIPS*', below) - Show Parens -- Highlight the surrounding parenthesis - Expand Word -- Expand the word you have typed to match another - word in the same buffer; repeat to get a - different expansion +This file, idlelib/help.txt is out-of-date and no longer used by Idle. +It is deprecated and will be removed in the future, possibly in 3.6 +---------------------------------------------------------------------- -Format Menu (only in Edit window): - - Indent Region -- Shift selected lines right 4 spaces - Dedent Region -- Shift selected lines left 4 spaces - Comment Out Region -- Insert ## in front of selected lines - Uncomment Region -- Remove leading # or ## from selected lines - Tabify Region -- Turns *leading* stretches of spaces into tabs - (Note: We recommend using 4 space blocks to indent Python code.) - Untabify Region -- Turn *all* tabs into the right number of spaces - New Indent Width... -- Open dialog to change indent width - Format Paragraph -- Reformat the current blank-line-separated - paragraph - -Run Menu (only in Edit window): - - Python Shell -- Open or wake up the Python shell window - --- - Check Module -- Run a syntax check on the module - Run Module -- Execute the current file in the __main__ namespace +[See the end of this file for ** TIPS ** on using IDLE !!] -Shell Menu (only in Shell window): +IDLE is the Python IDE built with the tkinter GUI toolkit. - View Last Restart -- Scroll the shell window to the last restart - Restart Shell -- Restart the interpreter with a fresh environment +IDLE has the following features: +-coded in 100% pure Python, using the tkinter GUI toolkit +-cross-platform: works on Windows, Unix, and OS X +-multi-window text editor with multiple undo, Python colorizing, smart indent, +call tips, and many other features +-Python shell window (a.k.a interactive interpreter) +-debugger (not complete, but you can set breakpoints, view and step) -Debug Menu (only in Shell window): +Menus: - Go to File/Line -- look around the insert point for a filename - and line number, open the file, and show the line - Debugger (toggle) -- Run commands in the shell under the debugger - Stack Viewer -- Show the stack traceback of the last exception - Auto-open Stack Viewer (toggle) -- Open stack viewer on traceback +IDLE has two window types the Shell window and the Editor window. It is +possible to have multiple editor windows simultaneously. IDLE's +menus dynamically change based on which window is currently selected. Each menu +documented below indicates which window type it is associated with. -Options Menu: +File Menu (Shell and Editor): - Configure IDLE -- Open a configuration dialog. Fonts, indentation, + New File -- Create a new file editing window + Open... -- Open an existing file + Open Module... -- Open an existing module (searches sys.path) + Recent Files... -- Open a list of recent files + Class Browser -- Show classes and methods in current file + Path Browser -- Show sys.path directories, modules, classes, + and methods + --- + Save -- Save current window to the associated file (unsaved + windows have a * before and after the window title) + + Save As... -- Save current window to new file, which becomes + the associated file + Save Copy As... -- Save current window to different file + without changing the associated file + --- + Print Window -- Print the current window + --- + Close -- Close current window (asks to save if unsaved) + Exit -- Close all windows, quit (asks to save if unsaved) + +Edit Menu (Shell and Editor): + + Undo -- Undo last change to current window + (a maximum of 1000 changes may be undone) + Redo -- Redo last undone change to current window + --- + Cut -- Copy a selection into system-wide clipboard, + then delete the selection + Copy -- Copy selection into system-wide clipboard + Paste -- Insert system-wide clipboard into window + Select All -- Select the entire contents of the edit buffer + --- + Find... -- Open a search dialog box with many options + Find Again -- Repeat last search + Find Selection -- Search for the string in the selection + Find in Files... -- Open a search dialog box for searching files + Replace... -- Open a search-and-replace dialog box + Go to Line -- Ask for a line number and show that line + Expand Word -- Expand the word you have typed to match another + word in the same buffer; repeat to get a + different expansion + Show Calltip -- After an unclosed parenthesis for a function, open + a small window with function parameter hints + Show Parens -- Highlight the surrounding parenthesis + Show Completions -- Open a scroll window allowing selection keywords + and attributes. (see '*TIPS*', below) + +Format Menu (Editor window only): + + Indent Region -- Shift selected lines right by the indent width + (default 4 spaces) + Dedent Region -- Shift selected lines left by the indent width + (default 4 spaces) + Comment Out Region -- Insert ## in front of selected lines + Uncomment Region -- Remove leading # or ## from selected lines + Tabify Region -- Turns *leading* stretches of spaces into tabs. + (Note: We recommend using 4 space blocks to indent Python code.) + Untabify Region -- Turn *all* tabs into the corrent number of spaces + Toggle tabs -- Open a dialog to switch between indenting with + spaces and tabs. + New Indent Width... -- Open a dialog to change indent width. The + accepted default by the Python community is 4 + spaces. + Format Paragraph -- Reformat the current blank-line-separated + paragraph. All lines in the paragraph will be + formatted to less than 80 columns. + --- + Strip trailing whitespace -- Removed any space characters after the end + of the last non-space character + +Run Menu (Editor window only): + + Python Shell -- Open or wake up the Python shell window + --- + Check Module -- Check the syntax of the module currently open in the + Editor window. If the module has not been saved IDLE + will prompt the user to save the code. + Run Module -- Restart the shell to clean the environment, then + execute the currently open module. If the module has + not been saved IDLE will prompt the user to save the + code. + +Shell Menu (Shell window only): + + View Last Restart -- Scroll the shell window to the last Shell restart + Restart Shell -- Restart the shell to clean the environment + +Debug Menu (Shell window only): + + Go to File/Line -- Look around the insert point for a filename + and line number, open the file, and show the line. + Useful to view the source lines referenced in an + exception traceback. Available in the context + menu of the Shell window. + Debugger (toggle) -- This feature is not complete and considered + experimental. Run commands in the shell under the + debugger. + Stack Viewer -- Show the stack traceback of the last exception + Auto-open Stack Viewer (toggle) -- Toggle automatically opening the + stack viewer on unhandled + exception + +Options Menu (Shell and Editor): + + Configure IDLE -- Open a configuration dialog. Fonts, indentation, keybindings, and color themes may be altered. - Startup Preferences may be set, and Additional Help - Sources can be specified. - - On OS X this menu is not present, use - menu 'IDLE -> Preferences...' instead. - --- - Code Context -- Open a pane at the top of the edit window which - shows the block context of the section of code - which is scrolling off the top or the window. - (Not present in Shell window.) - -Windows Menu: - - Zoom Height -- toggles the window between configured size - and maximum height. - --- - The rest of this menu lists the names of all open windows; - select one to bring it to the foreground (deiconifying it if - necessary). + Startup Preferences may be set, and additional Help + sources can be specified. On OS X, open the + configuration dialog by selecting Preferences + in the application menu. + + --- + Code Context (toggle) -- Open a pane at the top of the edit window + which shows the block context of the section + of code which is scrolling off the top or the + window. This is not present in the Shell + window only the Editor window. + +Window Menu (Shell and Editor): + + Zoom Height -- Toggles the window between normal size (40x80 initial + setting) and maximum height. The initial size is in the Configure + IDLE dialog under the general tab. + --- + The rest of this menu lists the names of all open windows; + select one to bring it to the foreground (deiconifying it if + necessary). Help Menu: - About IDLE -- Version, copyright, license, credits - IDLE Readme -- Background discussion and change details - --- - IDLE Help -- Display this file - Python Docs -- Access local Python documentation, if - installed. Otherwise, access www.python.org. - --- - (Additional Help Sources may be added here) - -Edit context menu (Right-click / Control-click on OS X in Edit window): - - Cut -- Copy a selection into system-wide clipboard, + About IDLE -- Version, copyright, license, credits + --- + IDLE Help -- Display this file which is a help file for IDLE + detailing the menu options, basic editing and navigation, + and other tips. + Python Docs -- Access local Python documentation, if + installed. Or will start a web browser and open + docs.python.org showing the latest Python documentation. + --- + Additional help sources may be added here with the Configure IDLE + dialog under the General tab. + +Editor context menu (Right-click / Control-click on OS X in Edit window): + + Cut -- Copy a selection into system-wide clipboard, then delete the selection - Copy -- Copy selection into system-wide clipboard - Paste -- Insert system-wide clipboard into window - Set Breakpoint -- Sets a breakpoint (when debugger open) - Clear Breakpoint -- Clears the breakpoint on that line + Copy -- Copy selection into system-wide clipboard + Paste -- Insert system-wide clipboard into window + Set Breakpoint -- Sets a breakpoint. Breakpoints are only enabled + when the debugger is open. + Clear Breakpoint -- Clears the breakpoint on that line Shell context menu (Right-click / Control-click on OS X in Shell window): - Cut -- Copy a selection into system-wide clipboard, + Cut -- Copy a selection into system-wide clipboard, then delete the selection - Copy -- Copy selection into system-wide clipboard - Paste -- Insert system-wide clipboard into window - --- - Go to file/line -- Same as in Debug menu + Copy -- Copy selection into system-wide clipboard + Paste -- Insert system-wide clipboard into window + --- + Go to file/line -- Same as in Debug menu ** TIPS ** @@ -144,159 +191,182 @@ Shell context menu (Right-click / Control-click on OS X in Shell window): Additional Help Sources: - Windows users can Google on zopeshelf.chm to access Zope help files in - the Windows help format. The Additional Help Sources feature of the - configuration GUI supports .chm, along with any other filetypes - supported by your browser. Supply a Menu Item title, and enter the - location in the Help File Path slot of the New Help Source dialog. Use - http:// and/or www. to identify external URLs, or download the file and - browse for its path on your machine using the Browse button. + Windows users can Google on zopeshelf.chm to access Zope help files in + the Windows help format. The Additional Help Sources feature of the + configuration GUI supports .chm, along with any other filetypes + supported by your browser. Supply a Menu Item title, and enter the + location in the Help File Path slot of the New Help Source dialog. Use + http:// and/or www. to identify external URLs, or download the file and + browse for its path on your machine using the Browse button. - All users can access the extensive sources of help, including - tutorials, available at www.python.org/doc. Selected URLs can be added - or removed from the Help menu at any time using Configure IDLE. + All users can access the extensive sources of help, including + tutorials, available at docs.python.org. Selected URLs can be added + or removed from the Help menu at any time using Configure IDLE. Basic editing and navigation: - Backspace deletes char to the left; DEL deletes char to the right. - Control-backspace deletes word left, Control-DEL deletes word right. - Arrow keys and Page Up/Down move around. - Control-left/right Arrow moves by words in a strange but useful way. - Home/End go to begin/end of line. - Control-Home/End go to begin/end of file. - Some useful Emacs bindings are inherited from Tcl/Tk: - Control-a beginning of line - Control-e end of line - Control-k kill line (but doesn't put it in clipboard) - Control-l center window around the insertion point - Standard Windows bindings may work on that platform. - Keybindings are selected in the Settings Dialog, look there. + Backspace deletes char to the left; DEL deletes char to the right. + Control-backspace deletes word left, Control-DEL deletes word right. + Arrow keys and Page Up/Down move around. + Control-left/right Arrow moves by words in a strange but useful way. + Home/End go to begin/end of line. + Control-Home/End go to begin/end of file. + Some useful Emacs bindings are inherited from Tcl/Tk: + Control-a beginning of line + Control-e end of line + Control-k kill line (but doesn't put it in clipboard) + Control-l center window around the insertion point + Standard keybindings (like Control-c to copy and Control-v to + paste) may work. Keybindings are selected in the Configure IDLE + dialog. Automatic indentation: - After a block-opening statement, the next line is indented by 4 spaces - (in the Python Shell window by one tab). After certain keywords - (break, return etc.) the next line is dedented. In leading - indentation, Backspace deletes up to 4 spaces if they are there. Tab - inserts spaces (in the Python Shell window one tab), number depends on - Indent Width. (N.B. Currently tabs are restricted to four spaces due - to Tcl/Tk issues.) + After a block-opening statement, the next line is indented by 4 spaces + (in the Python Shell window by one tab). After certain keywords + (break, return etc.) the next line is dedented. In leading + indentation, Backspace deletes up to 4 spaces if they are there. Tab + inserts spaces (in the Python Shell window one tab), number depends on + Indent Width. Currently tabs are restricted to four spaces due + to Tcl/Tk limitations. See also the indent/dedent region commands in the edit menu. Completions: - Completions are supplied for functions, classes, and attributes of - classes, both built-in and user-defined. Completions are also provided - for filenames. - - The AutoCompleteWindow (ACW) will open after a predefined delay - (default is two seconds) after a '.' or (in a string) an os.sep is - typed. If after one of those characters (plus zero or more other - characters) you type a Tab the ACW will open immediately if a possible - continuation is found. - - If there is only one possible completion for the characters entered, a - Tab will supply that completion without opening the ACW. - - 'Show Completions' will force open a completions window. In an empty - string, this will contain the files in the current directory. On a - blank line, it will contain the built-in and user-defined functions and - classes in the current name spaces, plus any modules imported. If some - characters have been entered, the ACW will attempt to be more specific. - - If string of characters is typed, the ACW selection will jump to the - entry most closely matching those characters. Entering a Tab will cause - the longest non-ambiguous match to be entered in the Edit window or - Shell. Two Tabs in a row will supply the current ACW selection, as - will Return or a double click. Cursor keys, Page Up/Down, mouse - selection, and the scrollwheel all operate on the ACW. - - 'Hidden' attributes can be accessed by typing the beginning of hidden - name after a '.'. e.g. '_'. This allows access to modules with - '__all__' set, or to class-private attributes. - - Completions and the 'Expand Word' facility can save a lot of typing! - - Completions are currently limited to those in the namespaces. Names in - an Edit window which are not via __main__ or sys.modules will not be - found. Run the module once with your imports to correct this - situation. Note that IDLE itself places quite a few modules in - sys.modules, so much can be found by default, e.g. the re module. - - If you don't like the ACW popping up unbidden, simply make the delay - longer or disable the extension. OTOH, you could make the delay zero. - - You could also switch off the CallTips extension. (We will be adding - a delay to the call tip window.) + Completions are supplied for functions, classes, and attributes of + classes, both built-in and user-defined. Completions are also provided + for filenames. + + The AutoCompleteWindow (ACW) will open after a predefined delay + (default is two seconds) after a '.' or (in a string) an os.sep is + typed. If after one of those characters (plus zero or more other + characters) a tab is typed the ACW will open immediately if a possible + continuation is found. + + If there is only one possible completion for the characters entered, a + tab will supply that completion without opening the ACW. + + 'Show Completions' will force open a completions window, by default the + Control-space keys will open a completions window. In an empty + string, this will contain the files in the current directory. On a + blank line, it will contain the built-in and user-defined functions and + classes in the current name spaces, plus any modules imported. If some + characters have been entered, the ACW will attempt to be more specific. + + If string of characters is typed, the ACW selection will jump to the + entry most closely matching those characters. Entering a tab will cause + the longest non-ambiguous match to be entered in the Edit window or + Shell. Two tabs in a row will supply the current ACW selection, as + will return or a double click. Cursor keys, Page Up/Down, mouse + selection, and the scroll wheel all operate on the ACW. + + "Hidden" attributes can be accessed by typing the beginning of hidden + name after a '.', e.g. '_'. This allows access to modules with + '__all__' set, or to class-private attributes. + + Completions and the 'Expand Word' facility can save a lot of typing! + + Completions are currently limited to those in the namespaces. Names in + an Editor window which are not via __main__ or sys.modules will not be + found. Run the module once with your imports to correct this + situation. Note that IDLE itself places quite a few modules in + sys.modules, so much can be found by default, e.g. the re module. + + If you don't like the ACW popping up unbidden, simply make the delay + longer or disable the extension. Or another option is the delay could + be set to zero. Another alternative to preventing ACW popups is to + disable the call tips extension. Python Shell window: - Control-c interrupts executing command. - Control-d sends end-of-file; closes window if typed at >>> prompt. + Control-c interrupts executing command. + Control-d sends end-of-file; closes window if typed at >>> prompt. + Alt-/ expand word is also useful to reduce typing. Command history: - Alt-p retrieves previous command matching what you have typed. - Alt-n retrieves next. - (These are Control-p, Control-n on OS X) - Return while cursor is on a previous command retrieves that command. - Expand word is also useful to reduce typing. + Alt-p retrieves previous command matching what you have typed. On OS X + use Control-p. + Alt-n retrieves next. On OS X use Control-n. + Return while cursor is on a previous command retrieves that command. Syntax colors: - The coloring is applied in a background "thread", so you may - occasionally see uncolorized text. To change the color - scheme, use the Configure IDLE / Highlighting dialog. + The coloring is applied in a background "thread", so you may + occasionally see uncolorized text. To change the color + scheme, use the Configure IDLE / Highlighting dialog. Python default syntax colors: - Keywords orange - Builtins royal purple - Strings green - Comments red - Definitions blue + Keywords orange + Builtins royal purple + Strings green + Comments red + Definitions blue Shell default colors: - Console output brown - stdout blue - stderr red - stdin black + Console output brown + stdout blue + stderr red + stdin black Other preferences: - The font preferences, keybinding, and startup preferences can - be changed using the Settings dialog. + The font preferences, highlighting, keys, and general preferences can + be changed via the Configure IDLE menu option. Be sure to note that + keys can be user defined, IDLE ships with four built in key sets. In + addition a user can create a custom key set in the Configure IDLE + dialog under the keys tab. Command line usage: - Enter idle -h at the command prompt to get a usage message. - -Running without a subprocess: - - If IDLE is started with the -n command line switch it will run in a - single process and will not create the subprocess which runs the RPC - Python execution server. This can be useful if Python cannot create - the subprocess or the RPC socket interface on your platform. However, - in this mode user code is not isolated from IDLE itself. Also, the - environment is not restarted when Run/Run Module (F5) is selected. If - your code has been modified, you must reload() the affected modules and - re-import any specific items (e.g. from foo import baz) if the changes - are to take effect. For these reasons, it is preferable to run IDLE - with the default subprocess if at all possible. + Enter idle -h at the command prompt to get a usage message. + + idle.py [-c command] [-d] [-e] [-s] [-t title] [arg] ... + + -c command run this command + -d enable debugger + -e edit mode; arguments are files to be edited + -s run $IDLESTARTUP or $PYTHONSTARTUP first + -t title set title of shell window + + If there are arguments: + 1. If -e is used, arguments are files opened for editing and sys.argv + reflects the arguments passed to IDLE itself. + 2. Otherwise, if -c is used, all arguments are placed in + sys.argv[1:...], with sys.argv[0] set to -c. + 3. Otherwise, if neither -e nor -c is used, the first argument is a + script which is executed with the remaining arguments in + sys.argv[1:...] and sys.argv[0] set to the script name. If the + script name is -, no script is executed but an interactive Python + session is started; the arguments are still available in sys.argv. + +Running without a subprocess: (DEPRECATED in Python 3.4 see Issue 16123) + + If IDLE is started with the -n command line switch it will run in a + single process and will not create the subprocess which runs the RPC + Python execution server. This can be useful if Python cannot create + the subprocess or the RPC socket interface on your platform. However, + in this mode user code is not isolated from IDLE itself. Also, the + environment is not restarted when Run/Run Module (F5) is selected. If + your code has been modified, you must reload() the affected modules and + re-import any specific items (e.g. from foo import baz) if the changes + are to take effect. For these reasons, it is preferable to run IDLE + with the default subprocess if at all possible. Extensions: - IDLE contains an extension facility. See the beginning of - config-extensions.def in the idlelib directory for further information. - The default extensions are currently: - - FormatParagraph - AutoExpand - ZoomHeight - ScriptBinding - CallTips - ParenMatch - AutoComplete - CodeContext + IDLE contains an extension facility. See the beginning of + config-extensions.def in the idlelib directory for further information. + The default extensions are currently: + + FormatParagraph + AutoExpand + ZoomHeight + ScriptBinding + CallTips + ParenMatch + AutoComplete + CodeContext diff --git a/Lib/idlelib/idle.bat b/Lib/idlelib/idle.bat index e77b96e9b5..3d619a37ee 100755 --- a/Lib/idlelib/idle.bat +++ b/Lib/idlelib/idle.bat @@ -1,4 +1,4 @@ -@echo off -rem Start IDLE using the appropriate Python interpreter -set CURRDIR=%~dp0 -start "IDLE" "%CURRDIR%..\..\pythonw.exe" "%CURRDIR%idle.pyw" %1 %2 %3 %4 %5 %6 %7 %8 %9 +@echo off
+rem Start IDLE using the appropriate Python interpreter
+set CURRDIR=%~dp0
+start "IDLE" "%CURRDIR%..\..\pythonw.exe" "%CURRDIR%idle.pyw" %1 %2 %3 %4 %5 %6 %7 %8 %9
diff --git a/Lib/idlelib/idle.pyw b/Lib/idlelib/idle.pyw index 0db5fd426e..142cb322ac 100644 --- a/Lib/idlelib/idle.pyw +++ b/Lib/idlelib/idle.pyw @@ -2,20 +2,16 @@ try: import idlelib.PyShell except ImportError: # IDLE is not installed, but maybe PyShell is on sys.path: - try: - from . import PyShell - except ImportError: - raise - else: - import os - idledir = os.path.dirname(os.path.abspath(PyShell.__file__)) - if idledir != os.getcwd(): - # We're not in the IDLE directory, help the subprocess find run.py - pypath = os.environ.get('PYTHONPATH', '') - if pypath: - os.environ['PYTHONPATH'] = pypath + ':' + idledir - else: - os.environ['PYTHONPATH'] = idledir - PyShell.main() + from . import PyShell + import os + idledir = os.path.dirname(os.path.abspath(PyShell.__file__)) + if idledir != os.getcwd(): + # We're not in the IDLE directory, help the subprocess find run.py + pypath = os.environ.get('PYTHONPATH', '') + if pypath: + os.environ['PYTHONPATH'] = pypath + ':' + idledir + else: + os.environ['PYTHONPATH'] = idledir + PyShell.main() else: idlelib.PyShell.main() diff --git a/Lib/idlelib/idle_test/README.txt b/Lib/idlelib/idle_test/README.txt index 6b92483193..2339926ef3 100644 --- a/Lib/idlelib/idle_test/README.txt +++ b/Lib/idlelib/idle_test/README.txt @@ -1,14 +1,24 @@ README FOR IDLE TESTS IN IDLELIB.IDLE_TEST +0. Quick Start + +Automated unit tests were added in 2.7 for Python 2.x and 3.3 for Python 3.x. +To run the tests from a command line: + +python -m test.test_idle + +Human-mediated tests were added later in 2.7 and in 3.4. + +python -m idlelib.idle_test.htest + 1. Test Files The idle directory, idlelib, has over 60 xyz.py files. The idle_test -subdirectory should contain a test_xyy.py for each. (For test modules, make -'xyz' lower case, and possibly shorten it.) Each file should start with the -something like the following template, with the blanks after after '.' and 'as', -and before and after '_' filled in. ---- +subdirectory should contain a test_xyz.py for each, where 'xyz' is lowercased +even if xyz.py is not. Here is a possible template, with the blanks after after +'.' and 'as', and before and after '_' to be filled in. + import unittest from test.support import requires import idlelib. as @@ -18,34 +28,33 @@ class _Test(unittest.TestCase): def test_(self): if __name__ == '__main__': - unittest.main(verbosity=2, exit=2) ---- -Idle tests are run with unittest; do not use regrtest's test_main. + unittest.main(verbosity=2) + +Add the following at the end of xyy.py, with the appropriate name added after +'test_'. Some files already have something like this for htest. If so, insert +the import and unittest.main lines before the htest lines. -Once test_xyy is written, the following should go at the end of xyy.py, -with xyz (lowercased) added after 'test_'. ---- if __name__ == "__main__": - from test import support; support.use_resources = ['gui'] import unittest unittest.main('idlelib.idle_test.test_', verbosity=2, exit=False) ---- -2. Gui Tests -Gui tests need 'requires' and 'use_resources' from test.support -(test.test_support in 2.7). A test is a gui test if it creates a Tk root or -master object either directly or indirectly by instantiating a tkinter or -idle class. For the benefit of buildbot machines that do not have a graphics -screen, gui tests must be 'guarded' by "requires('gui')" in a setUp -function or method. This will typically be setUpClass. +2. GUI Tests + +When run as part of the Python test suite, Idle gui tests need to run +test.support.requires('gui') (test.test_support in 2.7). A test is a gui test +if it creates a Tk root or master object either directly or indirectly by +instantiating a tkinter or idle class. For the benefit of test processes that +either have no graphical environment available or are not allowed to use it, gui +tests must be 'guarded' by "requires('gui')" in a setUp function or method. +This will typically be setUpClass. + +To avoid interfering with other gui tests, all gui objects must be destroyed and +deleted by the end of the test. Widgets, such as a Tk root, created in a setUpX +function, should be destroyed in the corresponding tearDownX. Module and class +widget attributes should also be deleted.. -To avoid interfering with other gui tests, all gui objects must be destroyed -and deleted by the end of the test. If a widget, such as a Tk root, is created -in a setUpX function, destroy it in the corresponding tearDownX. For module -and class attributes, also delete the widget. ---- @classmethod def setUpClass(cls): requires('gui') @@ -55,56 +64,80 @@ and class attributes, also delete the widget. def tearDownClass(cls): cls.root.destroy() del cls.root ---- - -Support.requires('gui') returns true if it is either called in a main module -(which never happens on buildbots) or if use_resources contains 'gui'. -Use_resources is set by test.regrtest but not by unittest. So when running -tests in another module with unittest, we set it ourselves, as in the xyz.py -template above. - -Since non-gui tests always run, but gui tests only sometimes, tests of non-gui -operations should best avoid needing a gui. Methods that make incidental use of -tkinter (tk) variables and messageboxes can do this by using the mock classes in -idle_test/mock_tk.py. There is also a mock text that will handle some uses of the -tk Text widget. - - -3. Running Tests - -Assume that xyz.py and test_xyz.py end with the "if __name__" statements given -above. In Idle, pressing F5 in an editor window with either loaded will run all -tests in the test_xyz file with the version of Python running Idle. The test -report and any tracebacks will appear in the Shell window. The options in these -"if __name__" statements are appropriate for developers running (as opposed to -importing) either of the files during development: verbosity=2 lists all test -methods in the file; exit=False avoids a spurious sys.exit traceback that would -otherwise occur when running in Idle. The following command lines also run -all test methods, including gui tests, in test_xyz.py. (The exceptions are that -idlelib and idlelib.idle start Idle and idlelib.PyShell should (issue 18330).) - -python -m idlelib.xyz # With the capitalization of the xyz module + + +Requires('gui') causes the test(s) it guards to be skipped if any of +a few conditions are met: + + - The tests are being run by regrtest.py, and it was started without enabling + the "gui" resource with the "-u" command line option. + + - The tests are being run on Windows by a service that is not allowed to + interact with the graphical environment. + + - The tests are being run on Mac OSX in a process that cannot make a window + manager connection. + + - tkinter.Tk cannot be successfully instantiated for some reason. + + - test.support.use_resources has been set by something other than + regrtest.py and does not contain "gui". + +Tests of non-gui operations should avoid creating tk widgets. Incidental uses of +tk variables and messageboxes can be replaced by the mock classes in +idle_test/mock_tk.py. The mock text handles some uses of the tk Text widget. + + +3. Running Unit Tests + +Assume that xyz.py and test_xyz.py both end with a unittest.main() call. +Running either from an Idle editor runs all tests in the test_xyz file with the +version of Python running Idle. Test output appears in the Shell window. The +'verbosity=2' option lists all test methods in the file, which is appropriate +when developing tests. The 'exit=False' option is needed in xyx.py files when an +htest follows. + +The following command lines also run all test methods, including +gui tests, in test_xyz.py. (Both '-m idlelib' and '-m idlelib.idle' start +Idle and so cannot run tests.) + +python -m idlelib.xyz python -m idlelib.idle_test.test_xyz -To run all idle_test/test_*.py tests, either interactively -('>>>', with unittest imported) or from a command line, use one of the -following. (Notes: unittest does not run gui tests; in 2.7, 'test ' (with the -space) is 'test.regrtest '; where present, -v and -ugui can be omitted.) +The following runs all idle_test/test_*.py tests interactively. + +>>> import unittest +>>> unittest.main('idlelib.idle_test', verbosity=2) + +The following run all Idle tests at a command line. Option '-v' is the same as +'verbosity=2'. (For 2.7, replace 'test' in the second line with +'test.regrtest'.) ->>> unittest.main('idlelib.idle_test', verbosity=2, exit=False) python -m unittest -v idlelib.idle_test python -m test -v -ugui test_idle python -m test.test_idle The idle tests are 'discovered' by idlelib.idle_test.__init__.load_tests, which is also imported into test.test_idle. Normally, neither file should be -changed when working on individual test modules. The third command runs runs +changed when working on individual test modules. The third command runs unittest indirectly through regrtest. The same happens when the entire test suite is run with 'python -m test'. So that command must work for buildbots to stay green. Idle tests must not disturb the environment in a way that makes other tests fail (issue 18081). To run an individual Testcase or test method, extend the dotted name given to -unittest on the command line. (But gui tests will not this way.) +unittest on the command line. python -m unittest -v idlelib.idle_test.test_xyz.Test_case.test_meth + + +4. Human-mediated Tests + +Human-mediated tests are widget tests that cannot be automated but need human +verification. They are contained in idlelib/idle_test/htest.py, which has +instructions. (Some modules need an auxiliary function, identified with # htest +# on the header line.) The set is about complete, though some tests need +improvement. To run all htests, run the htest file from an editor or from the +command line with: + +python -m idlelib.idle_test.htest diff --git a/Lib/idlelib/idle_test/htest.py b/Lib/idlelib/idle_test/htest.py new file mode 100644 index 0000000000..3e24518a0f --- /dev/null +++ b/Lib/idlelib/idle_test/htest.py @@ -0,0 +1,401 @@ +'''Run human tests of Idle's window, dialog, and popup widgets. + +run(*tests) +Create a master Tk window. Within that, run each callable in tests +after finding the matching test spec in this file. If tests is empty, +run an htest for each spec dict in this file after finding the matching +callable in the module named in the spec. Close the window to skip or +end the test. + +In a tested module, let X be a global name bound to a callable (class +or function) whose .__name__ attrubute is also X (the usual situation). +The first parameter of X must be 'parent'. When called, the parent +argument will be the root window. X must create a child Toplevel +window (or subclass thereof). The Toplevel may be a test widget or +dialog, in which case the callable is the corresonding class. Or the +Toplevel may contain the widget to be tested or set up a context in +which a test widget is invoked. In this latter case, the callable is a +wrapper function that sets up the Toplevel and other objects. Wrapper +function names, such as _editor_window', should start with '_'. + + +End the module with + +if __name__ == '__main__': + <unittest, if there is one> + from idlelib.idle_test.htest import run + run(X) + +To have wrapper functions and test invocation code ignored by coveragepy +reports, put '# htest #' on the def statement header line. + +def _wrapper(parent): # htest # + +Also make sure that the 'if __name__' line matches the above. Then have +make sure that .coveragerc includes the following. + +[report] +exclude_lines = + .*# htest # + if __name__ == .__main__.: + +(The "." instead of "'" is intentional and necessary.) + + +To run any X, this file must contain a matching instance of the +following template, with X.__name__ prepended to '_spec'. +When all tests are run, the prefix is use to get X. + +_spec = { + 'file': '', + 'kwds': {'title': ''}, + 'msg': "" + } + +file (no .py): run() imports file.py. +kwds: augmented with {'parent':root} and passed to X as **kwds. +title: an example kwd; some widgets need this, delete if not. +msg: master window hints about testing the widget. + + +Modules and classes not being tested at the moment: +PyShell.PyShellEditorWindow +Debugger.Debugger +AutoCompleteWindow.AutoCompleteWindow +OutputWindow.OutputWindow (indirectly being tested with grep test) +''' + +from importlib import import_module +from idlelib.macosxSupport import _initializeTkVariantTests +import tkinter as tk + +AboutDialog_spec = { + 'file': 'aboutDialog', + 'kwds': {'title': 'aboutDialog test', + '_htest': True, + }, + 'msg': "Test every button. Ensure Python, TK and IDLE versions " + "are correctly displayed.\n [Close] to exit.", + } + +_calltip_window_spec = { + 'file': 'CallTipWindow', + 'kwds': {}, + 'msg': "Typing '(' should display a calltip.\n" + "Typing ') should hide the calltip.\n" + } + +_class_browser_spec = { + 'file': 'ClassBrowser', + 'kwds': {}, + 'msg': "Inspect names of module, class(with superclass if " + "applicable), methods and functions.\nToggle nested items.\n" + "Double clicking on items prints a traceback for an exception " + "that is ignored." + } + +_color_delegator_spec = { + 'file': 'ColorDelegator', + 'kwds': {}, + 'msg': "The text is sample Python code.\n" + "Ensure components like comments, keywords, builtins,\n" + "string, definitions, and break are correctly colored.\n" + "The default color scheme is in idlelib/config-highlight.def" + } + +ConfigDialog_spec = { + 'file': 'configDialog', + 'kwds': {'title': 'ConfigDialogTest', + '_htest': True,}, + 'msg': "IDLE preferences dialog.\n" + "In the 'Fonts/Tabs' tab, changing font face, should update the " + "font face of the text in the area below it.\nIn the " + "'Highlighting' tab, try different color schemes. Clicking " + "items in the sample program should update the choices above it." + "\nIn the 'Keys', 'General' and 'Extensions' tabs, test settings" + "of interest." + "\n[Ok] to close the dialog.[Apply] to apply the settings and " + "and [Cancel] to revert all changes.\nRe-run the test to ensure " + "changes made have persisted." + } + +# TODO Improve message +_dyn_option_menu_spec = { + 'file': 'dynOptionMenuWidget', + 'kwds': {}, + 'msg': "Select one of the many options in the 'old option set'.\n" + "Click the button to change the option set.\n" + "Select one of the many options in the 'new option set'." + } + +# TODO edit wrapper +_editor_window_spec = { + 'file': 'EditorWindow', + 'kwds': {}, + 'msg': "Test editor functions of interest.\n" + "Best to close editor first." + } + +GetCfgSectionNameDialog_spec = { + 'file': 'configSectionNameDialog', + 'kwds': {'title':'Get Name', + 'message':'Enter something', + 'used_names': {'abc'}, + '_htest': True}, + 'msg': "After the text entered with [Ok] is stripped, <nothing>, " + "'abc', or more that 30 chars are errors.\n" + "Close 'Get Name' with a valid entry (printed to Shell), " + "[Cancel], or [X]", + } + +GetHelpSourceDialog_spec = { + 'file': 'configHelpSourceEdit', + 'kwds': {'title': 'Get helpsource', + '_htest': True}, + 'msg': "Enter menu item name and help file path\n " + "<nothing> and more than 30 chars are invalid menu item names.\n" + "<nothing>, file does not exist are invalid path items.\n" + "Test for incomplete web address for help file path.\n" + "A valid entry will be printed to shell with [0k].\n" + "[Cancel] will print None to shell", + } + +# Update once issue21519 is resolved. +GetKeysDialog_spec = { + 'file': 'keybindingDialog', + 'kwds': {'title': 'Test keybindings', + 'action': 'find-again', + 'currentKeySequences': [''] , + '_htest': True, + }, + 'msg': "Test for different key modifier sequences.\n" + "<nothing> is invalid.\n" + "No modifier key is invalid.\n" + "Shift key with [a-z],[0-9], function key, move key, tab, space" + "is invalid.\nNo validity checking if advanced key binding " + "entry is used." + } + +_grep_dialog_spec = { + 'file': 'GrepDialog', + 'kwds': {}, + 'msg': "Click the 'Show GrepDialog' button.\n" + "Test the various 'Find-in-files' functions.\n" + "The results should be displayed in a new '*Output*' window.\n" + "'Right-click'->'Goto file/line' anywhere in the search results " + "should open that file \nin a new EditorWindow." + } + +_io_binding_spec = { + 'file': 'IOBinding', + 'kwds': {}, + 'msg': "Test the following bindings.\n" + "<Control-o> to open file from dialog.\n" + "Edit the file.\n" + "<Control-s> to save the file.\n" + "Check that changes were saved by opening the file elsewhere." + } + +_multi_call_spec = { + 'file': 'MultiCall', + 'kwds': {}, + 'msg': "The following actions should trigger a print to console or IDLE" + " Shell.\nEntering and leaving the text area, key entry, " + "<Control-Key>,\n<Alt-Key-a>, <Control-Key-a>, " + "<Alt-Control-Key-a>, \n<Control-Button-1>, <Alt-Button-1> and " + "focusing out of the window\nare sequences to be tested." + } + +_multistatus_bar_spec = { + 'file': 'MultiStatusBar', + 'kwds': {}, + 'msg': "Ensure presence of multi-status bar below text area.\n" + "Click 'Update Status' to change the multi-status text" + } + +_object_browser_spec = { + 'file': 'ObjectBrowser', + 'kwds': {}, + 'msg': "Double click on items upto the lowest level.\n" + "Attributes of the objects and related information " + "will be displayed side-by-side at each level." + } + +_path_browser_spec = { + 'file': 'PathBrowser', + 'kwds': {}, + 'msg': "Test for correct display of all paths in sys.path.\n" + "Toggle nested items upto the lowest level.\n" + "Double clicking on an item prints a traceback\n" + "for an exception that is ignored." + } + +_percolator_spec = { + 'file': 'Percolator', + 'kwds': {}, + 'msg': "There are two tracers which can be toggled using a checkbox.\n" + "Toggling a tracer 'on' by checking it should print tracer" + "output to the console or to the IDLE shell.\n" + "If both the tracers are 'on', the output from the tracer which " + "was switched 'on' later, should be printed first\n" + "Test for actions like text entry, and removal." + } + +_replace_dialog_spec = { + 'file': 'ReplaceDialog', + 'kwds': {}, + 'msg': "Click the 'Replace' button.\n" + "Test various replace options in the 'Replace dialog'.\n" + "Click [Close] or [X] to close the 'Replace Dialog'." + } + +_search_dialog_spec = { + 'file': 'SearchDialog', + 'kwds': {}, + 'msg': "Click the 'Search' button.\n" + "Test various search options in the 'Search dialog'.\n" + "Click [Close] or [X] to close the 'Search Dialog'." + } + +_scrolled_list_spec = { + 'file': 'ScrolledList', + 'kwds': {}, + 'msg': "You should see a scrollable list of items\n" + "Selecting (clicking) or double clicking an item " + "prints the name to the console or Idle shell.\n" + "Right clicking an item will display a popup." + } + +show_idlehelp_spec = { + 'file': 'help', + 'kwds': {}, + 'msg': "If the help text displays, this works.\n" + "Text is selectable. Window is scrollable." + } + +_stack_viewer_spec = { + 'file': 'StackViewer', + 'kwds': {}, + 'msg': "A stacktrace for a NameError exception.\n" + "Expand 'idlelib ...' and '<locals>'.\n" + "Check that exc_value, exc_tb, and exc_type are correct.\n" + } + +_tabbed_pages_spec = { + 'file': 'tabbedpages', + 'kwds': {}, + 'msg': "Toggle between the two tabs 'foo' and 'bar'\n" + "Add a tab by entering a suitable name for it.\n" + "Remove an existing tab by entering its name.\n" + "Remove all existing tabs.\n" + "<nothing> is an invalid add page and remove page name.\n" + } + +TextViewer_spec = { + 'file': 'textView', + 'kwds': {'title': 'Test textView', + 'text':'The quick brown fox jumps over the lazy dog.\n'*35, + '_htest': True}, + 'msg': "Test for read-only property of text.\n" + "Text is selectable. Window is scrollable.", + } + +_tooltip_spec = { + 'file': 'ToolTip', + 'kwds': {}, + 'msg': "Place mouse cursor over both the buttons\n" + "A tooltip should appear with some text." + } + +_tree_widget_spec = { + 'file': 'TreeWidget', + 'kwds': {}, + 'msg': "The canvas is scrollable.\n" + "Click on folders upto to the lowest level." + } + +_undo_delegator_spec = { + 'file': 'UndoDelegator', + 'kwds': {}, + 'msg': "Click [Undo] to undo any action.\n" + "Click [Redo] to redo any action.\n" + "Click [Dump] to dump the current state " + "by printing to the console or the IDLE shell.\n" + } + +_widget_redirector_spec = { + 'file': 'WidgetRedirector', + 'kwds': {}, + 'msg': "Every text insert should be printed to the console." + "or the IDLE shell." + } + +def run(*tests): + root = tk.Tk() + root.title('IDLE htest') + root.resizable(0, 0) + _initializeTkVariantTests(root) + + # a scrollable Label like constant width text widget. + frameLabel = tk.Frame(root, padx=10) + frameLabel.pack() + text = tk.Text(frameLabel, wrap='word') + text.configure(bg=root.cget('bg'), relief='flat', height=4, width=70) + scrollbar = tk.Scrollbar(frameLabel, command=text.yview) + text.config(yscrollcommand=scrollbar.set) + scrollbar.pack(side='right', fill='y', expand=False) + text.pack(side='left', fill='both', expand=True) + + test_list = [] # List of tuples of the form (spec, callable widget) + if tests: + for test in tests: + test_spec = globals()[test.__name__ + '_spec'] + test_spec['name'] = test.__name__ + test_list.append((test_spec, test)) + else: + for k, d in globals().items(): + if k.endswith('_spec'): + test_name = k[:-5] + test_spec = d + test_spec['name'] = test_name + mod = import_module('idlelib.' + test_spec['file']) + test = getattr(mod, test_name) + test_list.append((test_spec, test)) + + test_name = tk.StringVar('') + callable_object = None + test_kwds = None + + def next(): + + nonlocal test_name, callable_object, test_kwds + if len(test_list) == 1: + next_button.pack_forget() + test_spec, callable_object = test_list.pop() + test_kwds = test_spec['kwds'] + test_kwds['parent'] = root + test_name.set('Test ' + test_spec['name']) + + text.configure(state='normal') # enable text editing + text.delete('1.0','end') + text.insert("1.0",test_spec['msg']) + text.configure(state='disabled') # preserve read-only property + + def run_test(): + widget = callable_object(**test_kwds) + try: + print(widget.result) + except AttributeError: + pass + + button = tk.Button(root, textvariable=test_name, command=run_test) + button.pack() + next_button = tk.Button(root, text="Next", command=next) + next_button.pack() + + next() + + root.mainloop() + +if __name__ == '__main__': + run() diff --git a/Lib/idlelib/idle_test/mock_idle.py b/Lib/idlelib/idle_test/mock_idle.py index c364a24dac..1672a3413e 100644 --- a/Lib/idlelib/idle_test/mock_idle.py +++ b/Lib/idlelib/idle_test/mock_idle.py @@ -5,6 +5,33 @@ Attributes and methods will be added as needed for tests. from idlelib.idle_test.mock_tk import Text +class Func: + '''Mock function captures args and returns result set by test. + + Attributes: + self.called - records call even if no args, kwds passed. + self.result - set by init, returned by call. + self.args - captures positional arguments. + self.kwds - captures keyword arguments. + + Most common use will probably be to mock methods. + Mock_tk.Var and Mbox_func are special variants of this. + ''' + def __init__(self, result=None): + self.called = False + self.result = result + self.args = None + self.kwds = None + def __call__(self, *args, **kwds): + self.called = True + self.args = args + self.kwds = kwds + if isinstance(self.result, BaseException): + raise self.result + else: + return self.result + + class Editor: '''Minimally imitate EditorWindow.EditorWindow class. ''' @@ -17,6 +44,7 @@ class Editor: last = self.text.index('end') return first, last + class UndoDelegator: '''Minimally imitate UndoDelegator,UndoDelegator class. ''' diff --git a/Lib/idlelib/idle_test/mock_tk.py b/Lib/idlelib/idle_test/mock_tk.py index 762bbc905d..86fe84884f 100644 --- a/Lib/idlelib/idle_test/mock_tk.py +++ b/Lib/idlelib/idle_test/mock_tk.py @@ -1,9 +1,27 @@ """Classes that replace tkinter gui objects used by an object being tested. -A gui object is anything with a master or parent paramenter, which is typically -required in spite of what the doc strings say. +A gui object is anything with a master or parent parameter, which is +typically required in spite of what the doc strings say. """ +class Event: + '''Minimal mock with attributes for testing event handlers. + + This is not a gui object, but is used as an argument for callbacks + that access attributes of the event passed. If a callback ignores + the event, other than the fact that is happened, pass 'event'. + + Keyboard, mouse, window, and other sources generate Event instances. + Event instances have the following attributes: serial (number of + event), time (of event), type (of event as number), widget (in which + event occurred), and x,y (position of mouse). There are other + attributes for specific events, such as keycode for key events. + tkinter.Event.__doc__ has more but is still not complete. + ''' + def __init__(self, **kwds): + "Create event with attributes needed for test" + self.__dict__.update(kwds) + class Var: "Use for String/Int/BooleanVar: incomplete" def __init__(self, master=None, value=None, name=None): @@ -20,9 +38,10 @@ class Mbox_func: Instead of displaying a message box, the mock's call method saves the arguments as instance attributes, which test functions can then examime. + The test can set the result returned to ask function """ - def __init__(self): - self.result = None # The return for all show funcs + def __init__(self, result=None): + self.result = result # Return None for all show funcs def __call__(self, title, message, *args, **kwds): # Save all args for possible examination by tester self.title = title @@ -97,7 +116,7 @@ class Text: """Return a (line, char) tuple of int indexes into self.data. This implements .index without converting the result back to a string. - The result is contrained by the number of lines and linelengths of + The result is constrained by the number of lines and linelengths of self.data. For many indexes, the result is initially (1, 0). The input index may have any of several possible forms: diff --git a/Lib/idlelib/idle_test/test_autocomplete.py b/Lib/idlelib/idle_test/test_autocomplete.py new file mode 100644 index 0000000000..3a2192e8af --- /dev/null +++ b/Lib/idlelib/idle_test/test_autocomplete.py @@ -0,0 +1,143 @@ +import unittest +from test.support import requires +from tkinter import Tk, Text + +import idlelib.AutoComplete as ac +import idlelib.AutoCompleteWindow as acw +import idlelib.macosxSupport as mac +from idlelib.idle_test.mock_idle import Func +from idlelib.idle_test.mock_tk import Event + +class AutoCompleteWindow: + def complete(): + return + +class DummyEditwin: + def __init__(self, root, text): + self.root = root + self.text = text + self.indentwidth = 8 + self.tabwidth = 8 + self.context_use_ps1 = True + + +class AutoCompleteTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + mac.setupApp(cls.root, None) + cls.text = Text(cls.root) + cls.editor = DummyEditwin(cls.root, cls.text) + + @classmethod + def tearDownClass(cls): + cls.root.destroy() + del cls.text + del cls.editor + del cls.root + + def setUp(self): + self.editor.text.delete('1.0', 'end') + self.autocomplete = ac.AutoComplete(self.editor) + + def test_init(self): + self.assertEqual(self.autocomplete.editwin, self.editor) + + def test_make_autocomplete_window(self): + testwin = self.autocomplete._make_autocomplete_window() + self.assertIsInstance(testwin, acw.AutoCompleteWindow) + + def test_remove_autocomplete_window(self): + self.autocomplete.autocompletewindow = ( + self.autocomplete._make_autocomplete_window()) + self.autocomplete._remove_autocomplete_window() + self.assertIsNone(self.autocomplete.autocompletewindow) + + def test_force_open_completions_event(self): + # Test that force_open_completions_event calls _open_completions + o_cs = Func() + self.autocomplete.open_completions = o_cs + self.autocomplete.force_open_completions_event('event') + self.assertEqual(o_cs.args, (True, False, True)) + + def test_try_open_completions_event(self): + Equal = self.assertEqual + autocomplete = self.autocomplete + trycompletions = self.autocomplete.try_open_completions_event + o_c_l = Func() + autocomplete._open_completions_later = o_c_l + + # _open_completions_later should not be called with no text in editor + trycompletions('event') + Equal(o_c_l.args, None) + + # _open_completions_later should be called with COMPLETE_ATTRIBUTES (1) + self.text.insert('1.0', 're.') + trycompletions('event') + Equal(o_c_l.args, (False, False, False, 1)) + + # _open_completions_later should be called with COMPLETE_FILES (2) + self.text.delete('1.0', 'end') + self.text.insert('1.0', '"./Lib/') + trycompletions('event') + Equal(o_c_l.args, (False, False, False, 2)) + + def test_autocomplete_event(self): + Equal = self.assertEqual + autocomplete = self.autocomplete + + # Test that the autocomplete event is ignored if user is pressing a + # modifier key in addition to the tab key + ev = Event(mc_state=True) + self.assertIsNone(autocomplete.autocomplete_event(ev)) + del ev.mc_state + + # If autocomplete window is open, complete() method is called + self.text.insert('1.0', 're.') + # This must call autocomplete._make_autocomplete_window() + Equal(self.autocomplete.autocomplete_event(ev), 'break') + + # If autocomplete window is not active or does not exist, + # open_completions is called. Return depends on its return. + autocomplete._remove_autocomplete_window() + o_cs = Func() # .result = None + autocomplete.open_completions = o_cs + Equal(self.autocomplete.autocomplete_event(ev), None) + Equal(o_cs.args, (False, True, True)) + o_cs.result = True + Equal(self.autocomplete.autocomplete_event(ev), 'break') + Equal(o_cs.args, (False, True, True)) + + def test_open_completions_later(self): + # Test that autocomplete._delayed_completion_id is set + pass + + def test_delayed_open_completions(self): + # Test that autocomplete._delayed_completion_id set to None and that + # open_completions only called if insertion index is the same as + # _delayed_completion_index + pass + + def test_open_completions(self): + # Test completions of files and attributes as well as non-completion + # of errors + pass + + def test_fetch_completions(self): + # Test that fetch_completions returns 2 lists: + # For attribute completion, a large list containing all variables, and + # a small list containing non-private variables. + # For file completion, a large list containing all files in the path, + # and a small list containing files that do not start with '.' + pass + + def test_get_entity(self): + # Test that a name is in the namespace of sys.modules and + # __main__.__dict__ + pass + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_autoexpand.py b/Lib/idlelib/idle_test/test_autoexpand.py new file mode 100644 index 0000000000..7ca941ec29 --- /dev/null +++ b/Lib/idlelib/idle_test/test_autoexpand.py @@ -0,0 +1,141 @@ +"""Unit tests for idlelib.AutoExpand""" +import unittest +from test.support import requires +from tkinter import Text, Tk +#from idlelib.idle_test.mock_tk import Text +from idlelib.AutoExpand import AutoExpand + + +class Dummy_Editwin: + # AutoExpand.__init__ only needs .text + def __init__(self, text): + self.text = text + +class AutoExpandTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + if 'tkinter' in str(Text): + requires('gui') + cls.tk = Tk() + cls.text = Text(cls.tk) + else: + cls.text = Text() + cls.auto_expand = AutoExpand(Dummy_Editwin(cls.text)) + + @classmethod + def tearDownClass(cls): + if hasattr(cls, 'tk'): + cls.tk.destroy() + del cls.tk + del cls.text, cls.auto_expand + + def tearDown(self): + self.text.delete('1.0', 'end') + + def test_get_prevword(self): + text = self.text + previous = self.auto_expand.getprevword + equal = self.assertEqual + + equal(previous(), '') + + text.insert('insert', 't') + equal(previous(), 't') + + text.insert('insert', 'his') + equal(previous(), 'this') + + text.insert('insert', ' ') + equal(previous(), '') + + text.insert('insert', 'is') + equal(previous(), 'is') + + text.insert('insert', '\nsample\nstring') + equal(previous(), 'string') + + text.delete('3.0', 'insert') + equal(previous(), '') + + text.delete('1.0', 'end') + equal(previous(), '') + + def test_before_only(self): + previous = self.auto_expand.getprevword + expand = self.auto_expand.expand_word_event + equal = self.assertEqual + + self.text.insert('insert', 'ab ac bx ad ab a') + equal(self.auto_expand.getwords(), ['ab', 'ad', 'ac', 'a']) + expand('event') + equal(previous(), 'ab') + expand('event') + equal(previous(), 'ad') + expand('event') + equal(previous(), 'ac') + expand('event') + equal(previous(), 'a') + + def test_after_only(self): + # Also add punctuation 'noise' that should be ignored. + text = self.text + previous = self.auto_expand.getprevword + expand = self.auto_expand.expand_word_event + equal = self.assertEqual + + text.insert('insert', 'a, [ab] ac: () bx"" cd ac= ad ya') + text.mark_set('insert', '1.1') + equal(self.auto_expand.getwords(), ['ab', 'ac', 'ad', 'a']) + expand('event') + equal(previous(), 'ab') + expand('event') + equal(previous(), 'ac') + expand('event') + equal(previous(), 'ad') + expand('event') + equal(previous(), 'a') + + def test_both_before_after(self): + text = self.text + previous = self.auto_expand.getprevword + expand = self.auto_expand.expand_word_event + equal = self.assertEqual + + text.insert('insert', 'ab xy yz\n') + text.insert('insert', 'a ac by ac') + + text.mark_set('insert', '2.1') + equal(self.auto_expand.getwords(), ['ab', 'ac', 'a']) + expand('event') + equal(previous(), 'ab') + expand('event') + equal(previous(), 'ac') + expand('event') + equal(previous(), 'a') + + def test_other_expand_cases(self): + text = self.text + expand = self.auto_expand.expand_word_event + equal = self.assertEqual + + # no expansion candidate found + equal(self.auto_expand.getwords(), []) + equal(expand('event'), 'break') + + text.insert('insert', 'bx cy dz a') + equal(self.auto_expand.getwords(), []) + + # reset state by successfully expanding once + # move cursor to another position and expand again + text.insert('insert', 'ac xy a ac ad a') + text.mark_set('insert', '1.7') + expand('event') + initial_state = self.auto_expand.state + text.mark_set('insert', '1.end') + expand('event') + new_state = self.auto_expand.state + self.assertNotEqual(initial_state, new_state) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_calltips.py b/Lib/idlelib/idle_test/test_calltips.py index f3637646c4..b2a733cc0d 100644 --- a/Lib/idlelib/idle_test/test_calltips.py +++ b/Lib/idlelib/idle_test/test_calltips.py @@ -52,11 +52,12 @@ class Get_signatureTest(unittest.TestCase): def gtest(obj, out): self.assertEqual(signature(obj), out) - gtest(List, List.__doc__) + if List.__doc__ is not None: + gtest(List, List.__doc__) gtest(list.__new__, - 'T.__new__(S, ...) -> a new object with type S, a subtype of T') + 'Create and return a new object. See help(type) for accurate signature.') gtest(list.__init__, - 'x.__init__(...) initializes x; see help(type(x)) for signature') + 'Initialize self. See help(type(self)) for accurate signature.') append_doc = "L.append(object) -> None -- append object to end" gtest(list.append, append_doc) gtest([].append, append_doc) @@ -66,10 +67,12 @@ class Get_signatureTest(unittest.TestCase): gtest(SB(), default_tip) def test_signature_wrap(self): - self.assertEqual(signature(textwrap.TextWrapper), '''\ + if textwrap.TextWrapper.__doc__ is not None: + self.assertEqual(signature(textwrap.TextWrapper), '''\ (width=70, initial_indent='', subsequent_indent='', expand_tabs=True, replace_whitespace=True, fix_sentence_endings=False, break_long_words=True, - drop_whitespace=True, break_on_hyphens=True, tabsize=8)''') + drop_whitespace=True, break_on_hyphens=True, tabsize=8, *, max_lines=None, + placeholder=' [...]')''') def test_docline_truncation(self): def f(): pass @@ -107,20 +110,23 @@ bytes() -> empty bytes object''') def t5(a, b=None, *args, **kw): 'doc' t5.tip = "(a, b=None, *args, **kw)" + doc = '\ndoc' if t1.__doc__ is not None else '' for func in (t1, t2, t3, t4, t5, TC): - self.assertEqual(signature(func), func.tip + '\ndoc') + self.assertEqual(signature(func), func.tip + doc) def test_methods(self): + doc = '\ndoc' if TC.__doc__ is not None else '' for meth in (TC.t1, TC.t2, TC.t3, TC.t4, TC.t5, TC.t6, TC.__call__): - self.assertEqual(signature(meth), meth.tip + "\ndoc") - self.assertEqual(signature(TC.cm), "(a)\ndoc") - self.assertEqual(signature(TC.sm), "(b)\ndoc") + self.assertEqual(signature(meth), meth.tip + doc) + self.assertEqual(signature(TC.cm), "(a)" + doc) + self.assertEqual(signature(TC.sm), "(b)" + doc) def test_bound_methods(self): # test that first parameter is correctly removed from argspec + doc = '\ndoc' if TC.__doc__ is not None else '' for meth, mtip in ((tc.t1, "()"), (tc.t4, "(*args)"), (tc.t6, "(self)"), (tc.__call__, '(ci)'), (tc, '(ci)'), (TC.cm, "(a)"),): - self.assertEqual(signature(meth), mtip + "\ndoc") + self.assertEqual(signature(meth), mtip + doc) def test_starred_parameter(self): # test that starred first parameter is *not* removed from argspec diff --git a/Lib/idlelib/idle_test/test_configdialog.py b/Lib/idlelib/idle_test/test_configdialog.py new file mode 100644 index 0000000000..68831236b7 --- /dev/null +++ b/Lib/idlelib/idle_test/test_configdialog.py @@ -0,0 +1,32 @@ +'''Unittests for idlelib/configHandler.py + +Coverage: 46% just by creating dialog. The other half is change code. + +''' +import unittest +from test.support import requires +from tkinter import Tk +from idlelib.configDialog import ConfigDialog +from idlelib.macosxSupport import _initializeTkVariantTests + + +class ConfigDialogTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + _initializeTkVariantTests(cls.root) + + @classmethod + def tearDownClass(cls): + cls.root.destroy() + del cls.root + + def test_dialog(self): + d=ConfigDialog(self.root, 'Test', _utest=True) + d.destroy() + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py new file mode 100644 index 0000000000..a31d26d25d --- /dev/null +++ b/Lib/idlelib/idle_test/test_editor.py @@ -0,0 +1,16 @@ +import unittest +from tkinter import Tk, Text +from idlelib.EditorWindow import EditorWindow +from test.support import requires + +class Editor_func_test(unittest.TestCase): + def test_filename_to_unicode(self): + func = EditorWindow._filename_to_unicode + class dummy(): filesystemencoding = 'utf-8' + pairs = (('abc', 'abc'), ('a\U00011111c', 'a\ufffdc'), + (b'abc', 'abc'), (b'a\xf0\x91\x84\x91c', 'a\ufffdc')) + for inp, out in pairs: + self.assertEqual(func(dummy, inp), out) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_formatparagraph.py b/Lib/idlelib/idle_test/test_formatparagraph.py index f4a7c2d680..f6039e6ab4 100644 --- a/Lib/idlelib/idle_test/test_formatparagraph.py +++ b/Lib/idlelib/idle_test/test_formatparagraph.py @@ -2,7 +2,7 @@ import unittest from idlelib import FormatParagraph as fp from idlelib.EditorWindow import EditorWindow -from tkinter import Tk, Text, TclError +from tkinter import Tk, Text from test.support import requires @@ -293,7 +293,7 @@ class FormatEventTest(unittest.TestCase): # Set cursor ('insert' mark) to '1.0', within text. text.insert('1.0', self.test_string) text.mark_set('insert', '1.0') - self.formatter('ParameterDoesNothing') + self.formatter('ParameterDoesNothing', limit=70) result = text.get('1.0', 'insert') # find function includes \n expected = ( @@ -305,7 +305,7 @@ class FormatEventTest(unittest.TestCase): # Select from 1.11 to line end. text.insert('1.0', self.test_string) text.tag_add('sel', '1.11', '1.end') - self.formatter('ParameterDoesNothing') + self.formatter('ParameterDoesNothing', limit=70) result = text.get('1.0', 'insert') # selection excludes \n expected = ( @@ -319,7 +319,7 @@ class FormatEventTest(unittest.TestCase): # Select 2 long lines. text.insert('1.0', self.multiline_test_string) text.tag_add('sel', '2.0', '4.0') - self.formatter('ParameterDoesNothing') + self.formatter('ParameterDoesNothing', limit=70) result = text.get('2.0', 'insert') expected = ( " The second line's length is way over the max width. It goes on and\n" @@ -334,7 +334,7 @@ class FormatEventTest(unittest.TestCase): # Set cursor ('insert') to '1.0', within block. text.insert('1.0', self.multiline_test_comment) - self.formatter('ParameterDoesNothing') + self.formatter('ParameterDoesNothing', limit=70) result = text.get('1.0', 'insert') expected = ( "# The first line is under the max width. The second line's length is\n" @@ -348,7 +348,7 @@ class FormatEventTest(unittest.TestCase): # Select line 2, verify line 1 unaffected. text.insert('1.0', self.multiline_test_comment) text.tag_add('sel', '2.0', '3.0') - self.formatter('ParameterDoesNothing') + self.formatter('ParameterDoesNothing', limit=70) result = text.get('1.0', 'insert') expected = ( "# The first line is under the max width.\n" diff --git a/Lib/idlelib/idle_test/test_hyperparser.py b/Lib/idlelib/idle_test/test_hyperparser.py new file mode 100644 index 0000000000..edfc783fe8 --- /dev/null +++ b/Lib/idlelib/idle_test/test_hyperparser.py @@ -0,0 +1,273 @@ +"""Unittest for idlelib.HyperParser""" +import unittest +from test.support import requires +from tkinter import Tk, Text +from idlelib.EditorWindow import EditorWindow +from idlelib.HyperParser import HyperParser + +class DummyEditwin: + def __init__(self, text): + self.text = text + self.indentwidth = 8 + self.tabwidth = 8 + self.context_use_ps1 = True + self.num_context_lines = 50, 500, 1000 + + _build_char_in_string_func = EditorWindow._build_char_in_string_func + is_char_in_string = EditorWindow.is_char_in_string + + +class HyperParserTest(unittest.TestCase): + code = ( + '"""This is a module docstring"""\n' + '# this line is a comment\n' + 'x = "this is a string"\n' + "y = 'this is also a string'\n" + 'l = [i for i in range(10)]\n' + 'm = [py*py for # comment\n' + ' py in l]\n' + 'x.__len__\n' + "z = ((r'asdf')+('a')))\n" + '[x for x in\n' + 'for = False\n' + 'cliché = "this is a string with unicode, what a cliché"' + ) + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + cls.text = Text(cls.root) + cls.editwin = DummyEditwin(cls.text) + + @classmethod + def tearDownClass(cls): + del cls.text, cls.editwin + cls.root.destroy() + del cls.root + + def setUp(self): + self.text.insert('insert', self.code) + + def tearDown(self): + self.text.delete('1.0', 'end') + self.editwin.context_use_ps1 = True + + def get_parser(self, index): + """ + Return a parser object with index at 'index' + """ + return HyperParser(self.editwin, index) + + def test_init(self): + """ + test corner cases in the init method + """ + with self.assertRaises(ValueError) as ve: + self.text.tag_add('console', '1.0', '1.end') + p = self.get_parser('1.5') + self.assertIn('precedes', str(ve.exception)) + + # test without ps1 + self.editwin.context_use_ps1 = False + + # number of lines lesser than 50 + p = self.get_parser('end') + self.assertEqual(p.rawtext, self.text.get('1.0', 'end')) + + # number of lines greater than 50 + self.text.insert('end', self.text.get('1.0', 'end')*4) + p = self.get_parser('54.5') + + def test_is_in_string(self): + get = self.get_parser + + p = get('1.0') + self.assertFalse(p.is_in_string()) + p = get('1.4') + self.assertTrue(p.is_in_string()) + p = get('2.3') + self.assertFalse(p.is_in_string()) + p = get('3.3') + self.assertFalse(p.is_in_string()) + p = get('3.7') + self.assertTrue(p.is_in_string()) + p = get('4.6') + self.assertTrue(p.is_in_string()) + p = get('12.54') + self.assertTrue(p.is_in_string()) + + def test_is_in_code(self): + get = self.get_parser + + p = get('1.0') + self.assertTrue(p.is_in_code()) + p = get('1.1') + self.assertFalse(p.is_in_code()) + p = get('2.5') + self.assertFalse(p.is_in_code()) + p = get('3.4') + self.assertTrue(p.is_in_code()) + p = get('3.6') + self.assertFalse(p.is_in_code()) + p = get('4.14') + self.assertFalse(p.is_in_code()) + + def test_get_surrounding_bracket(self): + get = self.get_parser + + def without_mustclose(parser): + # a utility function to get surrounding bracket + # with mustclose=False + return parser.get_surrounding_brackets(mustclose=False) + + def with_mustclose(parser): + # a utility function to get surrounding bracket + # with mustclose=True + return parser.get_surrounding_brackets(mustclose=True) + + p = get('3.2') + self.assertIsNone(with_mustclose(p)) + self.assertIsNone(without_mustclose(p)) + + p = get('5.6') + self.assertTupleEqual(without_mustclose(p), ('5.4', '5.25')) + self.assertTupleEqual(without_mustclose(p), with_mustclose(p)) + + p = get('5.23') + self.assertTupleEqual(without_mustclose(p), ('5.21', '5.24')) + self.assertTupleEqual(without_mustclose(p), with_mustclose(p)) + + p = get('6.15') + self.assertTupleEqual(without_mustclose(p), ('6.4', '6.end')) + self.assertIsNone(with_mustclose(p)) + + p = get('9.end') + self.assertIsNone(with_mustclose(p)) + self.assertIsNone(without_mustclose(p)) + + def test_get_expression(self): + get = self.get_parser + + p = get('4.2') + self.assertEqual(p.get_expression(), 'y ') + + p = get('4.7') + with self.assertRaises(ValueError) as ve: + p.get_expression() + self.assertIn('is inside a code', str(ve.exception)) + + p = get('5.25') + self.assertEqual(p.get_expression(), 'range(10)') + + p = get('6.7') + self.assertEqual(p.get_expression(), 'py') + + p = get('6.8') + self.assertEqual(p.get_expression(), '') + + p = get('7.9') + self.assertEqual(p.get_expression(), 'py') + + p = get('8.end') + self.assertEqual(p.get_expression(), 'x.__len__') + + p = get('9.13') + self.assertEqual(p.get_expression(), "r'asdf'") + + p = get('9.17') + with self.assertRaises(ValueError) as ve: + p.get_expression() + self.assertIn('is inside a code', str(ve.exception)) + + p = get('10.0') + self.assertEqual(p.get_expression(), '') + + p = get('10.6') + self.assertEqual(p.get_expression(), '') + + p = get('10.11') + self.assertEqual(p.get_expression(), '') + + p = get('11.3') + self.assertEqual(p.get_expression(), '') + + p = get('11.11') + self.assertEqual(p.get_expression(), 'False') + + p = get('12.6') + self.assertEqual(p.get_expression(), 'cliché') + + def test_eat_identifier(self): + def is_valid_id(candidate): + result = HyperParser._eat_identifier(candidate, 0, len(candidate)) + if result == len(candidate): + return True + elif result == 0: + return False + else: + err_msg = "Unexpected result: {} (expected 0 or {}".format( + result, len(candidate) + ) + raise Exception(err_msg) + + # invalid first character which is valid elsewhere in an identifier + self.assertFalse(is_valid_id('2notid')) + + # ASCII-only valid identifiers + self.assertTrue(is_valid_id('valid_id')) + self.assertTrue(is_valid_id('_valid_id')) + self.assertTrue(is_valid_id('valid_id_')) + self.assertTrue(is_valid_id('_2valid_id')) + + # keywords which should be "eaten" + self.assertTrue(is_valid_id('True')) + self.assertTrue(is_valid_id('False')) + self.assertTrue(is_valid_id('None')) + + # keywords which should not be "eaten" + self.assertFalse(is_valid_id('for')) + self.assertFalse(is_valid_id('import')) + self.assertFalse(is_valid_id('return')) + + # valid unicode identifiers + self.assertTrue(is_valid_id('cliche')) + self.assertTrue(is_valid_id('cliché')) + self.assertTrue(is_valid_id('a٢')) + + # invalid unicode identifiers + self.assertFalse(is_valid_id('2a')) + self.assertFalse(is_valid_id('٢a')) + self.assertFalse(is_valid_id('a²')) + + # valid identifier after "punctuation" + self.assertEqual(HyperParser._eat_identifier('+ var', 0, 5), len('var')) + self.assertEqual(HyperParser._eat_identifier('+var', 0, 4), len('var')) + self.assertEqual(HyperParser._eat_identifier('.var', 0, 4), len('var')) + + # invalid identifiers + self.assertFalse(is_valid_id('+')) + self.assertFalse(is_valid_id(' ')) + self.assertFalse(is_valid_id(':')) + self.assertFalse(is_valid_id('?')) + self.assertFalse(is_valid_id('^')) + self.assertFalse(is_valid_id('\\')) + self.assertFalse(is_valid_id('"')) + self.assertFalse(is_valid_id('"a string"')) + + def test_eat_identifier_various_lengths(self): + eat_id = HyperParser._eat_identifier + + for length in range(1, 21): + self.assertEqual(eat_id('a' * length, 0, length), length) + self.assertEqual(eat_id('é' * length, 0, length), length) + self.assertEqual(eat_id('a' + '2' * (length - 1), 0, length), length) + self.assertEqual(eat_id('é' + '2' * (length - 1), 0, length), length) + self.assertEqual(eat_id('é' + 'a' * (length - 1), 0, length), length) + self.assertEqual(eat_id('é' * (length - 1) + 'a', 0, length), length) + self.assertEqual(eat_id('+' * length, 0, length), 0) + self.assertEqual(eat_id('2' + 'a' * (length - 1), 0, length), 0) + self.assertEqual(eat_id('2' + 'é' * (length - 1), 0, length), 0) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_io.py b/Lib/idlelib/idle_test/test_io.py new file mode 100644 index 0000000000..e0e3b985e2 --- /dev/null +++ b/Lib/idlelib/idle_test/test_io.py @@ -0,0 +1,233 @@ +import unittest +import io +from idlelib.PyShell import PseudoInputFile, PseudoOutputFile + + +class S(str): + def __str__(self): + return '%s:str' % type(self).__name__ + def __unicode__(self): + return '%s:unicode' % type(self).__name__ + def __len__(self): + return 3 + def __iter__(self): + return iter('abc') + def __getitem__(self, *args): + return '%s:item' % type(self).__name__ + def __getslice__(self, *args): + return '%s:slice' % type(self).__name__ + +class MockShell: + def __init__(self): + self.reset() + + def write(self, *args): + self.written.append(args) + + def readline(self): + return self.lines.pop() + + def close(self): + pass + + def reset(self): + self.written = [] + + def push(self, lines): + self.lines = list(lines)[::-1] + + +class PseudeOutputFilesTest(unittest.TestCase): + def test_misc(self): + shell = MockShell() + f = PseudoOutputFile(shell, 'stdout', 'utf-8') + self.assertIsInstance(f, io.TextIOBase) + self.assertEqual(f.encoding, 'utf-8') + self.assertIsNone(f.errors) + self.assertIsNone(f.newlines) + self.assertEqual(f.name, '<stdout>') + self.assertFalse(f.closed) + self.assertTrue(f.isatty()) + self.assertFalse(f.readable()) + self.assertTrue(f.writable()) + self.assertFalse(f.seekable()) + + def test_unsupported(self): + shell = MockShell() + f = PseudoOutputFile(shell, 'stdout', 'utf-8') + self.assertRaises(OSError, f.fileno) + self.assertRaises(OSError, f.tell) + self.assertRaises(OSError, f.seek, 0) + self.assertRaises(OSError, f.read, 0) + self.assertRaises(OSError, f.readline, 0) + + def test_write(self): + shell = MockShell() + f = PseudoOutputFile(shell, 'stdout', 'utf-8') + f.write('test') + self.assertEqual(shell.written, [('test', 'stdout')]) + shell.reset() + f.write('t\xe8st') + self.assertEqual(shell.written, [('t\xe8st', 'stdout')]) + shell.reset() + + f.write(S('t\xe8st')) + self.assertEqual(shell.written, [('t\xe8st', 'stdout')]) + self.assertEqual(type(shell.written[0][0]), str) + shell.reset() + + self.assertRaises(TypeError, f.write) + self.assertEqual(shell.written, []) + self.assertRaises(TypeError, f.write, b'test') + self.assertRaises(TypeError, f.write, 123) + self.assertEqual(shell.written, []) + self.assertRaises(TypeError, f.write, 'test', 'spam') + self.assertEqual(shell.written, []) + + def test_writelines(self): + shell = MockShell() + f = PseudoOutputFile(shell, 'stdout', 'utf-8') + f.writelines([]) + self.assertEqual(shell.written, []) + shell.reset() + f.writelines(['one\n', 'two']) + self.assertEqual(shell.written, + [('one\n', 'stdout'), ('two', 'stdout')]) + shell.reset() + f.writelines(['on\xe8\n', 'tw\xf2']) + self.assertEqual(shell.written, + [('on\xe8\n', 'stdout'), ('tw\xf2', 'stdout')]) + shell.reset() + + f.writelines([S('t\xe8st')]) + self.assertEqual(shell.written, [('t\xe8st', 'stdout')]) + self.assertEqual(type(shell.written[0][0]), str) + shell.reset() + + self.assertRaises(TypeError, f.writelines) + self.assertEqual(shell.written, []) + self.assertRaises(TypeError, f.writelines, 123) + self.assertEqual(shell.written, []) + self.assertRaises(TypeError, f.writelines, [b'test']) + self.assertRaises(TypeError, f.writelines, [123]) + self.assertEqual(shell.written, []) + self.assertRaises(TypeError, f.writelines, [], []) + self.assertEqual(shell.written, []) + + def test_close(self): + shell = MockShell() + f = PseudoOutputFile(shell, 'stdout', 'utf-8') + self.assertFalse(f.closed) + f.write('test') + f.close() + self.assertTrue(f.closed) + self.assertRaises(ValueError, f.write, 'x') + self.assertEqual(shell.written, [('test', 'stdout')]) + f.close() + self.assertRaises(TypeError, f.close, 1) + + +class PseudeInputFilesTest(unittest.TestCase): + def test_misc(self): + shell = MockShell() + f = PseudoInputFile(shell, 'stdin', 'utf-8') + self.assertIsInstance(f, io.TextIOBase) + self.assertEqual(f.encoding, 'utf-8') + self.assertIsNone(f.errors) + self.assertIsNone(f.newlines) + self.assertEqual(f.name, '<stdin>') + self.assertFalse(f.closed) + self.assertTrue(f.isatty()) + self.assertTrue(f.readable()) + self.assertFalse(f.writable()) + self.assertFalse(f.seekable()) + + def test_unsupported(self): + shell = MockShell() + f = PseudoInputFile(shell, 'stdin', 'utf-8') + self.assertRaises(OSError, f.fileno) + self.assertRaises(OSError, f.tell) + self.assertRaises(OSError, f.seek, 0) + self.assertRaises(OSError, f.write, 'x') + self.assertRaises(OSError, f.writelines, ['x']) + + def test_read(self): + shell = MockShell() + f = PseudoInputFile(shell, 'stdin', 'utf-8') + shell.push(['one\n', 'two\n', '']) + self.assertEqual(f.read(), 'one\ntwo\n') + shell.push(['one\n', 'two\n', '']) + self.assertEqual(f.read(-1), 'one\ntwo\n') + shell.push(['one\n', 'two\n', '']) + self.assertEqual(f.read(None), 'one\ntwo\n') + shell.push(['one\n', 'two\n', 'three\n', '']) + self.assertEqual(f.read(2), 'on') + self.assertEqual(f.read(3), 'e\nt') + self.assertEqual(f.read(10), 'wo\nthree\n') + + shell.push(['one\n', 'two\n']) + self.assertEqual(f.read(0), '') + self.assertRaises(TypeError, f.read, 1.5) + self.assertRaises(TypeError, f.read, '1') + self.assertRaises(TypeError, f.read, 1, 1) + + def test_readline(self): + shell = MockShell() + f = PseudoInputFile(shell, 'stdin', 'utf-8') + shell.push(['one\n', 'two\n', 'three\n', 'four\n']) + self.assertEqual(f.readline(), 'one\n') + self.assertEqual(f.readline(-1), 'two\n') + self.assertEqual(f.readline(None), 'three\n') + shell.push(['one\ntwo\n']) + self.assertEqual(f.readline(), 'one\n') + self.assertEqual(f.readline(), 'two\n') + shell.push(['one', 'two', 'three']) + self.assertEqual(f.readline(), 'one') + self.assertEqual(f.readline(), 'two') + shell.push(['one\n', 'two\n', 'three\n']) + self.assertEqual(f.readline(2), 'on') + self.assertEqual(f.readline(1), 'e') + self.assertEqual(f.readline(1), '\n') + self.assertEqual(f.readline(10), 'two\n') + + shell.push(['one\n', 'two\n']) + self.assertEqual(f.readline(0), '') + self.assertRaises(TypeError, f.readlines, 1.5) + self.assertRaises(TypeError, f.readlines, '1') + self.assertRaises(TypeError, f.readlines, 1, 1) + + def test_readlines(self): + shell = MockShell() + f = PseudoInputFile(shell, 'stdin', 'utf-8') + shell.push(['one\n', 'two\n', '']) + self.assertEqual(f.readlines(), ['one\n', 'two\n']) + shell.push(['one\n', 'two\n', '']) + self.assertEqual(f.readlines(-1), ['one\n', 'two\n']) + shell.push(['one\n', 'two\n', '']) + self.assertEqual(f.readlines(None), ['one\n', 'two\n']) + shell.push(['one\n', 'two\n', '']) + self.assertEqual(f.readlines(0), ['one\n', 'two\n']) + shell.push(['one\n', 'two\n', '']) + self.assertEqual(f.readlines(3), ['one\n']) + shell.push(['one\n', 'two\n', '']) + self.assertEqual(f.readlines(4), ['one\n', 'two\n']) + + shell.push(['one\n', 'two\n', '']) + self.assertRaises(TypeError, f.readlines, 1.5) + self.assertRaises(TypeError, f.readlines, '1') + self.assertRaises(TypeError, f.readlines, 1, 1) + + def test_close(self): + shell = MockShell() + f = PseudoInputFile(shell, 'stdin', 'utf-8') + shell.push(['one\n', 'two\n', '']) + self.assertFalse(f.closed) + self.assertEqual(f.readline(), 'one\n') + f.close() + self.assertFalse(f.closed) + self.assertEqual(f.readline(), 'two\n') + self.assertRaises(TypeError, f.close, 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/idlelib/idle_test/test_parenmatch.py b/Lib/idlelib/idle_test/test_parenmatch.py new file mode 100644 index 0000000000..9aba4bec93 --- /dev/null +++ b/Lib/idlelib/idle_test/test_parenmatch.py @@ -0,0 +1,109 @@ +"""Test idlelib.ParenMatch.""" +# This must currently be a gui test because ParenMatch methods use +# several text methods not defined on idlelib.idle_test.mock_tk.Text. + +import unittest +from unittest.mock import Mock +from test.support import requires +from tkinter import Tk, Text +from idlelib.ParenMatch import ParenMatch + +class DummyEditwin: + def __init__(self, text): + self.text = text + self.indentwidth = 8 + self.tabwidth = 8 + self.context_use_ps1 = True + + +class ParenMatchTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + cls.text = Text(cls.root) + cls.editwin = DummyEditwin(cls.text) + cls.editwin.text_frame = Mock() + + @classmethod + def tearDownClass(cls): + del cls.text, cls.editwin + cls.root.destroy() + del cls.root + + def tearDown(self): + self.text.delete('1.0', 'end') + + def test_paren_expression(self): + """ + Test ParenMatch with 'expression' style. + """ + text = self.text + pm = ParenMatch(self.editwin) + pm.set_style('expression') + + text.insert('insert', 'def foobar(a, b') + pm.flash_paren_event('event') + self.assertIn('<<parenmatch-check-restore>>', text.event_info()) + self.assertTupleEqual(text.tag_prevrange('paren', 'end'), + ('1.10', '1.15')) + text.insert('insert', ')') + pm.restore_event() + self.assertNotIn('<<parenmatch-check-restore>>', text.event_info()) + self.assertEqual(text.tag_prevrange('paren', 'end'), ()) + + # paren_closed_event can only be tested as below + pm.paren_closed_event('event') + self.assertTupleEqual(text.tag_prevrange('paren', 'end'), + ('1.10', '1.16')) + + def test_paren_default(self): + """ + Test ParenMatch with 'default' style. + """ + text = self.text + pm = ParenMatch(self.editwin) + pm.set_style('default') + + text.insert('insert', 'def foobar(a, b') + pm.flash_paren_event('event') + self.assertIn('<<parenmatch-check-restore>>', text.event_info()) + self.assertTupleEqual(text.tag_prevrange('paren', 'end'), + ('1.10', '1.11')) + text.insert('insert', ')') + pm.restore_event() + self.assertNotIn('<<parenmatch-check-restore>>', text.event_info()) + self.assertEqual(text.tag_prevrange('paren', 'end'), ()) + + def test_paren_corner(self): + """ + Test corner cases in flash_paren_event and paren_closed_event. + + These cases force conditional expression and alternate paths. + """ + text = self.text + pm = ParenMatch(self.editwin) + + text.insert('insert', '# this is a commen)') + self.assertIsNone(pm.paren_closed_event('event')) + + text.insert('insert', '\ndef') + self.assertIsNone(pm.flash_paren_event('event')) + self.assertIsNone(pm.paren_closed_event('event')) + + text.insert('insert', ' a, *arg)') + self.assertIsNone(pm.paren_closed_event('event')) + + def test_handle_restore_timer(self): + pm = ParenMatch(self.editwin) + pm.restore_event = Mock() + pm.handle_restore_timer(0) + self.assertTrue(pm.restore_event.called) + pm.restore_event.reset_mock() + pm.handle_restore_timer(1) + self.assertFalse(pm.restore_event.called) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_pathbrowser.py b/Lib/idlelib/idle_test/test_pathbrowser.py index 7ad7c97a79..afb886fa33 100644 --- a/Lib/idlelib/idle_test/test_pathbrowser.py +++ b/Lib/idlelib/idle_test/test_pathbrowser.py @@ -1,5 +1,8 @@ import unittest -import idlelib.PathBrowser as PathBrowser +import os +import sys +import idlelib +from idlelib import PathBrowser class PathBrowserTest(unittest.TestCase): @@ -7,6 +10,18 @@ class PathBrowserTest(unittest.TestCase): # Issue16226 - make sure that getting a sublist works d = PathBrowser.DirBrowserTreeItem('') d.GetSubList() + self.assertEqual('', d.GetText()) + + dir = os.path.split(os.path.abspath(idlelib.__file__))[0] + self.assertEqual(d.ispackagedir(dir), True) + self.assertEqual(d.ispackagedir(dir + '/Icons'), False) + + def test_PathBrowserTreeItem(self): + p = PathBrowser.PathBrowserTreeItem() + self.assertEqual(p.GetText(), 'sys.path') + sub = p.GetSubList() + self.assertEqual(len(sub), len(sys.path)) + self.assertEqual(type(sub[0]), PathBrowser.DirBrowserTreeItem) if __name__ == '__main__': unittest.main(verbosity=2, exit=False) diff --git a/Lib/idlelib/idle_test/test_searchdialogbase.py b/Lib/idlelib/idle_test/test_searchdialogbase.py new file mode 100644 index 0000000000..8036b918c5 --- /dev/null +++ b/Lib/idlelib/idle_test/test_searchdialogbase.py @@ -0,0 +1,165 @@ +'''Unittests for idlelib/SearchDialogBase.py + +Coverage: 99%. The only thing not covered is inconsequential -- +testing skipping of suite when self.needwrapbutton is false. + +''' +import unittest +from test.support import requires +from tkinter import Tk, Toplevel, Frame ##, BooleanVar, StringVar +from idlelib import SearchEngine as se +from idlelib import SearchDialogBase as sdb +from idlelib.idle_test.mock_idle import Func +## from idlelib.idle_test.mock_tk import Var + +# The ## imports above & following could help make some tests gui-free. +# However, they currently make radiobutton tests fail. +##def setUpModule(): +## # Replace tk objects used to initialize se.SearchEngine. +## se.BooleanVar = Var +## se.StringVar = Var +## +##def tearDownModule(): +## se.BooleanVar = BooleanVar +## se.StringVar = StringVar + +class SearchDialogBaseTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + + @classmethod + def tearDownClass(cls): + cls.root.destroy() + del cls.root + + def setUp(self): + self.engine = se.SearchEngine(self.root) # None also seems to work + self.dialog = sdb.SearchDialogBase(root=self.root, engine=self.engine) + + def tearDown(self): + self.dialog.close() + + def test_open_and_close(self): + # open calls create_widgets, which needs default_command + self.dialog.default_command = None + + # Since text parameter of .open is not used in base class, + # pass dummy 'text' instead of tk.Text(). + self.dialog.open('text') + self.assertEqual(self.dialog.top.state(), 'normal') + self.dialog.close() + self.assertEqual(self.dialog.top.state(), 'withdrawn') + + self.dialog.open('text', searchphrase="hello") + self.assertEqual(self.dialog.ent.get(), 'hello') + self.dialog.close() + + def test_create_widgets(self): + self.dialog.create_entries = Func() + self.dialog.create_option_buttons = Func() + self.dialog.create_other_buttons = Func() + self.dialog.create_command_buttons = Func() + + self.dialog.default_command = None + self.dialog.create_widgets() + + self.assertTrue(self.dialog.create_entries.called) + self.assertTrue(self.dialog.create_option_buttons.called) + self.assertTrue(self.dialog.create_other_buttons.called) + self.assertTrue(self.dialog.create_command_buttons.called) + + def test_make_entry(self): + equal = self.assertEqual + self.dialog.row = 0 + self.dialog.top = Toplevel(self.root) + entry, label = self.dialog.make_entry("Test:", 'hello') + equal(label['text'], 'Test:') + + self.assertIn(entry.get(), 'hello') + egi = entry.grid_info() + equal(int(egi['row']), 0) + equal(int(egi['column']), 1) + equal(int(egi['rowspan']), 1) + equal(int(egi['columnspan']), 1) + equal(self.dialog.row, 1) + + def test_create_entries(self): + self.dialog.row = 0 + self.engine.setpat('hello') + self.dialog.create_entries() + self.assertIn(self.dialog.ent.get(), 'hello') + + def test_make_frame(self): + self.dialog.row = 0 + self.dialog.top = Toplevel(self.root) + frame, label = self.dialog.make_frame() + self.assertEqual(label, '') + self.assertIsInstance(frame, Frame) + + frame, label = self.dialog.make_frame('testlabel') + self.assertEqual(label['text'], 'testlabel') + self.assertIsInstance(frame, Frame) + + def btn_test_setup(self, meth): + self.dialog.top = Toplevel(self.root) + self.dialog.row = 0 + return meth() + + def test_create_option_buttons(self): + e = self.engine + for state in (0, 1): + for var in (e.revar, e.casevar, e.wordvar, e.wrapvar): + var.set(state) + frame, options = self.btn_test_setup( + self.dialog.create_option_buttons) + for spec, button in zip (options, frame.pack_slaves()): + var, label = spec + self.assertEqual(button['text'], label) + self.assertEqual(var.get(), state) + if state == 1: + button.deselect() + else: + button.select() + self.assertEqual(var.get(), 1 - state) + + def test_create_other_buttons(self): + for state in (False, True): + var = self.engine.backvar + var.set(state) + frame, others = self.btn_test_setup( + self.dialog.create_other_buttons) + buttons = frame.pack_slaves() + for spec, button in zip(others, buttons): + val, label = spec + self.assertEqual(button['text'], label) + if val == state: + # hit other button, then this one + # indexes depend on button order + self.assertEqual(var.get(), state) + buttons[val].select() + self.assertEqual(var.get(), 1 - state) + buttons[1-val].select() + self.assertEqual(var.get(), state) + + def test_make_button(self): + self.dialog.top = Toplevel(self.root) + self.dialog.buttonframe = Frame(self.dialog.top) + btn = self.dialog.make_button('Test', self.dialog.close) + self.assertEqual(btn['text'], 'Test') + + def test_create_command_buttons(self): + self.dialog.create_command_buttons() + # Look for close button command in buttonframe + closebuttoncommand = '' + for child in self.dialog.buttonframe.winfo_children(): + if child['text'] == 'close': + closebuttoncommand = child['command'] + self.assertIn('close', closebuttoncommand) + + + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=2) diff --git a/Lib/idlelib/idle_test/test_searchengine.py b/Lib/idlelib/idle_test/test_searchengine.py index 129a5a35a6..c7792fb188 100644 --- a/Lib/idlelib/idle_test/test_searchengine.py +++ b/Lib/idlelib/idle_test/test_searchengine.py @@ -7,7 +7,7 @@ import re import unittest -from test.support import requires +# from test.support import requires from tkinter import BooleanVar, StringVar, TclError # ,Tk, Text import tkinter.messagebox as tkMessageBox from idlelib import SearchEngine as se diff --git a/Lib/idlelib/idle_test/test_text.py b/Lib/idlelib/idle_test/test_text.py index 5ac2fd74e3..7e823df3db 100644 --- a/Lib/idlelib/idle_test/test_text.py +++ b/Lib/idlelib/idle_test/test_text.py @@ -3,7 +3,6 @@ import unittest from test.support import requires from _tkinter import TclError -import tkinter as tk class TextTest(object): diff --git a/Lib/idlelib/idle_test/test_textview.py b/Lib/idlelib/idle_test/test_textview.py new file mode 100644 index 0000000000..68e5b82ad9 --- /dev/null +++ b/Lib/idlelib/idle_test/test_textview.py @@ -0,0 +1,97 @@ +'''Test the functions and main class method of textView.py. + +Since all methods and functions create (or destroy) a TextViewer, which +is a widget containing multiple widgets, all tests must be gui tests. +Using mock Text would not change this. Other mocks are used to retrieve +information about calls. + +The coverage is essentially 100%. +''' +from test.support import requires +requires('gui') + +import unittest +import os +from tkinter import Tk +from idlelib import textView as tv +from idlelib.idle_test.mock_idle import Func +from idlelib.idle_test.mock_tk import Mbox + +def setUpModule(): + global root + root = Tk() + +def tearDownModule(): + global root + root.destroy() # pyflakes falsely sees root as undefined + del root + + +class TV(tv.TextViewer): # used by TextViewTest + transient = Func() + grab_set = Func() + wait_window = Func() + +class TextViewTest(unittest.TestCase): + + def setUp(self): + TV.transient.__init__() + TV.grab_set.__init__() + TV.wait_window.__init__() + + def test_init_modal(self): + view = TV(root, 'Title', 'test text') + self.assertTrue(TV.transient.called) + self.assertTrue(TV.grab_set.called) + self.assertTrue(TV.wait_window.called) + view.Ok() + + def test_init_nonmodal(self): + view = TV(root, 'Title', 'test text', modal=False) + self.assertFalse(TV.transient.called) + self.assertFalse(TV.grab_set.called) + self.assertFalse(TV.wait_window.called) + view.Ok() + + def test_ok(self): + view = TV(root, 'Title', 'test text', modal=False) + view.destroy = Func() + view.Ok() + self.assertTrue(view.destroy.called) + del view.destroy # unmask real function + view.destroy + + +class textviewTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.orig_mbox = tv.tkMessageBox + tv.tkMessageBox = Mbox + + @classmethod + def tearDownClass(cls): + tv.tkMessageBox = cls.orig_mbox + del cls.orig_mbox + + def test_view_text(self): + # If modal True, tkinter will error with 'can't invoke "event" command' + view = tv.view_text(root, 'Title', 'test text', modal=False) + self.assertIsInstance(view, tv.TextViewer) + + def test_view_file(self): + test_dir = os.path.dirname(__file__) + testfile = os.path.join(test_dir, 'test_textview.py') + view = tv.view_file(root, 'Title', testfile, modal=False) + self.assertIsInstance(view, tv.TextViewer) + self.assertIn('Test', view.textView.get('1.0', '1.end')) + view.Ok() + + # Mock messagebox will be used and view_file will not return anything + testfile = os.path.join(test_dir, '../notthere.py') + view = tv.view_file(root, 'Title', testfile, modal=False) + self.assertIsNone(view) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_warning.py b/Lib/idlelib/idle_test/test_warning.py index 18627ddd23..54ac993e88 100644 --- a/Lib/idlelib/idle_test/test_warning.py +++ b/Lib/idlelib/idle_test/test_warning.py @@ -68,6 +68,15 @@ class ShellWarnTest(unittest.TestCase): 'Test', UserWarning, 'test_warning.py', 99, f, 'Line of code') self.assertEqual(shellmsg.splitlines(), f.getvalue().splitlines()) +class ImportWarnTest(unittest.TestCase): + def test_idlever(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + import idlelib.idlever + self.assertEqual(len(w), 1) + self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) + self.assertIn("version", str(w[-1].message)) + if __name__ == '__main__': unittest.main(verbosity=2, exit=False) diff --git a/Lib/idlelib/idle_test/test_widgetredir.py b/Lib/idlelib/idle_test/test_widgetredir.py new file mode 100644 index 0000000000..64405615a0 --- /dev/null +++ b/Lib/idlelib/idle_test/test_widgetredir.py @@ -0,0 +1,122 @@ +"""Unittest for idlelib.WidgetRedirector + +100% coverage +""" +from test.support import requires +import unittest +from idlelib.idle_test.mock_idle import Func +from tkinter import Tk, Text, TclError +from idlelib.WidgetRedirector import WidgetRedirector + + +class InitCloseTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.tk = Tk() + cls.text = Text(cls.tk) + + @classmethod + def tearDownClass(cls): + cls.text.destroy() + cls.tk.destroy() + del cls.text, cls.tk + + def test_init(self): + redir = WidgetRedirector(self.text) + self.assertEqual(redir.widget, self.text) + self.assertEqual(redir.tk, self.text.tk) + self.assertRaises(TclError, WidgetRedirector, self.text) + redir.close() # restore self.tk, self.text + + def test_close(self): + redir = WidgetRedirector(self.text) + redir.register('insert', Func) + redir.close() + self.assertEqual(redir._operations, {}) + self.assertFalse(hasattr(self.text, 'widget')) + + +class WidgetRedirectorTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.tk = Tk() + cls.text = Text(cls.tk) + + @classmethod + def tearDownClass(cls): + cls.text.destroy() + cls.tk.destroy() + del cls.text, cls.tk + + def setUp(self): + self.redir = WidgetRedirector(self.text) + self.func = Func() + self.orig_insert = self.redir.register('insert', self.func) + self.text.insert('insert', 'asdf') # leaves self.text empty + + def tearDown(self): + self.text.delete('1.0', 'end') + self.redir.close() + + def test_repr(self): # partly for 100% coverage + self.assertIn('Redirector', repr(self.redir)) + self.assertIn('Original', repr(self.orig_insert)) + + def test_register(self): + self.assertEqual(self.text.get('1.0', 'end'), '\n') + self.assertEqual(self.func.args, ('insert', 'asdf')) + self.assertIn('insert', self.redir._operations) + self.assertIn('insert', self.text.__dict__) + self.assertEqual(self.text.insert, self.func) + + def test_original_command(self): + self.assertEqual(self.orig_insert.operation, 'insert') + self.assertEqual(self.orig_insert.tk_call, self.text.tk.call) + self.orig_insert('insert', 'asdf') + self.assertEqual(self.text.get('1.0', 'end'), 'asdf\n') + + def test_unregister(self): + self.assertIsNone(self.redir.unregister('invalid operation name')) + self.assertEqual(self.redir.unregister('insert'), self.func) + self.assertNotIn('insert', self.redir._operations) + self.assertNotIn('insert', self.text.__dict__) + + def test_unregister_no_attribute(self): + del self.text.insert + self.assertEqual(self.redir.unregister('insert'), self.func) + + def test_dispatch_intercept(self): + self.func.__init__(True) + self.assertTrue(self.redir.dispatch('insert', False)) + self.assertFalse(self.func.args[0]) + + def test_dispatch_bypass(self): + self.orig_insert('insert', 'asdf') + # tk.call returns '' where Python would return None + self.assertEqual(self.redir.dispatch('delete', '1.0', 'end'), '') + self.assertEqual(self.text.get('1.0', 'end'), '\n') + + def test_dispatch_error(self): + self.func.__init__(TclError()) + self.assertEqual(self.redir.dispatch('insert', False), '') + self.assertEqual(self.redir.dispatch('invalid'), '') + + def test_command_dispatch(self): + # Test that .__init__ causes redirection of tk calls + # through redir.dispatch + self.tk.call(self.text._w, 'insert', 'hello') + self.assertEqual(self.func.args, ('hello',)) + self.assertEqual(self.text.get('1.0', 'end'), '\n') + # Ensure that called through redir .dispatch and not through + # self.text.insert by having mock raise TclError. + self.func.__init__(TclError()) + self.assertEqual(self.tk.call(self.text._w, 'insert', 'boo'), '') + + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/Lib/idlelib/idlever.py b/Lib/idlelib/idlever.py index 9ad7d89a6e..3e9f69a3e3 100644 --- a/Lib/idlelib/idlever.py +++ b/Lib/idlelib/idlever.py @@ -1 +1,12 @@ -IDLE_VERSION = "3.3.6" +""" +The separate Idle version was eliminated years ago; +idlelib.idlever is no longer used by Idle +and will be removed in 3.6 or later. Use + from sys import version + IDLE_VERSION = version[:version.index(' ')] +""" +# Kept for now only for possible existing extension use +import warnings as w +w.warn(__doc__, DeprecationWarning, stacklevel=2) +from sys import version +IDLE_VERSION = version[:version.index(' ')] diff --git a/Lib/idlelib/keybindingDialog.py b/Lib/idlelib/keybindingDialog.py index 0f0da8c7e9..e6438bfc39 100644 --- a/Lib/idlelib/keybindingDialog.py +++ b/Lib/idlelib/keybindingDialog.py @@ -4,15 +4,16 @@ Dialog for building Tkinter accelerator key bindings from tkinter import * import tkinter.messagebox as tkMessageBox import string -from idlelib import macosxSupport +import sys class GetKeysDialog(Toplevel): - def __init__(self,parent,title,action,currentKeySequences): + def __init__(self,parent,title,action,currentKeySequences,_htest=False): """ action - string, the name of the virtual event these keys will be mapped to currentKeys - list, a list of all key sequence lists currently mapped to virtual events, for overlap checking + _htest - bool, change box location when running htest """ Toplevel.__init__(self, parent) self.configure(borderwidth=5) @@ -38,11 +39,14 @@ class GetKeysDialog(Toplevel): self.LoadFinalKeyList() self.withdraw() #hide while setting geometry self.update_idletasks() - self.geometry("+%d+%d" % - ((parent.winfo_rootx()+((parent.winfo_width()/2) - -(self.winfo_reqwidth()/2)), - parent.winfo_rooty()+((parent.winfo_height()/2) - -(self.winfo_reqheight()/2)) )) ) #centre dialog over parent + self.geometry( + "+%d+%d" % ( + parent.winfo_rootx() + + (parent.winfo_width()/2 - self.winfo_reqwidth()/2), + parent.winfo_rooty() + + ((parent.winfo_height()/2 - self.winfo_reqheight()/2) + if not _htest else 150) + ) ) #centre dialog over parent (or below htest box) self.deiconify() #geometry set, unhide self.wait_window() @@ -133,8 +137,7 @@ class GetKeysDialog(Toplevel): order is also important: key binding equality depends on it, so config-keys.def must use the same ordering. """ - import sys - if macosxSupport.runningAsOSXApp(): + if sys.platform == "darwin": self.modifiers = ['Shift', 'Control', 'Option', 'Command'] else: self.modifiers = ['Control', 'Alt', 'Shift'] @@ -259,11 +262,5 @@ class GetKeysDialog(Toplevel): return keysOK if __name__ == '__main__': - #test the dialog - root=Tk() - def run(): - keySeq='' - dlg=GetKeysDialog(root,'Get Keys','find-again',[]) - print(dlg.result) - Button(root,text='Dialog',command=run).pack() - root.mainloop() + from idlelib.idle_test.htest import run + run(GetKeysDialog) diff --git a/Lib/idlelib/macosxSupport.py b/Lib/idlelib/macosxSupport.py index 67069fa0f3..b96bae1d55 100644 --- a/Lib/idlelib/macosxSupport.py +++ b/Lib/idlelib/macosxSupport.py @@ -1,48 +1,70 @@ """ -A number of function that enhance IDLE on MacOSX when it used as a normal -GUI application (as opposed to an X11 application). +A number of functions that enhance IDLE on Mac OSX. """ import sys import tkinter from os import path +import warnings +def runningAsOSXApp(): + warnings.warn("runningAsOSXApp() is deprecated, use isAquaTk()", + DeprecationWarning, stacklevel=2) + return isAquaTk() -_appbundle = None +def isCarbonAquaTk(root): + warnings.warn("isCarbonAquaTk(root) is deprecated, use isCarbonTk()", + DeprecationWarning, stacklevel=2) + return isCarbonTk() -def runningAsOSXApp(): +_tk_type = None + +def _initializeTkVariantTests(root): + """ + Initializes OS X Tk variant values for + isAquaTk(), isCarbonTk(), isCocoaTk(), and isXQuartz(). """ - Returns True if Python is running from within an app on OSX. - If so, the various OS X customizations will be triggered later (menu - fixup, et al). (Originally, this test was supposed to condition - behavior on whether IDLE was running under Aqua Tk rather than - under X11 Tk but that does not work since a framework build - could be linked with X11. For several releases, this test actually - differentiates between whether IDLE is running from a framework or - not. As a future enhancement, it should be considered whether there - should be a difference based on framework and any needed X11 adaptions - should be made dependent on a new function that actually tests for X11.) - """ - global _appbundle - if _appbundle is None: - _appbundle = sys.platform == 'darwin' - if _appbundle: - import sysconfig - _appbundle = bool(sysconfig.get_config_var('PYTHONFRAMEWORK')) - return _appbundle - -_carbonaquatk = None + global _tk_type + if sys.platform == 'darwin': + ws = root.tk.call('tk', 'windowingsystem') + if 'x11' in ws: + _tk_type = "xquartz" + elif 'aqua' not in ws: + _tk_type = "other" + elif 'AppKit' in root.tk.call('winfo', 'server', '.'): + _tk_type = "cocoa" + else: + _tk_type = "carbon" + else: + _tk_type = "other" -def isCarbonAquaTk(root): +def isAquaTk(): + """ + Returns True if IDLE is using a native OS X Tk (Cocoa or Carbon). + """ + assert _tk_type is not None + return _tk_type == "cocoa" or _tk_type == "carbon" + +def isCarbonTk(): """ Returns True if IDLE is using a Carbon Aqua Tk (instead of the newer Cocoa Aqua Tk). """ - global _carbonaquatk - if _carbonaquatk is None: - _carbonaquatk = (runningAsOSXApp() and - 'aqua' in root.tk.call('tk', 'windowingsystem') and - 'AppKit' not in root.tk.call('winfo', 'server', '.')) - return _carbonaquatk + assert _tk_type is not None + return _tk_type == "carbon" + +def isCocoaTk(): + """ + Returns True if IDLE is using a Cocoa Aqua Tk. + """ + assert _tk_type is not None + return _tk_type == "cocoa" + +def isXQuartz(): + """ + Returns True if IDLE is using an OS X X11 Tk. + """ + assert _tk_type is not None + return _tk_type == "xquartz" def tkVersionWarning(root): """ @@ -53,8 +75,7 @@ def tkVersionWarning(root): can still crash unexpectedly. """ - if (runningAsOSXApp() and - ('AppKit' in root.tk.call('winfo', 'server', '.')) ): + if isCocoaTk(): patchlevel = root.tk.call('info', 'patchlevel') if patchlevel not in ('8.5.7', '8.5.9'): return False @@ -88,8 +109,8 @@ def hideTkConsole(root): def overrideRootMenu(root, flist): """ - Replace the Tk root menu by something that's more appropriate for - IDLE. + Replace the Tk root menu by something that is more appropriate for + IDLE with an Aqua Tk. """ # The menu that is attached to the Tk root (".") is also used by AquaTk for # all windows that don't specify a menu of their own. The default menubar @@ -102,17 +123,29 @@ def overrideRootMenu(root, flist): # # Due to a (mis-)feature of TkAqua the user will also see an empty Help # menu. - from tkinter import Menu, Text, Text - from idlelib.EditorWindow import prepstr, get_accelerator + from tkinter import Menu from idlelib import Bindings from idlelib import WindowList - from idlelib.MultiCall import MultiCallCreator + closeItem = Bindings.menudefs[0][1][-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 Bindings.menudefs[0][1][-3:] + Bindings.menudefs[0][1].insert(6, closeItem) + + # Remove the 'About' entry from the help menu, it is in the application + # menu + del Bindings.menudefs[-1][1][0:2] + # Remove the 'Configure Idle' entry from the options menu, it is in the + # application menu as 'Preferences' + del Bindings.menudefs[-2][1][0] menubar = Menu(root) root.configure(menu=menubar) menudict = {} - menudict['windows'] = menu = Menu(menubar, name='windows') + menudict['windows'] = menu = Menu(menubar, name='windows', tearoff=0) menubar.add_cascade(label='Window', menu=menu, underline=0) def postwindowsmenu(menu=menu): @@ -126,10 +159,14 @@ def overrideRootMenu(root, flist): WindowList.register_callback(postwindowsmenu) def about_dialog(event=None): + "Handle Help 'About IDLE' event." + # Synchronize with EditorWindow.EditorWindow.about_dialog. from idlelib import aboutDialog aboutDialog.AboutDialog(root, 'About IDLE') def config_dialog(event=None): + "Handle Options 'Configure IDLE' event." + # Synchronize with EditorWindow.EditorWindow.config_dialog. from idlelib import configDialog # Ensure that the root object has an instance_dict attribute, @@ -137,13 +174,13 @@ def overrideRootMenu(root, flist): # on an EditorWindow instance that is then passed as the first # argument to ConfigDialog) root.instance_dict = flist.inversedict - root.instance_dict = flist.inversedict configDialog.ConfigDialog(root, 'Settings') def help_dialog(event=None): - from idlelib import textView - fn = path.join(path.abspath(path.dirname(__file__)), 'help.txt') - textView.view_file(root, 'Help', fn) + "Handle Help 'IDLE Help' event." + # Synchronize with EditorWindow.EditorWindow.help_dialog. + from idlelib import help + help.show_idlehelp(root) root.bind('<<about-idle>>', about_dialog) root.bind('<<open-config-dialog>>', config_dialog) @@ -156,9 +193,10 @@ def overrideRootMenu(root, flist): # right thing for now. root.createcommand('exit', flist.close_all_callback) - if isCarbonAquaTk(root): + if isCarbonTk(): # for Carbon AquaTk, replace the default Tk apple menu - menudict['application'] = menu = Menu(menubar, name='apple') + menudict['application'] = menu = Menu(menubar, name='apple', + tearoff=0) menubar.add_cascade(label='IDLE', menu=menu) Bindings.menudefs.insert(0, ('application', [ @@ -171,8 +209,7 @@ def overrideRootMenu(root, flist): Bindings.menudefs[0][1].append( ('_Preferences....', '<<open-config-dialog>>'), ) - else: - # assume Cocoa AquaTk + if isCocoaTk(): # replace default About dialog with About IDLE one root.createcommand('tkAboutDialog', about_dialog) # replace default "Help" item in Help menu @@ -182,10 +219,22 @@ def overrideRootMenu(root, flist): def setupApp(root, flist): """ - Perform setup for the OSX application bundle. + Perform initial OS X customizations if needed. + Called from PyShell.main() after initial calls to Tk() + + There are currently three major versions of Tk in use on OS X: + 1. Aqua Cocoa Tk (native default since OS X 10.6) + 2. Aqua Carbon Tk (original native, 32-bit only, deprecated) + 3. X11 (supported by some third-party distributors, deprecated) + There are various differences among the three that affect IDLE + behavior, primarily with menus, mouse key events, and accelerators. + Some one-time customizations are performed here. + Others are dynamically tested throughout idlelib by calls to the + isAquaTk(), isCarbonTk(), isCocoaTk(), isXQuartz() functions which + are initialized here as well. """ - if not runningAsOSXApp(): return - - hideTkConsole(root) - overrideRootMenu(root, flist) - addOpenEventSupport(root, flist) + _initializeTkVariantTests(root) + if isAquaTk(): + hideTkConsole(root) + overrideRootMenu(root, flist) + addOpenEventSupport(root, flist) diff --git a/Lib/idlelib/rpc.py b/Lib/idlelib/rpc.py index ddce6e9389..aa33041e7b 100644 --- a/Lib/idlelib/rpc.py +++ b/Lib/idlelib/rpc.py @@ -29,6 +29,7 @@ accomplished in Idle. import sys import os +import io import socket import select import socketserver @@ -53,16 +54,15 @@ def pickle_code(co): ms = marshal.dumps(co) return unpickle_code, (ms,) -# XXX KBK 24Aug02 function pickling capability not used in Idle -# def unpickle_function(ms): -# return ms +def dumps(obj, protocol=None): + f = io.BytesIO() + p = CodePickler(f, protocol) + p.dump(obj) + return f.getvalue() -# def pickle_function(fn): -# assert isinstance(fn, type.FunctionType) -# return repr(fn) - -copyreg.pickle(types.CodeType, pickle_code, unpickle_code) -# copyreg.pickle(types.FunctionType, pickle_function, unpickle_function) +class CodePickler(pickle.Pickler): + dispatch_table = {types.CodeType: pickle_code} + dispatch_table.update(copyreg.dispatch_table) BUFSIZE = 8*1024 LOCALHOST = '127.0.0.1' @@ -199,7 +199,7 @@ class SocketIO(object): raise except KeyboardInterrupt: raise - except socket.error: + except OSError: raise except Exception as ex: return ("CALLEXC", ex) @@ -329,7 +329,7 @@ class SocketIO(object): def putmessage(self, message): self.debug("putmessage:%d:" % message[0]) try: - s = pickle.dumps(message) + s = dumps(message) except pickle.PicklingError: print("Cannot pickle:", repr(message), file=sys.__stderr__) raise @@ -340,10 +340,7 @@ class SocketIO(object): n = self.sock.send(s[:BUFSIZE]) except (AttributeError, TypeError): raise OSError("socket no longer exists") - except socket.error: - raise - else: - s = s[n:] + s = s[n:] buff = b'' bufneed = 4 @@ -357,7 +354,7 @@ class SocketIO(object): return None try: s = self.sock.recv(BUFSIZE) - except socket.error: + except OSError: raise EOFError if len(s) == 0: raise EOFError @@ -537,7 +534,7 @@ class RPCClient(SocketIO): SocketIO.__init__(self, working_sock) else: print("** Invalid host: ", address, file=sys.__stderr__) - raise socket.error + raise OSError def get_remote_proxy(self, oid): return RPCProxy(self, oid) diff --git a/Lib/idlelib/run.py b/Lib/idlelib/run.py index c1859b66b2..595e7bc3aa 100644 --- a/Lib/idlelib/run.py +++ b/Lib/idlelib/run.py @@ -1,8 +1,6 @@ import sys -import io import linecache import time -import socket import traceback import _thread as thread import threading @@ -150,8 +148,8 @@ def manage_socket(address): try: server = MyRPCServer(address, MyHandler) break - except socket.error as err: - print("IDLE Subprocess: socket error: " + err.args[1] + + except OSError as err: + print("IDLE Subprocess: OSError: " + err.args[1] + ", retrying....", file=sys.__stderr__) socket_error = err else: @@ -176,7 +174,7 @@ def show_socket_error(err, address): tkMessageBox.showerror("IDLE Subprocess Error", msg, parent=root) else: tkMessageBox.showerror("IDLE Subprocess Error", - "Socket Error: %s" % err.args[1]) + "Socket Error: %s" % err.args[1], parent=root) root.destroy() def print_exception(): diff --git a/Lib/idlelib/tabbedpages.py b/Lib/idlelib/tabbedpages.py index 2557732755..965f9f8593 100644 --- a/Lib/idlelib/tabbedpages.py +++ b/Lib/idlelib/tabbedpages.py @@ -467,9 +467,12 @@ class TabbedPageSet(Frame): self._tab_set.set_selected_tab(page_name) -if __name__ == '__main__': +def _tabbed_pages(parent): # test dialog root=Tk() + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 175)) + root.title("Test tabbed pages") tabPage=TabbedPageSet(root, page_names=['Foobar','Baz'], n_rows=0, expand_tabs=False, ) @@ -488,3 +491,8 @@ if __name__ == '__main__': labelPgName.pack(padx=5) entryPgName.pack(padx=5) root.mainloop() + + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(_tabbed_pages) diff --git a/Lib/idlelib/testcode.py b/Lib/idlelib/testcode.py deleted file mode 100644 index 05eaa562cd..0000000000 --- a/Lib/idlelib/testcode.py +++ /dev/null @@ -1,31 +0,0 @@ -import string - -def f(): - a = 0 - b = 1 - c = 2 - d = 3 - e = 4 - g() - -def g(): - h() - -def h(): - i() - -def i(): - j() - -def j(): - k() - -def k(): - l() - -l = lambda: test() - -def test(): - string.capwords(1) - -f() diff --git a/Lib/idlelib/textView.py b/Lib/idlelib/textView.py index dd50544c41..01b2d8f4ab 100644 --- a/Lib/idlelib/textView.py +++ b/Lib/idlelib/textView.py @@ -9,15 +9,21 @@ class TextViewer(Toplevel): """A simple text viewer dialog for IDLE """ - def __init__(self, parent, title, text, modal=True): + def __init__(self, parent, title, text, modal=True, _htest=False): """Show the given text in a scrollable window with a 'close' button + If modal option set to False, user can interact with other windows, + otherwise they will be unable to interact with other windows until + the textview window is closed. + + _htest - bool; change box location when running htest. """ Toplevel.__init__(self, parent) self.configure(borderwidth=5) - self.geometry("=%dx%d+%d+%d" % (625, 500, - parent.winfo_rootx() + 10, - parent.winfo_rooty() + 10)) + # place dialog below parent if running htest + self.geometry("=%dx%d+%d+%d" % (750, 500, + parent.winfo_rootx() + 10, + parent.winfo_rooty() + (10 if not _htest else 100))) #elguavas - config placeholders til config stuff completed self.bg = '#ffffff' self.fg = '#000000' @@ -66,32 +72,15 @@ def view_file(parent, title, filename, encoding=None, modal=True): try: with open(filename, 'r', encoding=encoding) as file: contents = file.read() - except OSError: - import tkinter.messagebox as tkMessageBox + except IOError: tkMessageBox.showerror(title='File Load Error', message='Unable to load file %r .' % filename, parent=parent) else: return view_text(parent, title, contents, modal) - if __name__ == '__main__': - #test the dialog - root=Tk() - root.title('textView test') - filename = './textView.py' - with open(filename, 'r') as f: - text = f.read() - btn1 = Button(root, text='view_text', - command=lambda:view_text(root, 'view_text', text)) - btn1.pack(side=LEFT) - btn2 = Button(root, text='view_file', - command=lambda:view_file(root, 'view_file', filename)) - btn2.pack(side=LEFT) - btn3 = Button(root, text='nonmodal view_text', - command=lambda:view_text(root, 'nonmodal view_text', text, - modal=False)) - btn3.pack(side=LEFT) - close = Button(root, text='Close', command=root.destroy) - close.pack(side=RIGHT) - root.mainloop() + import unittest + unittest.main('idlelib.idle_test.test_textview', verbosity=2, exit=False) + from idlelib.idle_test.htest import run + run(TextViewer) |