diff options
Diffstat (limited to 'Lib/idlelib')
160 files changed, 9985 insertions, 21849 deletions
diff --git a/Lib/idlelib/autocomplete.py b/Lib/idlelib/AutoComplete.py index c623d45..9381bda 100644 --- a/Lib/idlelib/autocomplete.py +++ b/Lib/idlelib/AutoComplete.py @@ -1,53 +1,58 @@ -"""Complete either attribute names or file names. +"""AutoComplete.py - An IDLE extension for automatically completing names. -Either on demand or after a user-selected delay after a key character, -pop up a list of candidates. +This extension can complete either attribute names or file names. It can pop +a window with all available names, for the user to select from. """ -import __main__ import os -import string import sys +import string -# Two types of completions; defined here for autocomplete_w import below. -ATTRS, FILES = 0, 1 -from idlelib import autocomplete_w -from idlelib.config import idleConf -from idlelib.hyperparser import HyperParser - -# Tuples passed to open_completions. -# EvalFunc, Complete, WantWin, Mode -FORCE = True, False, True, None # Control-Space. -TAB = False, True, True, None # Tab. -TRY_A = False, False, False, ATTRS # '.' for attributes. -TRY_F = False, False, False, FILES # '/' in quotes for file name. - -# This string includes all chars that may be in an identifier. -# TODO Update this here and elsewhere. +from idlelib.configHandler import idleConf + +# This string includes all chars that may be in a file name (without a path +# separator) +FILENAME_CHARS = string.ascii_letters + string.digits + os.curdir + "._~#$:-" +# This string includes all chars that may be in an identifier ID_CHARS = string.ascii_letters + string.digits + "_" -SEPS = f"{os.sep}{os.altsep if os.altsep else ''}" -TRIGGERS = f".{SEPS}" +# These constants represent the two different types of completions +COMPLETE_ATTRIBUTES, COMPLETE_FILES = range(1, 2+1) + +from idlelib import AutoCompleteWindow +from idlelib.HyperParser import HyperParser + +import __main__ + +SEPS = os.sep +if os.altsep: # e.g. '/' on Windows... + SEPS += os.altsep class AutoComplete: + menudefs = [ + ('edit', [ + ("Show Completions", "<<force-open-completions>>"), + ]) + ] + + popupwait = idleConf.GetOption("extensions", "AutoComplete", + "popupwait", type="int", default=0) + def __init__(self, editwin=None): self.editwin = editwin - if editwin is not None: # not in subprocess or no-gui test - self.text = editwin.text + if editwin is None: # subprocess and test + return + self.text = editwin.text self.autocompletewindow = None - # id of delayed call, and the index of the text insert when - # the delayed call was issued. If _delayed_completion_id is - # None, there is no delayed call. + + # id of delayed call, and the index of the text insert when the delayed + # call was issued. If _delayed_completion_id is None, there is no + # delayed call. self._delayed_completion_id = None self._delayed_completion_index = None - @classmethod - def reload(cls): - cls.popupwait = idleConf.GetOption( - "extensions", "AutoComplete", "popupwait", type="int", default=0) - - def _make_autocomplete_window(self): # Makes mocking easier. - return autocomplete_w.AutoCompleteWindow(self.text) + def _make_autocomplete_window(self): + return AutoCompleteWindow.AutoCompleteWindow(self.text) def _remove_autocomplete_window(self, event=None): if self.autocompletewindow: @@ -55,49 +60,61 @@ class AutoComplete: self.autocompletewindow = None def force_open_completions_event(self, event): - "(^space) Open completion list, even if a function call is needed." - self.open_completions(FORCE) - return "break" + """Happens when the user really wants to open a completion list, even + if a function call is needed. + """ + self.open_completions(True, False, True) + + def try_open_completions_event(self, event): + """Happens when it would be nice to open a completion list, but not + really necessary, for example after a dot, so function + calls won't be made. + """ + lastchar = self.text.get("insert-1c") + if lastchar == ".": + self._open_completions_later(False, False, False, + COMPLETE_ATTRIBUTES) + elif lastchar in SEPS: + self._open_completions_later(False, False, False, + COMPLETE_FILES) def autocomplete_event(self, event): - "(tab) Complete word or open list if multiple options." - if hasattr(event, "mc_state") and event.mc_state or\ - not self.text.get("insert linestart", "insert").strip(): - # A modifier was pressed along with the tab or - # there is only previous whitespace on this line, so tab. - return None + """Happens when the user wants to complete his word, and if necessary, + open a completion list after that (if there is more than one + completion) + """ + if hasattr(event, "mc_state") and event.mc_state: + # A modifier was pressed along with the tab, continue as usual. + return if self.autocompletewindow and self.autocompletewindow.is_active(): self.autocompletewindow.complete() return "break" else: - opened = self.open_completions(TAB) - return "break" if opened else None + opened = self.open_completions(False, True, True) + if opened: + return "break" - def try_open_completions_event(self, event=None): - "(./) Open completion list after pause with no movement." - lastchar = self.text.get("insert-1c") - if lastchar in TRIGGERS: - args = TRY_A if lastchar == "." else TRY_F - self._delayed_completion_index = self.text.index("insert") - if self._delayed_completion_id is not None: - self.text.after_cancel(self._delayed_completion_id) - self._delayed_completion_id = self.text.after( - self.popupwait, self._delayed_open_completions, args) - - def _delayed_open_completions(self, args): - "Call open_completions if index unchanged." + def _open_completions_later(self, *args): + self._delayed_completion_index = self.text.index("insert") + if self._delayed_completion_id is not None: + self.text.after_cancel(self._delayed_completion_id) + self._delayed_completion_id = \ + self.text.after(self.popupwait, self._delayed_open_completions, + *args) + + def _delayed_open_completions(self, *args): self._delayed_completion_id = None - if self.text.index("insert") == self._delayed_completion_index: - self.open_completions(args) + if self.text.index("insert") != self._delayed_completion_index: + return + self.open_completions(*args) - def open_completions(self, args): + def open_completions(self, evalfuncs, complete, userWantsWin, mode=None): """Find the completions and create the AutoCompleteWindow. Return True if successful (no syntax error or so found). - If complete is True, then if there's nothing to complete and no + if complete is True, then if there's nothing to complete and no start of completion, won't open completions and return False. If mode is given, will open a completion list only in this mode. """ - evalfuncs, complete, wantwin, mode = args # Cancel another delayed call, if it exists. if self._delayed_completion_id is not None: self.text.after_cancel(self._delayed_completion_id) @@ -106,49 +123,42 @@ class AutoComplete: hp = HyperParser(self.editwin, "insert") curline = self.text.get("insert linestart", "insert") i = j = len(curline) - if hp.is_in_string() and (not mode or mode==FILES): - # Find the beginning of the string. - # fetch_completions will look at the file system to determine - # whether the string value constitutes an actual file name - # XXX could consider raw strings here and unescape the string - # value if it's not raw. + if hp.is_in_string() and (not mode or mode==COMPLETE_FILES): self._remove_autocomplete_window() - mode = FILES - # Find last separator or string start - while i and curline[i-1] not in "'\"" + SEPS: + mode = COMPLETE_FILES + while i and curline[i-1] in FILENAME_CHARS: i -= 1 comp_start = curline[i:j] j = i - # Find string start - while i and curline[i-1] not in "'\"": + while i and curline[i-1] in FILENAME_CHARS + SEPS: i -= 1 comp_what = curline[i:j] - elif hp.is_in_code() and (not mode or mode==ATTRS): + elif hp.is_in_code() and (not mode or mode==COMPLETE_ATTRIBUTES): self._remove_autocomplete_window() - mode = ATTRS - while i and (curline[i-1] in ID_CHARS or ord(curline[i-1]) > 127): + mode = COMPLETE_ATTRIBUTES + while i and curline[i-1] in ID_CHARS: i -= 1 comp_start = curline[i:j] - if i and curline[i-1] == '.': # Need object with attributes. + if i and curline[i-1] == '.': hp.set_index("insert-%dc" % (len(curline)-(i-1))) comp_what = hp.get_expression() - if (not comp_what or - (not evalfuncs and comp_what.find('(') != -1)): - return None + if not comp_what or \ + (not evalfuncs and comp_what.find('(') != -1): + return else: comp_what = "" else: - return None + return if complete and not comp_what and not comp_start: - return None + return comp_lists = self.fetch_completions(comp_what, mode) if not comp_lists[0]: - return None + return self.autocompletewindow = self._make_autocomplete_window() return not self.autocompletewindow.show_window( comp_lists, "insert-%dc" % len(comp_start), - complete, mode, wantwin) + complete, mode, userWantsWin) def fetch_completions(self, what, mode): """Return a pair of lists of completions for something. The first list @@ -170,10 +180,10 @@ class AutoComplete: return rpcclt.remotecall("exec", "get_the_completion_list", (what, mode), {}) else: - if mode == ATTRS: + if mode == COMPLETE_ATTRIBUTES: if what == "": - namespace = {**__main__.__builtins__.__dict__, - **__main__.__dict__} + namespace = __main__.__dict__.copy() + namespace.update(__main__.__builtins__.__dict__) bigl = eval("dir()", namespace) bigl.sort() if "__all__" in bigl: @@ -192,7 +202,7 @@ class AutoComplete: except: return [], [] - elif mode == FILES: + elif mode == COMPLETE_FILES: if what == "": what = "." try: @@ -208,11 +218,11 @@ class AutoComplete: return smalll, bigl def get_entity(self, name): - "Lookup name in a namespace spanning sys.modules and __main.dict__." - return eval(name, {**sys.modules, **__main__.__dict__}) - + """Lookup name in a namespace spanning sys.modules and __main.dict__""" + namespace = sys.modules.copy() + namespace.update(__main__.__dict__) + return eval(name, namespace) -AutoComplete.reload() if __name__ == '__main__': from unittest import main diff --git a/Lib/idlelib/autocomplete_w.py b/Lib/idlelib/AutoCompleteWindow.py index 0643c09..205a29b 100644 --- a/Lib/idlelib/autocomplete_w.py +++ b/Lib/idlelib/AutoCompleteWindow.py @@ -1,23 +1,18 @@ """ -An auto-completion window for IDLE, used by the autocomplete extension +An auto-completion window for IDLE, used by the AutoComplete extension """ -import platform - -from tkinter import * -from tkinter.ttk import Frame, Scrollbar - -from idlelib.autocomplete import FILES, ATTRS -from idlelib.multicall import MC_SHIFT +from Tkinter import * +from idlelib.MultiCall import MC_SHIFT +from idlelib.AutoComplete import COMPLETE_FILES, COMPLETE_ATTRIBUTES HIDE_VIRTUAL_EVENT_NAME = "<<autocompletewindow-hide>>" -HIDE_FOCUS_OUT_SEQUENCE = "<FocusOut>" -HIDE_SEQUENCES = (HIDE_FOCUS_OUT_SEQUENCE, "<ButtonPress>") +HIDE_SEQUENCES = ("<FocusOut>", "<ButtonPress>") KEYPRESS_VIRTUAL_EVENT_NAME = "<<autocompletewindow-keypress>>" # We need to bind event beyond <Key> so that the function will be called # before the default specific IDLE function KEYPRESS_SEQUENCES = ("<Key>", "<Key-BackSpace>", "<Key-Return>", "<Key-Tab>", "<Key-Up>", "<Key-Down>", "<Key-Home>", "<Key-End>", - "<Key-Prior>", "<Key-Next>", "<Key-Escape>") + "<Key-Prior>", "<Key-Next>") KEYRELEASE_VIRTUAL_EVENT_NAME = "<<autocompletewindow-keyrelease>>" KEYRELEASE_SEQUENCE = "<KeyRelease>" LISTUPDATE_SEQUENCE = "<B1-ButtonRelease>" @@ -39,7 +34,8 @@ class AutoCompleteWindow: self.completions = None # A list with more completions, or None self.morecompletions = None - # The completion mode, either autocomplete.ATTRS or .FILES. + # The completion mode. Either AutoComplete.COMPLETE_ATTRIBUTES or + # AutoComplete.COMPLETE_FILES self.mode = None # The current completion start, on the text box (a string) self.start = None @@ -52,12 +48,10 @@ class AutoCompleteWindow: # (for example, he clicked the list) self.userwantswindow = None # event ids - self.hideid = self.keypressid = self.listupdateid = \ - self.winconfigid = self.keyreleaseid = self.doubleclickid = None + self.hideid = self.keypressid = self.listupdateid = self.winconfigid \ + = self.keyreleaseid = self.doubleclickid = None # Flag set if last keypress was a tab self.lastkey_was_tab = False - # Flag set to avoid recursive <Configure> callback invocations. - self.is_configuring = False def _change_start(self, newstart): min_len = min(len(self.start), len(newstart)) @@ -74,8 +68,8 @@ class AutoCompleteWindow: def _binary_search(self, s): """Find the first index in self.completions where completions[i] is - greater or equal to s, or the last index if there is no such. - """ + greater or equal to s, or the last index if there is no such + one.""" i = 0; j = len(self.completions) while j > i: m = (i + j) // 2 @@ -88,8 +82,7 @@ class AutoCompleteWindow: def _complete_string(self, s): """Assuming that s is the prefix of a string in self.completions, return the longest string which is a prefix of all the strings which - s is a prefix of them. If s is not a prefix of a string, return s. - """ + s is a prefix of them. If s is not a prefix of a string, return s.""" first = self._binary_search(s) if self.completions[first][:len(s)] != s: # There is not even one completion which s is a prefix of. @@ -118,10 +111,8 @@ class AutoCompleteWindow: return first_comp[:i] def _selection_changed(self): - """Call when the selection of the Listbox has changed. - - Updates the Listbox display and calls _change_start. - """ + """Should be called when the selection of the Listbox has changed. + Updates the Listbox display and calls _change_start.""" cursel = int(self.listbox.curselection()[0]) self.listbox.see(cursel) @@ -157,10 +148,8 @@ class AutoCompleteWindow: def show_window(self, comp_lists, index, complete, mode, userWantsWin): """Show the autocomplete list, bind events. - - If complete is True, complete the text, and if there is exactly - one matching completion, don't open a list. - """ + If complete is True, complete the text, and if there is exactly one + matching completion, don't open a list.""" # Handle the start we already have self.completions, self.morecompletions = comp_lists self.mode = mode @@ -195,7 +184,7 @@ class AutoCompleteWindow: pass self.scrollbar = scrollbar = Scrollbar(acw, orient=VERTICAL) self.listbox = listbox = Listbox(acw, yscrollcommand=scrollbar.set, - exportselection=False) + exportselection=False, bg="white") for item in self.completions: listbox.insert(END, item) self.origselforeground = listbox.cget("selectforeground") @@ -210,12 +199,10 @@ class AutoCompleteWindow: self._selection_changed() # bind events - self.hideaid = acw.bind(HIDE_VIRTUAL_EVENT_NAME, self.hide_event) - self.hidewid = self.widget.bind(HIDE_VIRTUAL_EVENT_NAME, self.hide_event) - acw.event_add(HIDE_VIRTUAL_EVENT_NAME, HIDE_FOCUS_OUT_SEQUENCE) + self.hideid = self.widget.bind(HIDE_VIRTUAL_EVENT_NAME, + self.hide_event) for seq in HIDE_SEQUENCES: self.widget.event_add(HIDE_VIRTUAL_EVENT_NAME, seq) - self.keypressid = self.widget.bind(KEYPRESS_VIRTUAL_EVENT_NAME, self.keypress_event) for seq in KEYPRESS_SEQUENCES: @@ -225,18 +212,11 @@ class AutoCompleteWindow: self.widget.event_add(KEYRELEASE_VIRTUAL_EVENT_NAME,KEYRELEASE_SEQUENCE) self.listupdateid = listbox.bind(LISTUPDATE_SEQUENCE, self.listselect_event) - self.is_configuring = False self.winconfigid = acw.bind(WINCONFIG_SEQUENCE, self.winconfig_event) self.doubleclickid = listbox.bind(DOUBLECLICK_SEQUENCE, self.doubleclick_event) - return None def winconfig_event(self, event): - if self.is_configuring: - # Avoid running on recursive <Configure> callback invocations. - return - - self.is_configuring = True if not self.is_active(): return # Position the completion list window @@ -244,7 +224,6 @@ class AutoCompleteWindow: text.see(self.startindex) x, y, cx, cy = text.bbox(self.startindex) acw = self.autocompletewindow - acw.update() acw_width, acw_height = acw.winfo_width(), acw.winfo_height() text_width, text_height = text.winfo_width(), text.winfo_height() new_x = text.winfo_rootx() + min(x, max(0, text_width - acw_width)) @@ -257,47 +236,18 @@ class AutoCompleteWindow: # place acw above current line new_y -= acw_height acw.wm_geometry("+%d+%d" % (new_x, new_y)) - acw.update_idletasks() - - if platform.system().startswith('Windows'): - # See issue 15786. When on Windows platform, Tk will misbehave - # to call winconfig_event multiple times, we need to prevent this, - # otherwise mouse button double click will not be able to used. - acw.unbind(WINCONFIG_SEQUENCE, self.winconfigid) - self.winconfigid = None - - self.is_configuring = False - - def _hide_event_check(self): - if not self.autocompletewindow: - return - - try: - if not self.autocompletewindow.focus_get(): - self.hide_window() - except KeyError: - # See issue 734176, when user click on menu, acw.focus_get() - # will get KeyError. - self.hide_window() def hide_event(self, event): - # Hide autocomplete list if it exists and does not have focus or - # mouse click on widget / text area. - if self.is_active(): - if event.type == EventType.FocusOut: - # On Windows platform, it will need to delay the check for - # acw.focus_get() when click on acw, otherwise it will return - # None and close the window - self.widget.after(1, self._hide_event_check) - elif event.type == EventType.ButtonPress: - # ButtonPress event only bind to self.widget - self.hide_window() + if not self.is_active(): + return + self.hide_window() def listselect_event(self, event): - if self.is_active(): - self.userwantswindow = True - cursel = int(self.listbox.curselection()[0]) - self._change_start(self.completions[cursel]) + if not self.is_active(): + return + self.userwantswindow = True + cursel = int(self.listbox.curselection()[0]) + self._change_start(self.completions[cursel]) def doubleclick_event(self, event): # Put the selected completion in the text, and close the list @@ -307,7 +257,7 @@ class AutoCompleteWindow: def keypress_event(self, event): if not self.is_active(): - return None + return keysym = event.keysym if hasattr(event, "mc_state"): state = event.mc_state @@ -316,7 +266,7 @@ class AutoCompleteWindow: if keysym != "Tab": self.lastkey_was_tab = False if (len(keysym) == 1 or keysym in ("underscore", "BackSpace") - or (self.mode == FILES and keysym in + or (self.mode == COMPLETE_FILES and keysym in ("period", "minus"))) \ and not (state & ~MC_SHIFT): # Normal editing of text @@ -332,7 +282,7 @@ class AutoCompleteWindow: # keysym == "BackSpace" if len(self.start) == 0: self.hide_window() - return None + return self._change_start(self.start[:-1]) self.lasttypedstart = self.start self.listbox.select_clear(0, int(self.listbox.curselection()[0])) @@ -341,14 +291,13 @@ class AutoCompleteWindow: return "break" elif keysym == "Return": - self.complete() self.hide_window() - return 'break' + return - elif (self.mode == ATTRS and keysym in + elif (self.mode == COMPLETE_ATTRIBUTES and keysym in ("period", "space", "parenleft", "parenright", "bracketleft", "bracketright")) or \ - (self.mode == FILES and keysym in + (self.mode == COMPLETE_FILES and keysym in ("slash", "backslash", "quotedbl", "apostrophe")) \ and not (state & ~MC_SHIFT): # If start is a prefix of the selection, but is not '' when @@ -356,10 +305,10 @@ class AutoCompleteWindow: # selected completion. Anyway, close the list. cursel = int(self.listbox.curselection()[0]) if self.completions[cursel][:len(self.start)] == self.start \ - and (self.mode == ATTRS or self.start): + and (self.mode == COMPLETE_ATTRIBUTES or self.start): self._change_start(self.completions[cursel]) self.hide_window() - return None + return elif keysym in ("Home", "End", "Prior", "Next", "Up", "Down") and \ not state: @@ -400,26 +349,17 @@ class AutoCompleteWindow: # first tab; let AutoComplete handle the completion self.userwantswindow = True self.lastkey_was_tab = True - return None + return elif any(s in keysym for s in ("Shift", "Control", "Alt", "Meta", "Command", "Option")): # A modifier key, so ignore - return None - - elif event.char and event.char >= ' ': - # Regular character with a non-length-1 keycode - self._change_start(self.start + event.char) - self.lasttypedstart = self.start - self.listbox.select_clear(0, int(self.listbox.curselection()[0])) - self.listbox.select_set(self._binary_search(self.start)) - self._selection_changed() - return "break" + return else: # Unknown event, close the window and let it through. self.hide_window() - return None + return def keyrelease_event(self, event): if not self.is_active(): @@ -441,15 +381,10 @@ class AutoCompleteWindow: return # unbind events - self.autocompletewindow.event_delete(HIDE_VIRTUAL_EVENT_NAME, - HIDE_FOCUS_OUT_SEQUENCE) for seq in HIDE_SEQUENCES: self.widget.event_delete(HIDE_VIRTUAL_EVENT_NAME, seq) - - self.autocompletewindow.unbind(HIDE_VIRTUAL_EVENT_NAME, self.hideaid) - self.widget.unbind(HIDE_VIRTUAL_EVENT_NAME, self.hidewid) - self.hideaid = None - self.hidewid = None + self.widget.unbind(HIDE_VIRTUAL_EVENT_NAME, self.hideid) + self.hideid = None for seq in KEYPRESS_SEQUENCES: self.widget.event_delete(KEYPRESS_VIRTUAL_EVENT_NAME, seq) self.widget.unbind(KEYPRESS_VIRTUAL_EVENT_NAME, self.keypressid) @@ -460,12 +395,8 @@ class AutoCompleteWindow: self.keyreleaseid = None self.listbox.unbind(LISTUPDATE_SEQUENCE, self.listupdateid) self.listupdateid = None - if self.winconfigid: - self.autocompletewindow.unbind(WINCONFIG_SEQUENCE, self.winconfigid) - self.winconfigid = None - - # Re-focusOn frame.text (See issue #15786) - self.widget.focus_set() + self.autocompletewindow.unbind(WINCONFIG_SEQUENCE, self.winconfigid) + self.winconfigid = None # destroy widgets self.scrollbar.destroy() @@ -474,10 +405,3 @@ class AutoCompleteWindow: self.listbox = None self.autocompletewindow.destroy() self.autocompletewindow = None - - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_autocomplete_w', verbosity=2, exit=False) - -# TODO: autocomplete/w htest here diff --git a/Lib/idlelib/autoexpand.py b/Lib/idlelib/AutoExpand.py index 92f5c84..7059054 100644 --- a/Lib/idlelib/autoexpand.py +++ b/Lib/idlelib/AutoExpand.py @@ -10,18 +10,27 @@ Changing the current text line or leaving the cursor in a different place before requesting the next selection causes AutoExpand to reset its state. -There is only one instance of Autoexpand. +This is an extension file and there is only one instance of AutoExpand. ''' -import re import string +import re +###$ event <<expand-word>> +###$ win <Alt-slash> +###$ unix <Alt-slash> class AutoExpand: + + menudefs = [ + ('edit', [ + ('E_xpand Word', '<<expand-word>>'), + ]), + ] + wordchars = string.ascii_letters + string.digits + "_" def __init__(self, editwin): self.text = editwin.text - self.bell = self.text.bell self.state = None def expand_word_event(self, event): @@ -37,14 +46,14 @@ class AutoExpand: words = self.getwords() index = 0 if not words: - self.bell() + self.text.bell() return "break" word = self.getprevword() self.text.delete("insert - %d chars" % len(word), "insert") newword = words[index] index = (index + 1) % len(words) if index == 0: - self.bell() # Warn we cycled around + self.text.bell() # Warn we cycled around self.text.insert("insert", newword) curinsert = self.text.index("insert") curline = self.text.get("insert linestart", "insert lineend") @@ -90,7 +99,6 @@ class AutoExpand: i = i-1 return line[i:] - if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_autoexpand', verbosity=2) + import unittest + unittest.main('idlelib.idle_test.test_autoexpand', verbosity=2) diff --git a/Lib/idlelib/mainmenu.py b/Lib/idlelib/Bindings.py index 74edce2..2fd8532 100644 --- a/Lib/idlelib/mainmenu.py +++ b/Lib/idlelib/Bindings.py @@ -8,11 +8,9 @@ the PythonShell window, and a Format menu which is only present in the Editor windows. """ -from importlib.util import find_spec +from idlelib.configHandler import idleConf -from idlelib.config import idleConf - -# Warning: menudefs is altered in macosx.overrideRootMenu() +# 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 @@ -25,7 +23,7 @@ menudefs = [ ('_New File', '<<open-new-window>>'), ('_Open...', '<<open-window-from-file>>'), ('Open _Module...', '<<open-module>>'), - ('Module _Browser', '<<open-class-browser>>'), + ('Class _Browser', '<<open-class-browser>>'), ('_Path Browser', '<<open-path-browser>>'), None, ('_Save', '<<save-window>>'), @@ -36,8 +34,7 @@ menudefs = [ None, ('_Close', '<<close-window>>'), ('E_xit', '<<close-all-windows>>'), - ]), - + ]), ('edit', [ ('_Undo', '<<undo>>'), ('_Redo', '<<redo>>'), @@ -53,14 +50,8 @@ menudefs = [ ('Find in Files...', '<<find-in-files>>'), ('R_eplace...', '<<replace>>'), ('Go to _Line', '<<goto-line>>'), - ('S_how Completions', '<<force-open-completions>>'), - ('E_xpand Word', '<<expand-word>>'), - ('Show C_all Tip', '<<force-open-calltip>>'), - ('Show Surrounding P_arens', '<<flash-paren>>'), - ]), - - ('format', [ - ('F_ormat Paragraph', '<<format-paragraph>>'), + ]), +('format', [ ('_Indent Region', '<<indent-region>>'), ('_Dedent Region', '<<dedent-region>>'), ('Comment _Out Region', '<<comment-region>>'), @@ -69,44 +60,26 @@ menudefs = [ ('Untabify Region', '<<untabify-region>>'), ('Toggle Tabs', '<<toggle-tabs>>'), ('New Indent Width', '<<change-indentwidth>>'), - ('S_trip Trailing Whitespace', '<<do-rstrip>>'), ]), - ('run', [ - ('R_un Module', '<<run-module>>'), - ('Run... _Customized', '<<run-custom>>'), - ('C_heck Module', '<<check-module>>'), ('Python Shell', '<<open-python-shell>>'), ]), - ('shell', [ ('_View Last Restart', '<<view-restart>>'), ('_Restart Shell', '<<restart-shell>>'), None, - ('_Previous History', '<<history-previous>>'), - ('_Next History', '<<history-next>>'), - None, ('_Interrupt Execution', '<<interrupt-execution>>'), ]), - ('debug', [ ('_Go to File/Line', '<<goto-file-line>>'), ('!_Debugger', '<<toggle-debugger>>'), ('_Stack Viewer', '<<open-stack-viewer>>'), ('!_Auto-open Stack Viewer', '<<toggle-jit-stack-viewer>>'), ]), - ('options', [ ('Configure _IDLE', '<<open-config-dialog>>'), None, - ('Show _Code Context', '<<toggle-code-context>>'), - ('Show _Line Numbers', '<<toggle-line-numbers>>'), - ('_Zoom Height', '<<zoom-height>>'), - ]), - - ('window', [ ]), - ('help', [ ('_About IDLE', '<<about-idle>>'), None, @@ -115,11 +88,4 @@ menudefs = [ ]), ] -if find_spec('turtledemo'): - menudefs[-1][1].append(('Turtle Demo', '<<open-turtle-demo>>')) - default_keydefs = idleConf.GetCurrentKeySet() - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_mainmenu', verbosity=2) diff --git a/Lib/idlelib/CallTipWindow.py b/Lib/idlelib/CallTipWindow.py new file mode 100644 index 0000000..2a453d0 --- /dev/null +++ b/Lib/idlelib/CallTipWindow.py @@ -0,0 +1,162 @@ +"""A CallTip window class for Tkinter/IDLE. + +After ToolTip.py, which uses ideas gleaned from PySol +Used by the CallTips IDLE extension. +""" +from Tkinter import Toplevel, Label, LEFT, SOLID, TclError + +HIDE_VIRTUAL_EVENT_NAME = "<<calltipwindow-hide>>" +HIDE_SEQUENCES = ("<Key-Escape>", "<FocusOut>") +CHECKHIDE_VIRTUAL_EVENT_NAME = "<<calltipwindow-checkhide>>" +CHECKHIDE_SEQUENCES = ("<KeyRelease>", "<ButtonRelease>") +CHECKHIDE_TIME = 100 # milliseconds + +MARK_RIGHT = "calltipwindowregion_right" + +class CallTip: + + def __init__(self, widget): + self.widget = widget + self.tipwindow = self.label = None + self.parenline = self.parencol = None + self.lastline = None + self.hideid = self.checkhideid = None + self.checkhide_after_id = None + + def position_window(self): + """Check if needs to reposition the window, and if so - do it.""" + curline = int(self.widget.index("insert").split('.')[0]) + if curline == self.lastline: + return + self.lastline = curline + self.widget.see("insert") + if curline == self.parenline: + box = self.widget.bbox("%d.%d" % (self.parenline, + self.parencol)) + else: + box = self.widget.bbox("%d.0" % curline) + if not box: + box = list(self.widget.bbox("insert")) + # align to left of window + box[0] = 0 + box[2] = 0 + x = box[0] + self.widget.winfo_rootx() + 2 + y = box[1] + box[3] + self.widget.winfo_rooty() + self.tipwindow.wm_geometry("+%d+%d" % (x, y)) + + def showtip(self, text, parenleft, parenright): + """Show the calltip, bind events which will close it and reposition it. + """ + # Only called in CallTips, where lines are truncated + self.text = text + if self.tipwindow or not self.text: + return + + self.widget.mark_set(MARK_RIGHT, parenright) + self.parenline, self.parencol = map( + int, self.widget.index(parenleft).split(".")) + + self.tipwindow = tw = Toplevel(self.widget) + self.position_window() + # remove border on calltip window + tw.wm_overrideredirect(1) + try: + # This command is only needed and available on Tk >= 8.4.0 for OSX + # Without it, call tips intrude on the typing process by grabbing + # the focus. + tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w, + "help", "noActivates") + except TclError: + pass + self.label = Label(tw, text=self.text, justify=LEFT, + background="#ffffe0", relief=SOLID, borderwidth=1, + font = self.widget['font']) + self.label.pack() + tw.update_idletasks() + tw.lift() # work around bug in Tk 8.5.18+ (issue #24570) + + self.checkhideid = self.widget.bind(CHECKHIDE_VIRTUAL_EVENT_NAME, + self.checkhide_event) + for seq in CHECKHIDE_SEQUENCES: + self.widget.event_add(CHECKHIDE_VIRTUAL_EVENT_NAME, seq) + self.widget.after(CHECKHIDE_TIME, self.checkhide_event) + self.hideid = self.widget.bind(HIDE_VIRTUAL_EVENT_NAME, + self.hide_event) + for seq in HIDE_SEQUENCES: + self.widget.event_add(HIDE_VIRTUAL_EVENT_NAME, seq) + + def checkhide_event(self, event=None): + if not self.tipwindow: + # If the event was triggered by the same event that unbinded + # this function, the function will be called nevertheless, + # so do nothing in this case. + return + curline, curcol = map(int, self.widget.index("insert").split('.')) + if curline < self.parenline or \ + (curline == self.parenline and curcol <= self.parencol) or \ + self.widget.compare("insert", ">", MARK_RIGHT): + self.hidetip() + else: + self.position_window() + if self.checkhide_after_id is not None: + self.widget.after_cancel(self.checkhide_after_id) + self.checkhide_after_id = \ + self.widget.after(CHECKHIDE_TIME, self.checkhide_event) + + def hide_event(self, event): + if not self.tipwindow: + # See the explanation in checkhide_event. + return + self.hidetip() + + def hidetip(self): + if not self.tipwindow: + return + + for seq in CHECKHIDE_SEQUENCES: + self.widget.event_delete(CHECKHIDE_VIRTUAL_EVENT_NAME, seq) + self.widget.unbind(CHECKHIDE_VIRTUAL_EVENT_NAME, self.checkhideid) + self.checkhideid = None + for seq in HIDE_SEQUENCES: + self.widget.event_delete(HIDE_VIRTUAL_EVENT_NAME, seq) + self.widget.unbind(HIDE_VIRTUAL_EVENT_NAME, self.hideid) + self.hideid = None + + self.label.destroy() + self.label = None + self.tipwindow.destroy() + self.tipwindow = None + + self.widget.mark_unset(MARK_RIGHT) + self.parenline = self.parencol = self.lastline = None + + def is_active(self): + return bool(self.tipwindow) + + +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__': + from idlelib.idle_test.htest import run + run(_calltip_window) diff --git a/Lib/idlelib/CallTips.py b/Lib/idlelib/CallTips.py new file mode 100644 index 0000000..3db2636 --- /dev/null +++ b/Lib/idlelib/CallTips.py @@ -0,0 +1,219 @@ +"""CallTips.py - An IDLE Extension to Jog Your Memory + +Call Tips are floating windows which display function, class, and method +parameter and docstring information when you type an opening parenthesis, and +which disappear when you type a closing parenthesis. + +""" +import __main__ +import re +import sys +import textwrap +import types + +from idlelib import CallTipWindow +from idlelib.HyperParser import HyperParser + + +class CallTips: + + menudefs = [ + ('edit', [ + ("Show call tip", "<<force-open-calltip>>"), + ]) + ] + + def __init__(self, editwin=None): + if editwin is None: # subprocess and test + self.editwin = None + return + self.editwin = editwin + self.text = editwin.text + self.calltip = None + self._make_calltip_window = self._make_tk_calltip_window + + def close(self): + self._make_calltip_window = None + + def _make_tk_calltip_window(self): + # See __init__ for usage + return CallTipWindow.CallTip(self.text) + + def _remove_calltip_window(self, event=None): + if self.calltip: + self.calltip.hidetip() + self.calltip = None + + def force_open_calltip_event(self, event): + """Happens when the user really wants to open a CallTip, even if a + function call is needed. + """ + self.open_calltip(True) + + def try_open_calltip_event(self, event): + """Happens when it would be nice to open a CallTip, but not really + necessary, for example after an opening bracket, so function calls + won't be made. + """ + self.open_calltip(False) + + def refresh_calltip_event(self, event): + """If there is already a calltip window, check if it is still needed, + and if so, reload it. + """ + if self.calltip and self.calltip.is_active(): + self.open_calltip(False) + + def open_calltip(self, evalfuncs): + self._remove_calltip_window() + + hp = HyperParser(self.editwin, "insert") + sur_paren = hp.get_surrounding_brackets('(') + if not sur_paren: + return + hp.set_index(sur_paren[0]) + expression = hp.get_expression() + if not expression or (not evalfuncs and expression.find('(') != -1): + return + arg_text = self.fetch_tip(expression) + if not arg_text: + return + self.calltip = self._make_calltip_window() + self.calltip.showtip(arg_text, sur_paren[0], sur_paren[1]) + + def fetch_tip(self, expression): + """Return the argument list and docstring of a function or class + + If there is a Python subprocess, get the calltip there. Otherwise, + either fetch_tip() is running in the subprocess itself or it was called + in an IDLE EditorWindow before any script had been run. + + The subprocess environment is that of the most recently run script. If + two unrelated modules are being edited some calltips in the current + module may be inoperative if the module was not the last to run. + + To find methods, fetch_tip must be fed a fully qualified name. + + """ + try: + rpcclt = self.editwin.flist.pyshell.interp.rpcclt + except AttributeError: + rpcclt = None + if rpcclt: + return rpcclt.remotecall("exec", "get_the_calltip", + (expression,), {}) + else: + entity = self.get_entity(expression) + return get_arg_text(entity) + + def get_entity(self, expression): + """Return the object corresponding to expression evaluated + in a namespace spanning sys.modules and __main.dict__. + """ + if expression: + namespace = sys.modules.copy() + namespace.update(__main__.__dict__) + try: + return eval(expression, namespace) + except BaseException: + # An uncaught exception closes idle, and eval can raise any + # exception, especially if user classes are involved. + return None + +def _find_constructor(class_ob): + # Given a class object, return a function object used for the + # constructor (ie, __init__() ) or None if we can't find one. + try: + return class_ob.__init__.im_func + except AttributeError: + for base in class_ob.__bases__: + rc = _find_constructor(base) + if rc is not None: return rc + return None + +# The following are used in get_arg_text +_MAX_COLS = 85 +_MAX_LINES = 5 # enough for bytes +_INDENT = ' '*4 # for wrapped signatures + +def get_arg_text(ob): + '''Return a string describing the signature of a callable object, or ''. + + For Python-coded functions and methods, the first line is introspected. + Delete 'self' parameter for classes (.__init__) and bound methods. + The next lines are the first lines of the doc string up to the first + empty line or _MAX_LINES. For builtins, this typically includes + the arguments in addition to the return value. + ''' + argspec = "" + try: + ob_call = ob.__call__ + except BaseException: + if type(ob) is types.ClassType: # old-style + ob_call = ob + else: + return argspec + + arg_offset = 0 + if type(ob) in (types.ClassType, types.TypeType): + # Look for the first __init__ in the class chain with .im_func. + # Slot wrappers (builtins, classes defined in funcs) do not. + fob = _find_constructor(ob) + if fob is None: + fob = lambda: None + else: + arg_offset = 1 + elif type(ob) == types.MethodType: + # bit of a hack for methods - turn it into a function + # and drop the "self" param for bound methods + fob = ob.im_func + if ob.im_self is not None: + arg_offset = 1 + elif type(ob_call) == types.MethodType: + # a callable class instance + fob = ob_call.im_func + arg_offset = 1 + else: + fob = ob + # Try to build one for Python defined functions + if type(fob) in [types.FunctionType, types.LambdaType]: + argcount = fob.func_code.co_argcount + real_args = fob.func_code.co_varnames[arg_offset:argcount] + defaults = fob.func_defaults or [] + defaults = list(map(lambda name: "=%s" % repr(name), defaults)) + defaults = [""] * (len(real_args) - len(defaults)) + defaults + items = map(lambda arg, dflt: arg + dflt, real_args, defaults) + for flag, pre, name in ((0x4, '*', 'args'), (0x8, '**', 'kwargs')): + if fob.func_code.co_flags & flag: + pre_name = pre + name + if name not in real_args: + items.append(pre_name) + else: + i = 1 + while ((name+'%s') % i) in real_args: + i += 1 + items.append((pre_name+'%s') % i) + argspec = ", ".join(items) + argspec = "(%s)" % re.sub("(?<!\d)\.\d+", "<tuple>", argspec) + + lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT) + if len(argspec) > _MAX_COLS else [argspec] if argspec else []) + + if isinstance(ob_call, types.MethodType): + doc = ob_call.__doc__ + else: + doc = getattr(ob, "__doc__", "") + if doc: + for line in doc.split('\n', _MAX_LINES)[:_MAX_LINES]: + line = line.strip() + if not line: + break + if len(line) > _MAX_COLS: + line = line[: _MAX_COLS - 3] + '...' + lines.append(line) + argspec = '\n'.join(lines) + return argspec + +if __name__ == '__main__': + from unittest import main + main('idlelib.idle_test.test_calltips', verbosity=2) diff --git a/Lib/idlelib/ChangeLog b/Lib/idlelib/ChangeLog index d7d7e1e..968a344 100644 --- a/Lib/idlelib/ChangeLog +++ b/Lib/idlelib/ChangeLog @@ -20,16 +20,16 @@ IDLEfork ChangeLog 2001-07-19 14:49 elguavas * ChangeLog, EditorWindow.py, INSTALLATION, NEWS.txt, README.txt, - TODO.txt, idlever.py: + TODO.txt, idlever.py: minor tidy-ups ready for 0.8.1 alpha tarball release 2001-07-17 15:12 kbk * INSTALLATION, setup.py: INSTALLATION: Remove the coexist.patch instructions - + **************** setup.py: - + Remove the idles script, add some words on IDLE Fork to the long_description, and clean up some line spacing. @@ -42,30 +42,30 @@ IDLEfork ChangeLog * PyShell.py, idle, idles: Implement idle command interface as suggested by GvR [idle-dev] 16 July **************** PyShell: Added functionality: - + usage: idle.py [-c command] [-d] [-i] [-r script] [-s] [-t title] [arg] ... - + idle file(s) (without options) edit the file(s) - + -c cmd run the command in a shell -d enable the debugger -i open an interactive shell -i file(s) open a shell and also an editor window for each file -r script run a file as a script in a shell -s run $IDLESTARTUP or $PYTHONSTARTUP before anything else -t title set title of shell window - + Remaining arguments are applied to the command (-c) or script (-r). - + ****************** idles: Removed the idles script, not needed - + ****************** idle: Removed the IdleConf references, not required anymore 2001-07-16 17:08 kbk * INSTALLATION, coexist.patch: Added installation instructions. - + Added a patch which modifies idlefork so that it can co-exist with "official" IDLE in the site-packages directory. This patch is not necessary if only idlefork IDLE is installed. See INSTALLATION for @@ -74,7 +74,7 @@ IDLEfork ChangeLog 2001-07-16 15:50 kbk * idles: Add a script "idles" which opens a Python Shell window. - + The default behaviour of idlefork idle is to open an editor window instead of a shell. Complex expressions may be run in a fresh environment by selecting "run". There are times, however, when a @@ -90,7 +90,7 @@ IDLEfork ChangeLog * PyShell.py, setup.py: Add a script "idles" which opens a Python Shell window. - + The default behaviour of idlefork idle is to open an editor window instead of a shell. Complex expressions may be run in a fresh environment by selecting "run". There are times, however, when a @@ -110,13 +110,13 @@ IDLEfork ChangeLog * setup.py: Installing Idle to site-packages via Distutils does not copy the Idle help.txt file. - + Ref SF Python Patch 422471 2001-07-14 15:26 kbk * keydefs.py: py-cvs-2001_07_13 (Rev 1.3) merge - + "Make copy, cut and paste events case insensitive. Reported by Patrick K. O'Brien on idle-dev. (Should other bindings follow suit?)" --GvR @@ -124,7 +124,7 @@ IDLEfork ChangeLog 2001-07-14 15:21 kbk * idle.py: py-cvs-2001_07_13 (Rev 1.4) merge - + "Move the action of loading the configuration to the IdleConf module rather than the idle.py script. This has advantages and disadvantages; the biggest advantage being that we can more easily @@ -133,21 +133,21 @@ IDLEfork ChangeLog 2001-07-14 15:18 kbk * extend.txt: py-cvs-2001_07_13 (Rev 1.4) merge - + "Quick update to the extension mechanism (extend.py is gone, long live config.txt)" --GvR 2001-07-14 15:15 kbk * StackViewer.py: py-cvs-2001_07_13 (Rev 1.16) merge - + "Refactored, with some future plans in mind. This now uses the new gotofileline() method defined in FileList.py" --GvR 2001-07-14 15:10 kbk * PyShell.py: py-cvs-2001_07_13 (Rev 1.34) merge - + "Amazing. A very subtle change in policy in descr-branch actually found a bug here. Here's the deal: Class PyShell derives from class OutputWindow. Method PyShell.close() wants to invoke its @@ -166,19 +166,19 @@ IDLEfork ChangeLog 2001-07-14 14:59 kbk * PyParse.py: py-cvs-2001_07_13 (Rel 1.9) merge - + "Taught IDLE's autoident parser that "yield" is a keyword that begins a stmt. Along w/ the preceding change to keyword.py, making all this work w/ a future-stmt just looks harder and harder." --tim_one - - (From Rel 1.8: "Hack to make this still work with Python 1.5.2. + + (From Rel 1.8: "Hack to make this still work with Python 1.5.2. ;-( " --fdrake) 2001-07-14 14:51 kbk * IdleConf.py: py-cvs-2001_07_13 (Rel 1.7) merge - + "Move the action of loading the configuration to the IdleConf module rather than the idle.py script. This has advantages and disadvantages; the biggest advantage being that we can more easily @@ -187,13 +187,13 @@ IDLEfork ChangeLog 2001-07-14 14:45 kbk * FileList.py: py-cvs-2000_07_13 (Rev 1.9) merge - + "Delete goodname() method, which is unused. Add gotofileline(), a convenience method which I intend to use in a variant. Rename test() to _test()." --GvR - + This was an interesting merge. The join completely missed removing - goodname(), which was adjacent, but outside of, a small conflict. + goodname(), which was adjacent, but outside of, a small conflict. I only caught it by comparing the 1.1.3.2/1.1.3.3 diff. CVS ain't infallible. @@ -245,13 +245,13 @@ IDLEfork ChangeLog 2001-07-14 10:13 kbk * PyShell.py: cvs-py-rel2_1 (Rev 1.29 - 1.33) merge - + Merged the following py-cvs revs without conflict: 1.29 Reduce copyright text output at startup 1.30 Delay setting sys.args until Tkinter is fully initialized 1.31 Whitespace normalization 1.32 Turn syntax warning into error when interactive 1.33 Fix warning initialization bug - + Note that module is extensively modified wrt py-cvs 2001-07-14 06:33 kbk @@ -317,14 +317,14 @@ IDLEfork ChangeLog 2001-07-13 13:35 kbk * EditorWindow.py: py-cvs-rel2_1 (Rev 1.33 - 1.37) merge - + VP IDLE version depended on VP's ExecBinding.py and spawn.py to get the path to the Windows Doc directory (relative to python.exe). Removed this conflicting code in favor of py-cvs updates which on Windows use a hard coded path relative to the location of this module. py-cvs updates include support for webbrowser.py. Module still has BrowserControl.py for 1.5.2 support. - + At this point, the differences wrt py-cvs relate to menu functionality. @@ -516,12 +516,12 @@ IDLEfork ChangeLog 2000-08-15 22:51 nowonder - * IDLEFORK.html: + * IDLEFORK.html: corrected email address 2000-08-15 22:47 nowonder - * IDLEFORK.html: + * IDLEFORK.html: added .html file for http://idlefork.sourceforge.net 2000-08-15 11:13 dscherer @@ -1194,7 +1194,7 @@ Wed Mar 10 05:18:02 1999 Guido van Rossum <guido@cnri.reston.va.us> ====================================================================== Python release 1.5.2b2, IDLE version 0.3 ====================================================================== - + Wed Feb 17 22:47:41 1999 Guido van Rossum <guido@cnri.reston.va.us> * NEWS.txt: News in 0.3. @@ -1330,7 +1330,7 @@ Sat Jan 9 22:01:33 1999 Guido van Rossum <guido@cnri.reston.va.us> ====================================================================== Python release 1.5.2b1, IDLE version 0.2 ====================================================================== - + Fri Jan 8 17:26:02 1999 Guido van Rossum <guido@cnri.reston.va.us> * README.txt, NEWS.txt: What's new in this release. diff --git a/Lib/idlelib/ClassBrowser.py b/Lib/idlelib/ClassBrowser.py new file mode 100644 index 0000000..d09c52f --- /dev/null +++ b/Lib/idlelib/ClassBrowser.py @@ -0,0 +1,236 @@ +"""Class browser. + +XXX TO DO: + +- reparse when source changed (maybe just a button would be OK?) + (or recheck on window popup) +- add popup menu with more options (e.g. doc strings, base classes, imports) +- show function argument list? (have to do pattern matching on source) +- should the classes and methods lists also be in the module's menu bar? +- add base classes to class browser tree +""" + +import os +import sys +import pyclbr + +from idlelib import PyShell +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, _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): + self.top.destroy() + self.node.destroy() + + def init(self, flist): + self.flist = flist + # reset pyclbr + pyclbr._modules.clear() + # create top + 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.CurrentTheme() + background = idleConf.GetHighlight(theme, 'normal')['background'] + sc = ScrolledCanvas(top, bg=background, highlightthickness=0, takefocus=1) + sc.frame.pack(expand=1, fill="both") + item = self.rootnode() + self.node = node = TreeNode(sc.canvas, None, item) + node.update() + node.expand() + + def settitle(self): + self.top.wm_title("Class Browser - " + self.name) + self.top.wm_iconname("Class Browser") + + def rootnode(self): + return ModuleBrowserTreeItem(self.file) + +class ModuleBrowserTreeItem(TreeItem): + + def __init__(self, file): + self.file = file + + def GetText(self): + return os.path.basename(self.file) + + def GetIconName(self): + return "python" + + def GetSubList(self): + sublist = [] + for name in self.listclasses(): + item = ClassBrowserTreeItem(name, self.classes, self.file) + sublist.append(item) + return sublist + + def OnDoubleClick(self): + if os.path.normcase(self.file[-3:]) != ".py": + return + if not os.path.exists(self.file): + return + PyShell.flist.open(self.file) + + def IsExpandable(self): + return os.path.normcase(self.file[-3:]) == ".py" + + def listclasses(self): + dir, file = os.path.split(self.file) + name, ext = os.path.splitext(file) + if os.path.normcase(ext) != ".py": + return [] + try: + dict = pyclbr.readmodule_ex(name, [dir] + sys.path) + except ImportError: + return [] + items = [] + self.classes = {} + for key, cl in dict.items(): + if cl.module == name: + s = key + if hasattr(cl, 'super') and cl.super: + supers = [] + for sup in cl.super: + if type(sup) is type(''): + sname = sup + else: + sname = sup.name + if sup.module != cl.module: + sname = "%s.%s" % (sup.module, sname) + supers.append(sname) + s = s + "(%s)" % ", ".join(supers) + items.append((cl.lineno, s)) + self.classes[s] = cl + items.sort() + list = [] + for item, s in items: + list.append(s) + return list + +class ClassBrowserTreeItem(TreeItem): + + def __init__(self, name, classes, file): + self.name = name + self.classes = classes + self.file = file + try: + self.cl = self.classes[self.name] + except (IndexError, KeyError): + self.cl = None + self.isfunction = isinstance(self.cl, pyclbr.Function) + + def GetText(self): + if self.isfunction: + return "def " + self.name + "(...)" + else: + return "class " + self.name + + def GetIconName(self): + if self.isfunction: + return "python" + else: + return "folder" + + def IsExpandable(self): + if self.cl: + try: + return not not self.cl.methods + except AttributeError: + return False + + def GetSubList(self): + if not self.cl: + return [] + sublist = [] + for name in self.listmethods(): + item = MethodBrowserTreeItem(name, self.cl, self.file) + sublist.append(item) + return sublist + + def OnDoubleClick(self): + if not os.path.exists(self.file): + return + edit = file_open(self.file) + if hasattr(self.cl, 'lineno'): + lineno = self.cl.lineno + edit.gotoline(lineno) + + def listmethods(self): + if not self.cl: + return [] + items = [] + for name, lineno in self.cl.methods.items(): + items.append((lineno, name)) + items.sort() + list = [] + for item, name in items: + list.append(name) + return list + +class MethodBrowserTreeItem(TreeItem): + + def __init__(self, name, cl, file): + self.name = name + self.cl = cl + self.file = file + + def GetText(self): + return "def " + self.name + "(...)" + + def GetIconName(self): + return "python" # XXX + + def IsExpandable(self): + return 0 + + def OnDoubleClick(self): + if not os.path.exists(self.file): + return + edit = file_open(self.file) + edit.gotoline(self.cl.methods[self.name]) + +def _class_browser(parent): #Wrapper for htest + try: + file = __file__ + except NameError: + file = sys.argv[0] + if sys.argv[1:]: + file = sys.argv[1] + else: + file = sys.argv[0] + dir, file = os.path.split(file) + name = os.path.splitext(file)[0] + flist = PyShell.PyShellFileList(parent) + global file_open + file_open = flist.open + ClassBrowser(flist, name, [dir], _htest=True) + +if __name__ == "__main__": + from idlelib.idle_test.htest import run + run(_class_browser) diff --git a/Lib/idlelib/CodeContext.py b/Lib/idlelib/CodeContext.py new file mode 100644 index 0000000..bb0cc9c --- /dev/null +++ b/Lib/idlelib/CodeContext.py @@ -0,0 +1,176 @@ +"""CodeContext - Extension to display the block context above the edit window + +Once code has scrolled off the top of a window, it can be difficult to +determine which block you are in. This extension implements a pane at the top +of each IDLE edit window which provides block structure hints. These hints are +the lines which contain the block opening keywords, e.g. 'if', for the +enclosing block. The number of hint lines is determined by the numlines +variable in the CodeContext section of config-extensions.def. Lines which do +not open blocks are not shown in the context hints pane. + +""" +import Tkinter +from Tkconstants import TOP, LEFT, X, W, SUNKEN +import re +from sys import maxint as INFINITY +from idlelib.configHandler import idleConf + +BLOCKOPENERS = {"class", "def", "elif", "else", "except", "finally", "for", + "if", "try", "while", "with"} +UPDATEINTERVAL = 100 # millisec +FONTUPDATEINTERVAL = 1000 # millisec + +getspacesfirstword =\ + lambda s, c=re.compile(r"^(\s*)(\w*)"): c.match(s).groups() + +class CodeContext: + menudefs = [('options', [('!Code Conte_xt', '<<toggle-code-context>>')])] + context_depth = idleConf.GetOption("extensions", "CodeContext", + "numlines", type="int", default=3) + bgcolor = idleConf.GetOption("extensions", "CodeContext", + "bgcolor", type="str", default="LightGray") + fgcolor = idleConf.GetOption("extensions", "CodeContext", + "fgcolor", type="str", default="Black") + def __init__(self, editwin): + self.editwin = editwin + self.text = editwin.text + self.textfont = self.text["font"] + self.label = None + # self.info is a list of (line number, indent level, line text, block + # keyword) tuples providing the block structure associated with + # self.topvisible (the linenumber of the line displayed at the top of + # the edit window). self.info[0] is initialized as a 'dummy' line which + # starts the toplevel 'block' of the module. + self.info = [(0, -1, "", False)] + self.topvisible = 1 + visible = idleConf.GetOption("extensions", "CodeContext", + "visible", type="bool", default=False) + if visible: + self.toggle_code_context_event() + self.editwin.setvar('<<toggle-code-context>>', True) + # Start two update cycles, one for context lines, one for font changes. + self.text.after(UPDATEINTERVAL, self.timer_event) + self.text.after(FONTUPDATEINTERVAL, self.font_timer_event) + + def toggle_code_context_event(self, event=None): + if not self.label: + # Calculate the border width and horizontal padding required to + # align the context with the text in the main Text widget. + # + # All values are passed through int(str(<value>)), since some + # values may be pixel objects, which can't simply be added to ints. + widgets = self.editwin.text, self.editwin.text_frame + # Calculate the required vertical padding + padx = 0 + for widget in widgets: + padx += int(str( widget.pack_info()['padx'] )) + padx += int(str( widget.cget('padx') )) + # Calculate the required border width + border = 0 + for widget in widgets: + border += int(str( widget.cget('border') )) + self.label = Tkinter.Label(self.editwin.top, + text="\n" * (self.context_depth - 1), + anchor=W, justify=LEFT, + font=self.textfont, + bg=self.bgcolor, fg=self.fgcolor, + width=1, #don't request more than we get + padx=padx, border=border, + relief=SUNKEN) + # Pack the label widget before and above the text_frame widget, + # thus ensuring that it will appear directly above text_frame + self.label.pack(side=TOP, fill=X, expand=False, + before=self.editwin.text_frame) + else: + self.label.destroy() + self.label = None + idleConf.SetOption("extensions", "CodeContext", "visible", + str(self.label is not None)) + idleConf.SaveUserCfgFiles() + + def get_line_info(self, linenum): + """Get the line indent value, text, and any block start keyword + + If the line does not start a block, the keyword value is False. + The indentation of empty lines (or comment lines) is INFINITY. + + """ + text = self.text.get("%d.0" % linenum, "%d.end" % linenum) + spaces, firstword = getspacesfirstword(text) + opener = firstword in BLOCKOPENERS and firstword + if len(text) == len(spaces) or text[len(spaces)] == '#': + indent = INFINITY + else: + indent = len(spaces) + return indent, text, opener + + def get_context(self, new_topvisible, stopline=1, stopindent=0): + """Get context lines, starting at new_topvisible and working backwards. + + Stop when stopline or stopindent is reached. Return a tuple of context + data and the indent level at the top of the region inspected. + + """ + assert stopline > 0 + lines = [] + # The indentation level we are currently in: + lastindent = INFINITY + # For a line to be interesting, it must begin with a block opening + # keyword, and have less indentation than lastindent. + for linenum in xrange(new_topvisible, stopline-1, -1): + indent, text, opener = self.get_line_info(linenum) + if indent < lastindent: + lastindent = indent + if opener in ("else", "elif"): + # We also show the if statement + lastindent += 1 + if opener and linenum < new_topvisible and indent >= stopindent: + lines.append((linenum, indent, text, opener)) + if lastindent <= stopindent: + break + lines.reverse() + return lines, lastindent + + def update_code_context(self): + """Update context information and lines visible in the context pane. + + """ + new_topvisible = int(self.text.index("@0,0").split('.')[0]) + if self.topvisible == new_topvisible: # haven't scrolled + return + if self.topvisible < new_topvisible: # scroll down + lines, lastindent = self.get_context(new_topvisible, + self.topvisible) + # retain only context info applicable to the region + # between topvisible and new_topvisible: + while self.info[-1][1] >= lastindent: + del self.info[-1] + elif self.topvisible > new_topvisible: # scroll up + stopindent = self.info[-1][1] + 1 + # retain only context info associated + # with lines above new_topvisible: + while self.info[-1][0] >= new_topvisible: + stopindent = self.info[-1][1] + del self.info[-1] + lines, lastindent = self.get_context(new_topvisible, + self.info[-1][0]+1, + stopindent) + self.info.extend(lines) + self.topvisible = new_topvisible + # empty lines in context pane: + context_strings = [""] * max(0, self.context_depth - len(self.info)) + # followed by the context hint lines: + context_strings += [x[2] for x in self.info[-self.context_depth:]] + self.label["text"] = '\n'.join(context_strings) + + def timer_event(self): + if self.label: + self.update_code_context() + self.text.after(UPDATEINTERVAL, self.timer_event) + + def font_timer_event(self): + newtextfont = self.text["font"] + if self.label and newtextfont != self.textfont: + self.textfont = newtextfont + self.label["font"] = self.textfont + self.text.after(FONTUPDATEINTERVAL, self.font_timer_event) diff --git a/Lib/idlelib/colorizer.py b/Lib/idlelib/ColorDelegator.py index db1266f..fec2670 100644 --- a/Lib/idlelib/colorizer.py +++ b/Lib/idlelib/ColorDelegator.py @@ -1,10 +1,9 @@ -import builtins -import keyword -import re import time - -from idlelib.config import idleConf -from idlelib.delegator import Delegator +import re +import keyword +import __builtin__ +from idlelib.Delegator import Delegator +from idlelib.configHandler import idleConf DEBUG = False @@ -14,12 +13,16 @@ def any(name, alternates): def make_pat(): kw = r"\b" + any("KEYWORD", keyword.kwlist) + r"\b" - builtinlist = [str(name) for name in dir(builtins) - if not name.startswith('_') and \ - name not in keyword.kwlist] + builtinlist = [str(name) for name in dir(__builtin__) + if not name.startswith('_')] + # We don't know whether "print" is a function or a keyword, + # so we always treat is as a keyword (the most common case). + builtinlist.remove('print') + # self.file = file("file") : + # 1st 'file' colorized normal, 2nd as builtin, 3rd as string builtin = r"([^.'\"\\#]\b|^)" + any("BUILTIN", builtinlist) + r"\b" comment = any("COMMENT", [r"#[^\n]*"]) - stringprefix = r"(?i:r|u|f|fr|rf|b|br|rb)?" + stringprefix = r"(\br|u|ur|R|U|UR|Ur|uR|b|B|br|Br|bR|BR)?" sqstring = stringprefix + r"'[^'\\\n]*(\\.[^'\\\n]*)*'?" dqstring = stringprefix + r'"[^"\\\n]*(\\.[^"\\\n]*)*"?' sq3string = stringprefix + r"'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?" @@ -31,68 +34,15 @@ def make_pat(): prog = re.compile(make_pat(), re.S) idprog = re.compile(r"\s+(\w+)", re.S) -def color_config(text): - """Set color options of Text widget. - - If ColorDelegator is used, this should be called first. - """ - # Called from htest, TextFrame, Editor, and Turtledemo. - # Not automatic because ColorDelegator does not know 'text'. - theme = idleConf.CurrentTheme() - normal_colors = idleConf.GetHighlight(theme, 'normal') - cursor_color = idleConf.GetHighlight(theme, 'cursor')['foreground'] - select_colors = idleConf.GetHighlight(theme, 'hilite') - text.config( - foreground=normal_colors['foreground'], - background=normal_colors['background'], - insertbackground=cursor_color, - selectforeground=select_colors['foreground'], - selectbackground=select_colors['background'], - inactiveselectbackground=select_colors['background'], # new in 8.5 - ) - - class ColorDelegator(Delegator): - """Delegator for syntax highlighting (text coloring). - - Instance variables: - delegate: Delegator below this one in the stack, meaning the - one this one delegates to. - - Used to track state: - after_id: Identifier for scheduled after event, which is a - timer for colorizing the text. - allow_colorizing: Boolean toggle for applying colorizing. - colorizing: Boolean flag when colorizing is in process. - stop_colorizing: Boolean flag to end an active colorizing - process. - """ def __init__(self): Delegator.__init__(self) - self.init_state() self.prog = prog self.idprog = idprog self.LoadTagDefs() - def init_state(self): - "Initialize variables that track colorizing state." - self.after_id = None - self.allow_colorizing = True - self.stop_colorizing = False - self.colorizing = False - def setdelegate(self, delegate): - """Set the delegate for this instance. - - A delegate is an instance of a Delegator class and each - delegate points to the next delegator in the stack. This - allows multiple delegators to be chained together for a - widget. The bottom delegate for a colorizer is a Text - widget. - - If there is a delegate, also start the colorizing process. - """ if self.delegate is not None: self.unbind("<<toggle-auto-coloring>>") Delegator.setdelegate(self, delegate) @@ -101,18 +51,17 @@ class ColorDelegator(Delegator): self.bind("<<toggle-auto-coloring>>", self.toggle_colorize_event) self.notify_range("1.0", "end") else: - # No delegate - stop any colorizing. + # No delegate - stop any colorizing self.stop_colorizing = True self.allow_colorizing = False def config_colors(self): - "Configure text widget tags with colors from tagdefs." for tag, cnf in self.tagdefs.items(): - self.tag_configure(tag, **cnf) + if cnf: + self.tag_configure(tag, **cnf) self.tag_raise('sel') def LoadTagDefs(self): - "Create dictionary of tag names to text colors." theme = idleConf.CurrentTheme() self.tagdefs = { "COMMENT": idleConf.GetHighlight(theme, "comment"), @@ -127,103 +76,97 @@ class ColorDelegator(Delegator): "hit": idleConf.GetHighlight(theme, "hit"), } - if DEBUG: print('tagdefs',self.tagdefs) + if DEBUG: print 'tagdefs',self.tagdefs def insert(self, index, chars, tags=None): - "Insert chars into widget at index and mark for colorizing." index = self.index(index) self.delegate.insert(index, chars, tags) self.notify_range(index, index + "+%dc" % len(chars)) def delete(self, index1, index2=None): - "Delete chars between indexes and mark for colorizing." index1 = self.index(index1) self.delegate.delete(index1, index2) self.notify_range(index1) + after_id = None + allow_colorizing = True + colorizing = False + def notify_range(self, index1, index2=None): - "Mark text changes for processing and restart colorizing, if active." self.tag_add("TODO", index1, index2) if self.after_id: - if DEBUG: print("colorizing already scheduled") + if DEBUG: print "colorizing already scheduled" return if self.colorizing: self.stop_colorizing = True - if DEBUG: print("stop colorizing") + if DEBUG: print "stop colorizing" if self.allow_colorizing: - if DEBUG: print("schedule colorizing") + if DEBUG: print "schedule colorizing" self.after_id = self.after(1, self.recolorize) - return - def close(self): + close_when_done = None # Window to be closed when done colorizing + + def close(self, close_when_done=None): if self.after_id: after_id = self.after_id self.after_id = None - if DEBUG: print("cancel scheduled recolorizer") + if DEBUG: print "cancel scheduled recolorizer" self.after_cancel(after_id) self.allow_colorizing = False self.stop_colorizing = True + if close_when_done: + if not self.colorizing: + close_when_done.destroy() + else: + self.close_when_done = close_when_done - def toggle_colorize_event(self, event=None): - """Toggle colorizing on and off. - - When toggling off, if colorizing is scheduled or is in - process, it will be cancelled and/or stopped. - - When toggling on, colorizing will be scheduled. - """ + def toggle_colorize_event(self, event): if self.after_id: after_id = self.after_id self.after_id = None - if DEBUG: print("cancel scheduled recolorizer") + if DEBUG: print "cancel scheduled recolorizer" self.after_cancel(after_id) if self.allow_colorizing and self.colorizing: - if DEBUG: print("stop colorizing") + if DEBUG: print "stop colorizing" self.stop_colorizing = True self.allow_colorizing = not self.allow_colorizing if self.allow_colorizing and not self.colorizing: self.after_id = self.after(1, self.recolorize) if DEBUG: - print("auto colorizing turned",\ - self.allow_colorizing and "on" or "off") + print "auto colorizing turned",\ + self.allow_colorizing and "on" or "off" return "break" def recolorize(self): - """Timer event (every 1ms) to colorize text. - - Colorizing is only attempted when the text widget exists, - when colorizing is toggled on, and when the colorizing - process is not already running. - - After colorizing is complete, some cleanup is done to - make sure that all the text has been colorized. - """ self.after_id = None if not self.delegate: - if DEBUG: print("no delegate") + if DEBUG: print "no delegate" return if not self.allow_colorizing: - if DEBUG: print("auto colorizing is off") + if DEBUG: print "auto colorizing is off" return if self.colorizing: - if DEBUG: print("already colorizing") + if DEBUG: print "already colorizing" return try: self.stop_colorizing = False self.colorizing = True - if DEBUG: print("colorizing...") - t0 = time.perf_counter() + if DEBUG: print "colorizing..." + t0 = time.clock() self.recolorize_main() - t1 = time.perf_counter() - if DEBUG: print("%.3f seconds" % (t1-t0)) + t1 = time.clock() + if DEBUG: print "%.3f seconds" % (t1-t0) finally: self.colorizing = False if self.allow_colorizing and self.tag_nextrange("TODO", "1.0"): - if DEBUG: print("reschedule colorizing") + if DEBUG: print "reschedule colorizing" self.after_id = self.after(1, self.recolorize) + if self.close_when_done: + top = self.close_when_done + self.close_when_done = None + top.destroy() def recolorize_main(self): - "Evaluate text and apply colorizing tags." next = "1.0" while True: item = self.tag_nextrange("TODO", next) @@ -251,7 +194,7 @@ class ColorDelegator(Delegator): ##print head, "get", mark, next, "->", repr(line) if not line: return - for tag in self.tagdefs: + for tag in self.tagdefs.keys(): self.tag_remove(tag, mark, next) chars = chars + line m = self.prog.search(chars) @@ -285,52 +228,31 @@ class ColorDelegator(Delegator): self.tag_add("TODO", next) self.update() if self.stop_colorizing: - if DEBUG: print("colorizing stopped") + if DEBUG: print "colorizing stopped" return def removecolors(self): - "Remove all colorizing tags." - for tag in self.tagdefs: + for tag in self.tagdefs.keys(): self.tag_remove(tag, "1.0", "end") - def _color_delegator(parent): # htest # - from tkinter import Toplevel, Text - from idlelib.percolator import Percolator + from Tkinter import Toplevel, Text + from idlelib.Percolator import Percolator top = Toplevel(parent) top.title("Test ColorDelegator") - x, y = map(int, parent.geometry().split('+')[1:]) - top.geometry("700x250+%d+%d" % (x + 20, y + 175)) - source = ( - "if True: int ('1') # keyword, builtin, string, comment\n" - "elif False: print(0)\n" - "else: float(None)\n" - "if iF + If + IF: 'keyword matching must respect case'\n" - "if'': x or'' # valid string-keyword no-space combinations\n" - "async def f(): await g()\n" - "# All valid prefixes for unicode and byte strings should be colored.\n" - "'x', '''x''', \"x\", \"\"\"x\"\"\"\n" - "r'x', u'x', R'x', U'x', f'x', F'x'\n" - "fr'x', Fr'x', fR'x', FR'x', rf'x', rF'x', Rf'x', RF'x'\n" - "b'x',B'x', br'x',Br'x',bR'x',BR'x', rb'x', rB'x',Rb'x',RB'x'\n" - "# Invalid combinations of legal characters should be half colored.\n" - "ur'x', ru'x', uf'x', fu'x', UR'x', ufr'x', rfu'x', xf'x', fx'x'\n" - ) + 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() - color_config(text) p = Percolator(text) d = ColorDelegator() p.insertfilter(d) - if __name__ == "__main__": - from unittest import main - main('idlelib.idle_test.test_colorizer', verbosity=2, exit=False) - from idlelib.idle_test.htest import run run(_color_delegator) diff --git a/Lib/idlelib/debugger.py b/Lib/idlelib/Debugger.py index ccd03e4..c517065 100644 --- a/Lib/idlelib/debugger.py +++ b/Lib/idlelib/Debugger.py @@ -1,18 +1,15 @@ -import bdb import os - -from tkinter import * -from tkinter.ttk import Frame, Scrollbar - -from idlelib import macosx -from idlelib.scrolledlist import ScrolledList -from idlelib.window import ListedToplevel +import bdb +from Tkinter import * +from idlelib.WindowList import ListedToplevel +from idlelib.ScrolledList import ScrolledList +from idlelib import macosxSupport class Idb(bdb.Bdb): def __init__(self, gui): - self.gui = gui # An instance of Debugger or proxy of remote. + self.gui = gui bdb.Bdb.__init__(self) def user_line(self, frame): @@ -37,10 +34,8 @@ class Idb(bdb.Bdb): return True else: prev_frame = frame.f_back - prev_name = prev_frame.f_code.co_filename - if 'idlelib' in prev_name and 'debugger' in prev_name: - # catch both idlelib/debugger.py and idlelib/debugger_r.py - # on both Posix and Windows + if prev_frame.f_code.co_filename.count('Debugger.py'): + # (that test will catch both Debugger.py and RemoteDebugger.py) return False return self.in_rpc_code(prev_frame) @@ -63,7 +58,7 @@ class Debugger: if idb is None: idb = Idb(self) self.pyshell = pyshell - self.idb = idb # If passed, a proxy of remote instance. + self.idb = idb self.frame = None self.make_gui() self.interacting = 0 @@ -364,7 +359,8 @@ class Debugger: def load_breakpoints(self): "Load PyShellEditorWindow breakpoints into subprocess debugger" - for editwin in self.pyshell.flist.inversedict: + pyshell_edit_windows = self.pyshell.flist.inversedict.keys() + for editwin in pyshell_edit_windows: filename = editwin.io.filename try: for lineno in editwin.breakpoints: @@ -375,7 +371,7 @@ class Debugger: class StackViewer(ScrolledList): def __init__(self, master, flist, gui): - if macosx.isAquaTk(): + if macosxSupport.isAquaTk(): # At least on with the stock AquaTk version on OSX 10.4 you'll # get a shaking GUI that eventually kills IDLE if the width # argument is specified. @@ -400,7 +396,8 @@ class StackViewer(ScrolledList): funcname = code.co_name import linecache sourceline = linecache.getline(filename, lineno) - sourceline = sourceline.strip() + import string + sourceline = string.strip(sourceline) if funcname in ("?", "", None): item = "%s, line %d: %s" % (modname, lineno, sourceline) else: @@ -464,8 +461,8 @@ class NamespaceViewer: height = 20*len(dict) # XXX 20 == observed height of Entry widget self.master = master self.title = title - import reprlib - self.repr = reprlib.Repr() + import repr + self.repr = repr.Repr() self.repr.maxstring = 60 self.repr.maxother = 60 self.frame = frame = Frame(master) @@ -491,27 +488,15 @@ class NamespaceViewer: return subframe = self.subframe frame = self.frame - for c in list(subframe.children.values()): + for c in subframe.children.values(): c.destroy() self.dict = None if not dict: l = Label(subframe, text="None") l.grid(row=0, column=0) else: - #names = sorted(dict) - ### - # Because of (temporary) limitations on the dict_keys type (not yet - # public or pickleable), have the subprocess to send a list of - # keys, not a dict_keys object. sorted() will take a dict_keys - # (no subprocess) or a list. - # - # There is also an obscure bug in sorted(dict) where the - # interpreter gets into a loop requesting non-existing dict[0], - # dict[1], dict[2], etc from the debugger_r.DictProxy. - ### - keys_list = dict.keys() - names = sorted(keys_list) - ### + names = dict.keys() + names.sort() row = 0 for name in names: value = dict[name] @@ -542,9 +527,3 @@ class NamespaceViewer: def close(self): self.frame.destroy() - -if __name__ == "__main__": - from unittest import main - main('idlelib.idle_test.test_debugger', verbosity=2, exit=False) - -# TODO: htest? diff --git a/Lib/idlelib/delegator.py b/Lib/idlelib/Delegator.py index 55c95da..c476516 100644 --- a/Lib/idlelib/delegator.py +++ b/Lib/idlelib/Delegator.py @@ -1,10 +1,10 @@ class Delegator: + # The cache is only used to be able to change delegates! + def __init__(self, delegate=None): self.delegate = delegate self.__cache = set() - # Cache is used to only remove added attributes - # when changing the delegate. def __getattr__(self, name): attr = getattr(self.delegate, name) # May raise AttributeError @@ -13,9 +13,6 @@ class Delegator: return attr def resetcache(self): - "Removes added attributes while leaving original attributes." - # Function is really about resetting delegator dict - # to original state. Cache is just a means for key in self.__cache: try: delattr(self, key) @@ -24,10 +21,5 @@ class Delegator: self.__cache.clear() def setdelegate(self, delegate): - "Reset attributes and change delegate." self.resetcache() self.delegate = delegate - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_delegator', verbosity=2) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/EditorWindow.py index 92dcf57..8a33719 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/EditorWindow.py @@ -1,80 +1,136 @@ -import importlib.abc -import importlib.util +import sys import os import platform import re -import string -import sys -import tokenize -import traceback +import imp +from Tkinter import * +import tkSimpleDialog +import tkMessageBox import webbrowser -from tkinter import * -from tkinter.font import Font -from tkinter.ttk import Scrollbar -import tkinter.simpledialog as tkSimpleDialog -import tkinter.messagebox as tkMessageBox - -from idlelib.config import idleConf -from idlelib import configdialog -from idlelib import grep +from idlelib.MultiCall import MultiCallCreator +from idlelib import WindowList +from idlelib import SearchDialog +from idlelib import GrepDialog +from idlelib import ReplaceDialog +from idlelib import PyParse +from idlelib.configHandler import idleConf +from idlelib import aboutDialog, textView, configDialog +from idlelib import macosxSupport from idlelib import help -from idlelib import help_about -from idlelib import macosx -from idlelib.multicall import MultiCallCreator -from idlelib import pyparse -from idlelib import query -from idlelib import replace -from idlelib import search -from idlelib.tree import wheel_event -from idlelib import window # The default tab setting for a Text widget, in average-width characters. TK_TABWIDTH_DEFAULT = 8 + _py_version = ' (%s)' % platform.python_version() -darwin = sys.platform == 'darwin' 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 release = '%s%s' % (major, minor) - release += '%s' % (micro,) + if micro: + release += '%s' % (micro,) if level == 'candidate': release += 'rc%s' % (serial,) elif level != 'final': release += '%s%s' % (level[0], serial) return release +def _find_module(fullname, path=None): + """Version of imp.find_module() that handles hierarchical module names""" + + file = None + for tgt in fullname.split('.'): + if file is not None: + file.close() # close intermediate files + (file, filename, descr) = imp.find_module(tgt, path) + if descr[2] == imp.PY_SOURCE: + break # find but not load the source file + module = imp.load_module(tgt, file, filename, descr) + try: + path = module.__path__ + except AttributeError: + raise ImportError, 'No source for module ' + module.__name__ + if descr[2] != imp.PY_SOURCE: + # If all of the above fails and didn't raise an exception,fallback + # to a straight import which can find __init__.py in a package. + m = __import__(fullname) + try: + filename = m.__file__ + except AttributeError: + pass + else: + file = None + base, ext = os.path.splitext(filename) + if ext == '.pyc': + ext = '.py' + filename = base + ext + descr = filename, None, imp.PY_SOURCE + return file, filename, descr + + +class HelpDialog(object): + + def __init__(self): + self.parent = None # parent of help window + self.dlg = None # the help window iteself + + def display(self, parent, near=None): + """ Display the help dialog. + + parent - parent widget for the help window + + 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: + self.nearwindow(near) + + def show_dialog(self, parent): + self.parent = parent + fn=os.path.join(os.path.abspath(os.path.dirname(__file__)),'help.txt') + self.dlg = dlg = textView.view_file(parent,'Help',fn, modal=False) + dlg.bind('<Destroy>', self.destroy, '+') + + def nearwindow(self, near): + # Place the help dialog near the window specified by parent. + # Note - this may not reposition the window in Metacity + # if "/apps/metacity/general/disable_workarounds" is enabled + dlg = self.dlg + geom = (near.winfo_rootx() + 10, near.winfo_rooty() + 10) + dlg.withdraw() + dlg.geometry("=+%d+%d" % geom) + dlg.deiconify() + dlg.lift() + + def destroy(self, ev=None): + self.dlg = None + self.parent = None + +helpDialog = HelpDialog() # singleton instance, no longer used + class EditorWindow(object): - from idlelib.percolator import Percolator - from idlelib.colorizer import ColorDelegator, color_config - from idlelib.undo import UndoDelegator - from idlelib.iomenu import IOBinding, encoding - from idlelib import mainmenu - from idlelib.statusbar import MultiStatusBar - from idlelib.autocomplete import AutoComplete - from idlelib.autoexpand import AutoExpand - from idlelib.calltip import Calltip - from idlelib.codecontext import CodeContext - from idlelib.sidebar import LineNumbers - from idlelib.format import FormatParagraph, FormatRegion, Indents, Rstrip - from idlelib.parenmatch import ParenMatch - from idlelib.squeezer import Squeezer - from idlelib.zoomheight import ZoomHeight - - filesystemencoding = sys.getfilesystemencoding() # for file names - help_url = None + from idlelib.Percolator import Percolator + from idlelib.ColorDelegator import ColorDelegator + from idlelib.UndoDelegator import UndoDelegator + from idlelib.IOBinding import IOBinding, filesystemencoding, encoding + from idlelib import Bindings + from Tkinter import Toplevel + from idlelib.MultiStatusBar import MultiStatusBar - allow_code_context = True - allow_line_numbers = True + help_url = None def __init__(self, flist=None, filename=None, key=None, root=None): - # Delay import: runscript imports pyshell imports EditorWindow. - from idlelib.runscript import ScriptBinding - if EditorWindow.help_url is None: - dochome = os.path.join(sys.base_prefix, 'Doc', 'index.html') + dochome = os.path.join(sys.prefix, 'Doc', 'index.html') if sys.platform.count('linux'): # look for html docs in a couple of standard places pyver = 'python-docs-' + '%s.%s.%s' % sys.version_info[:3] @@ -85,13 +141,13 @@ class EditorWindow(object): dochome = os.path.join(basepath, pyver, 'Doc', 'index.html') elif sys.platform[:3] == 'win': - chmfile = os.path.join(sys.base_prefix, 'Doc', + chmfile = os.path.join(sys.prefix, 'Doc', 'Python%s.chm' % _sphinx_version()) if os.path.isfile(chmfile): dochome = chmfile elif sys.platform == 'darwin': # documentation may be stored inside a python framework - dochome = os.path.join(sys.base_prefix, + dochome = os.path.join(sys.prefix, 'Resources/English.lproj/Documentation/index.html') dochome = os.path.normpath(dochome) if os.path.isfile(dochome): @@ -100,39 +156,42 @@ class EditorWindow(object): # Safari requires real file:-URLs EditorWindow.help_url = 'file://' + EditorWindow.help_url else: - EditorWindow.help_url = ("https://docs.python.org/%d.%d/" - % sys.version_info[:2]) + EditorWindow.help_url = "https://docs.python.org/%d.%d/" % sys.version_info[:2] self.flist = flist root = root or flist.root self.root = root + try: + sys.ps1 + except AttributeError: + sys.ps1 = '>>> ' self.menubar = Menu(root) - self.top = top = window.ListedToplevel(root, menu=self.menubar) + self.top = top = WindowList.ListedToplevel(root, menu=self.menubar) if flist: self.tkinter_vars = flist.vars #self.top.instance_dict makes flist.inversedict available to - #configdialog.py so it can access all EditorWindow instances + #configDialog.py so it can access all EditorWindow instances self.top.instance_dict = flist.inversedict else: self.tkinter_vars = {} # keys: Tkinter event names # values: Tkinter variable instances self.top.instance_dict = {} - self.recent_files_path = idleConf.userdir and os.path.join( - idleConf.userdir, 'recent-files.lst') - - self.prompt_last_line = '' # Override in PyShell + self.recent_files_path = os.path.join(idleConf.GetUserCfgDir(), + 'recent-files.lst') self.text_frame = text_frame = Frame(top) self.vbar = vbar = Scrollbar(text_frame, name='vbar') - width = idleConf.GetOption('main', 'EditorWindow', 'width', type='int') + self.width = idleConf.GetOption('main','EditorWindow','width', type='int') text_options = { 'name': 'text', 'padx': 5, 'wrap': 'none', 'highlightthickness': 0, - 'width': width, - 'tabstyle': 'wordprocessor', # new in 8.5 - 'height': idleConf.GetOption( - 'main', 'EditorWindow', 'height', type='int'), - } + 'width': self.width, + 'height': idleConf.GetOption('main', 'EditorWindow', 'height', type='int')} + if TkVersion >= 8.5: + # Starting with tk 8.5 we have to set the new tabstyle option + # to 'wordprocessor' to achieve the same display of tabs as in + # older tk versions. + text_options['tabstyle'] = 'wordprocessor' self.text = text = MultiCallCreator(Text)(text_frame, **text_options) self.top.focused_widget = self.text @@ -141,8 +200,8 @@ class EditorWindow(object): self.top.protocol("WM_DELETE_WINDOW", self.close) self.top.bind("<<close-window>>", self.close_event) - if macosx.isAquaTk(): - # Command-W on editor windows doesn't work without this. + 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 popup context menus there. For two @@ -152,11 +211,6 @@ class EditorWindow(object): else: # Elsewhere, use right-click for popup menus. text.bind("<3>",self.right_menu_event) - - text.bind('<MouseWheel>', wheel_event) - text.bind('<Button-4>', wheel_event) - text.bind('<Button-5>', wheel_event) - text.bind('<Configure>', self.handle_winconfig) text.bind("<<cut>>", self.cut) text.bind("<<copy>>", self.copy) text.bind("<<paste>>", self.paste) @@ -165,7 +219,7 @@ class EditorWindow(object): text.bind("<<python-docs>>", self.python_docs) text.bind("<<about-idle>>", self.about_dialog) text.bind("<<open-config-dialog>>", self.config_dialog) - text.bind("<<open-module>>", self.open_module_event) + text.bind("<<open-module>>", self.open_module) text.bind("<<do-nothing>>", lambda event: "break") text.bind("<<select-all>>", self.select_all) text.bind("<<remove-selection>>", self.remove_selection) @@ -178,17 +232,14 @@ class EditorWindow(object): text.bind("<<smart-backspace>>",self.smart_backspace_event) text.bind("<<newline-and-indent>>",self.newline_and_indent_event) text.bind("<<smart-indent>>",self.smart_indent_event) - self.fregion = fregion = self.FormatRegion(self) - # self.fregion used in smart_indent_event to access indent_region. - text.bind("<<indent-region>>", fregion.indent_region_event) - text.bind("<<dedent-region>>", fregion.dedent_region_event) - text.bind("<<comment-region>>", fregion.comment_region_event) - text.bind("<<uncomment-region>>", fregion.uncomment_region_event) - text.bind("<<tabify-region>>", fregion.tabify_region_event) - text.bind("<<untabify-region>>", fregion.untabify_region_event) - indents = self.Indents(self) - text.bind("<<toggle-tabs>>", indents.toggle_tabs_event) - text.bind("<<change-indentwidth>>", indents.change_indentwidth_event) + text.bind("<<indent-region>>",self.indent_region_event) + text.bind("<<dedent-region>>",self.dedent_region_event) + text.bind("<<comment-region>>",self.comment_region_event) + text.bind("<<uncomment-region>>",self.uncomment_region_event) + text.bind("<<tabify-region>>",self.tabify_region_event) + text.bind("<<untabify-region>>",self.untabify_region_event) + text.bind("<<toggle-tabs>>",self.toggle_tabs_event) + text.bind("<<change-indentwidth>>",self.change_indentwidth_event) text.bind("<Left>", self.move_at_edge_if_selection(0)) text.bind("<Right>", self.move_at_edge_if_selection(1)) text.bind("<<del-word-left>>", self.del_word_left) @@ -201,21 +252,17 @@ class EditorWindow(object): flist.dict[key] = self text.bind("<<open-new-window>>", self.new_callback) text.bind("<<close-all-windows>>", self.flist.close_all_callback) - text.bind("<<open-class-browser>>", self.open_module_browser) + 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() - text_frame.pack(side=LEFT, fill=BOTH, expand=1) - text_frame.rowconfigure(1, weight=1) - text_frame.columnconfigure(1, weight=1) - vbar['command'] = self.handle_yview - vbar.grid(row=1, column=2, sticky=NSEW) + vbar['command'] = text.yview + vbar.pack(side=RIGHT, fill=Y) text['yscrollcommand'] = vbar.set text['font'] = idleConf.GetFont(self.root, 'main', 'EditorWindow') - text.grid(row=1, column=1, sticky=NSEW) + text_frame.pack(side=LEFT, fill=BOTH, expand=1) + text.pack(side=TOP, fill=BOTH, expand=1) text.focus_set() - self.set_width() # usetabs true -> literal tab characters are used by indent and # dedent cmds, possibly mixed with spaces if @@ -226,8 +273,7 @@ class EditorWindow(object): # Although use-spaces=0 can be configured manually in config-main.def, # configuration of tabs v. spaces is not supported in the configuration # dialog. IDLE promotes the preferred Python indentation: use spaces! - usespaces = idleConf.GetOption('main', 'Indent', - 'use-spaces', type='bool') + usespaces = idleConf.GetOption('main', 'Indent', 'use-spaces', type='bool') self.usetabs = not usespaces # tabwidth is the display width of a literal tab character. @@ -242,11 +288,9 @@ class EditorWindow(object): self.indentwidth = self.tabwidth self.set_notabs_indentwidth() - # Store the current value of the insertofftime now so we can restore - # it if needed. - if not hasattr(idleConf, 'blink_off_time'): - idleConf.blink_off_time = self.text['insertofftime'] - self.update_cursor_blink() + # If context_use_ps1 is true, parsing searches back for a ps1 line; + # else searches for a popular (if, def, ...) Python stmt. + self.context_use_ps1 = False # When searching backwards for a reliable place to begin parsing, # first start num_context_lines[0] lines back, then @@ -255,35 +299,40 @@ class EditorWindow(object): # conceivable file). # Making the initial values larger slows things down more often. self.num_context_lines = 50, 500, 5000000 + self.per = per = self.Percolator(text) + self.undo = undo = self.UndoDelegator() per.insertfilter(undo) text.undo_block_start = undo.undo_block_start text.undo_block_stop = undo.undo_block_stop undo.set_saved_change_hook(self.saved_change_hook) + # IOBinding implements file I/O and printing functionality self.io = io = self.IOBinding(self) io.set_filename_change_hook(self.filename_change_hook) - self.good_load = False - self.set_indentation_params(False) + + # Create the recent files submenu + 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) + self.update_recent_files_list() + self.color = None # initialized below in self.ResetColorizer - self.code_context = None # optionally initialized later below - self.line_numbers = None # optionally initialized later below if filename: if os.path.exists(filename) and not os.path.isdir(filename): - if io.loadfile(filename): - self.good_load = True - is_py_src = self.ispythonsource(filename) - self.set_indentation_params(is_py_src) + io.loadfile(filename) else: io.set_filename(filename) - self.good_load = True - self.ResetColorizer() self.saved_change_hook() - self.update_recent_files_list() + + self.set_indentation_params(self.ispythonsource(filename)) + self.load_extensions() - menu = self.menudict.get('window') + + menu = self.menudict.get('windows') if menu: end = menu.index("end") if end is None: @@ -292,78 +341,27 @@ class EditorWindow(object): menu.add_separator() end = end + 1 self.wmenu_end = end - window.register_callback(self.postwindowsmenu) + WindowList.register_callback(self.postwindowsmenu) # Some abstractions so IDLE extensions are cross-IDE self.askyesno = tkMessageBox.askyesno self.askinteger = tkSimpleDialog.askinteger self.showerror = tkMessageBox.showerror - # Add pseudoevents for former extension fixed keys. - # (This probably needs to be done once in the process.) - text.event_add('<<autocomplete>>', '<Key-Tab>') - text.event_add('<<try-open-completions>>', '<KeyRelease-period>', - '<KeyRelease-slash>', '<KeyRelease-backslash>') - text.event_add('<<try-open-calltip>>', '<KeyRelease-parenleft>') - text.event_add('<<refresh-calltip>>', '<KeyRelease-parenright>') - text.event_add('<<paren-closed>>', '<KeyRelease-parenright>', - '<KeyRelease-bracketright>', '<KeyRelease-braceright>') - - # Former extension bindings depends on frame.text being packed - # (called from self.ResetColorizer()). - autocomplete = self.AutoComplete(self) - text.bind("<<autocomplete>>", autocomplete.autocomplete_event) - text.bind("<<try-open-completions>>", - autocomplete.try_open_completions_event) - text.bind("<<force-open-completions>>", - autocomplete.force_open_completions_event) - text.bind("<<expand-word>>", self.AutoExpand(self).expand_word_event) - text.bind("<<format-paragraph>>", - self.FormatParagraph(self).format_paragraph_event) - parenmatch = self.ParenMatch(self) - text.bind("<<flash-paren>>", parenmatch.flash_paren_event) - text.bind("<<paren-closed>>", parenmatch.paren_closed_event) - scriptbinding = ScriptBinding(self) - text.bind("<<check-module>>", scriptbinding.check_module_event) - text.bind("<<run-module>>", scriptbinding.run_module_event) - text.bind("<<run-custom>>", scriptbinding.run_custom_event) - text.bind("<<do-rstrip>>", self.Rstrip(self).do_rstrip) - ctip = self.Calltip(self) - text.bind("<<try-open-calltip>>", ctip.try_open_calltip_event) - #refresh-calltip must come after paren-closed to work right - text.bind("<<refresh-calltip>>", ctip.refresh_calltip_event) - text.bind("<<force-open-calltip>>", ctip.force_open_calltip_event) - text.bind("<<zoom-height>>", self.ZoomHeight(self).zoom_height_event) - if self.allow_code_context: - self.code_context = self.CodeContext(self) - text.bind("<<toggle-code-context>>", - self.code_context.toggle_code_context_event) + def _filename_to_unicode(self, filename): + """convert filename to unicode in order to display it in Tk""" + if isinstance(filename, unicode) or not filename: + return filename else: - self.update_menu_state('options', '*Code Context', 'disabled') - if self.allow_line_numbers: - self.line_numbers = self.LineNumbers(self) - if idleConf.GetOption('main', 'EditorWindow', - 'line-numbers-default', type='bool'): - self.toggle_line_numbers_event() - text.bind("<<toggle-line-numbers>>", self.toggle_line_numbers_event) - else: - self.update_menu_state('options', '*Line Numbers', 'disabled') - - def handle_winconfig(self, event=None): - self.set_width() - - def set_width(self): - text = self.text - inner_padding = sum(map(text.tk.getint, [text.cget('border'), - text.cget('padx')])) - pixel_width = text.winfo_width() - 2 * inner_padding - - # Divide the width of the Text widget by the font width, - # which is taken to be the width of '0' (zero). - # http://www.tcl.tk/man/tcl8.6/TkCmd/text.htm#M21 - zero_char_width = \ - Font(text, font=text.cget('font')).measure('0') - self.width = pixel_width // zero_char_width + try: + return filename.decode(self.filesystemencoding) + except UnicodeDecodeError: + # XXX + try: + return filename.decode(self.encoding) + except UnicodeDecodeError: + # byte-to-byte conversion + return filename.decode('iso8859-1') def new_callback(self, event): dirname, basename = self.io.defaultfilename() @@ -373,7 +371,7 @@ class EditorWindow(object): def home_callback(self, event): if (event.state & 4) != 0 and event.keysym == "Home": # state&4==Control. If <Control-Home>, use the Tk binding. - return None + return if self.text.index("iomark") and \ self.text.compare("iomark", "<=", "insert lineend") and \ self.text.compare("insert linestart", "<=", "iomark"): @@ -381,7 +379,7 @@ class EditorWindow(object): insertpt = int(self.text.index("iomark").split(".")[1]) else: line = self.text.get("insert linestart", "insert lineend") - for insertpt in range(len(line)): + for insertpt in xrange(len(line)): if line[insertpt] not in (' ','\t'): break else: @@ -395,11 +393,9 @@ class EditorWindow(object): self.text.tag_remove("sel", "1.0", "end") else: if not self.text.index("sel.first"): - # there was no previous selection - self.text.mark_set("my_anchor", "insert") + self.text.mark_set("my_anchor", "insert") # there was no previous selection else: - if self.text.compare(self.text.index("sel.first"), "<", - self.text.index("insert")): + if self.text.compare(self.text.index("sel.first"), "<", self.text.index("insert")): self.text.mark_set("my_anchor", "sel.first") # extend back else: self.text.mark_set("my_anchor", "sel.last") # extend forward @@ -440,7 +436,7 @@ class EditorWindow(object): ("format", "F_ormat"), ("run", "_Run"), ("options", "_Options"), - ("window", "_Window"), + ("windows", "_Window"), ("help", "_Help"), ] @@ -452,49 +448,26 @@ class EditorWindow(object): underline, label = prepstr(label) menudict[name] = menu = Menu(mbar, name=name, tearoff=0) mbar.add_cascade(label=label, menu=menu, underline=underline) - if macosx.isCarbonTk(): + + if macosxSupport.isCarbonTk(): # Insert the application menu menudict['application'] = menu = Menu(mbar, name='apple', - tearoff=0) + tearoff=0) mbar.add_cascade(label='IDLE', menu=menu) + self.fill_menus() - 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) self.base_helpmenu_length = self.menudict['help'].index(END) self.reset_help_menu_entries() def postwindowsmenu(self): - # Only called when Window menu exists - menu = self.menudict['window'] + # Only called when Windows menu exists + menu = self.menudict['windows'] end = menu.index("end") if end is None: end = -1 if end > self.wmenu_end: menu.delete(self.wmenu_end+1, end) - window.add_windows_to_menu(menu) - - def update_menu_label(self, menu, index, label): - "Update label for menu item at index." - menuitem = self.menudict[menu] - menuitem.entryconfig(index, label=label) - - def update_menu_state(self, menu, index, state): - "Update state for menu item at index." - menuitem = self.menudict[menu] - menuitem.entryconfig(index, state=state) - - def handle_yview(self, event, *args): - "Handle scrollbar." - if event == 'moveto': - fraction = float(args[0]) - lines = (round(self.getlineno('end') * fraction) - - self.getlineno('@0,0')) - event = 'scroll' - args = (lines, 'units') - self.text.yview(event, *args) - return 'break' + WindowList.add_windows_to_menu(menu) rmenu = None @@ -519,11 +492,9 @@ class EditorWindow(object): state = getattr(self, verify_state)() rmenu.entryconfigure(label, state=state) - rmenu.tk_popup(event.x_root, event.y_root) if iswin: self.text.config(cursor="ibeam") - return "break" rmenu_specs = [ # ("Label", "<<virtual-event>>", "statefuncname"), ... @@ -563,31 +534,28 @@ class EditorWindow(object): def about_dialog(self, event=None): "Handle Help 'About IDLE' event." - # Synchronize with macosx.overrideRootMenu.about_dialog. - help_about.AboutDialog(self.top) - return "break" + # 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 macosx.overrideRootMenu.config_dialog. - configdialog.ConfigDialog(self.top,'Settings') - return "break" + # Synchronize with macosxSupport.overrideRootMenu.config_dialog. + configDialog.ConfigDialog(self.top,'Settings') def help_dialog(self, event=None): "Handle Help 'IDLE Help' event." - # Synchronize with macosx.overrideRootMenu.help_dialog. + # Synchronize with macosxSupport.overrideRootMenu.help_dialog. if self.root: parent = self.root else: parent = self.top help.show_idlehelp(parent) - return "break" def python_docs(self, event=None): if sys.platform[:3] == 'win': try: os.startfile(self.help_url) - except OSError as why: + except WindowsError as why: tkMessageBox.showerror(title='Document Start Failure', message=str(why), parent=self.text) else: @@ -601,7 +569,7 @@ class EditorWindow(object): def copy(self,event): if not self.text.tag_ranges("sel"): # There is no selection, so do nothing and maybe interrupt. - return None + return self.text.event_generate("<<Copy>>") return "break" @@ -619,7 +587,6 @@ class EditorWindow(object): def remove_selection(self, event=None): self.text.tag_remove("sel", "1.0", "end") self.text.see("insert") - return "break" def move_at_edge_if_selection(self, edge_index): """Cursor move begins at start or end of selection @@ -650,23 +617,23 @@ class EditorWindow(object): return "break" def find_event(self, event): - search.find(self.text) + SearchDialog.find(self.text) return "break" def find_again_event(self, event): - search.find_again(self.text) + SearchDialog.find_again(self.text) return "break" def find_selection_event(self, event): - search.find_selection(self.text) + SearchDialog.find_selection(self.text) return "break" def find_in_files_event(self, event): - grep.grep(self.text, self.io, self.flist) + GrepDialog.grep(self.text, self.io, self.flist) return "break" def replace_event(self, event): - replace.replace(self.text) + ReplaceDialog.replace(self.text) return "break" def goto_line_event(self, event): @@ -680,60 +647,56 @@ class EditorWindow(object): return "break" text.mark_set("insert", "%d.0" % lineno) text.see("insert") - return "break" - def open_module(self): - """Get module name from user and open it. - - Return module path or None for calls by open_module_browser - when latter is not invoked in named editor window. - """ - # XXX This, open_module_browser, and open_path_browser - # would fit better in iomenu.IOBinding. + def open_module(self, event=None): + # XXX Shouldn't this be in IOBinding or in FileList? try: - name = self.text.get("sel.first", "sel.last").strip() + name = self.text.get("sel.first", "sel.last") except TclError: - name = '' - file_path = query.ModuleName( - self.text, "Open Module", - "Enter the name of a Python module\n" - "to search on sys.path and open:", - name).result - if file_path is not None: - if self.flist: - self.flist.open(file_path) - else: - self.io.loadfile(file_path) + name = "" + else: + name = name.strip() + name = tkSimpleDialog.askstring("Module", + "Enter the name of a Python module\n" + "to search on sys.path and open:", + parent=self.text, initialvalue=name) + if name: + name = name.strip() + if not name: + return + # XXX Ought to insert current file's directory in front of path + try: + (f, file_path, (suffix, mode, mtype)) = _find_module(name) + except (NameError, ImportError) as msg: + tkMessageBox.showerror("Import error", str(msg), parent=self.text) + return + if mtype != imp.PY_SOURCE: + tkMessageBox.showerror("Unsupported type", + "%s is not a source module" % name, parent=self.text) + return + if f: + f.close() + if self.flist: + self.flist.open(file_path) + else: + self.io.loadfile(file_path) return file_path - def open_module_event(self, event): - self.open_module() - return "break" - - def open_module_browser(self, event=None): + def open_class_browser(self, event=None): filename = self.io.filename if not (self.__class__.__name__ == 'PyShellEditorWindow' and filename): filename = self.open_module() if filename is None: - return "break" - from idlelib import browser - browser.ModuleBrowser(self.root, filename) - return "break" + return + head, tail = os.path.split(filename) + base, ext = os.path.splitext(tail) + from idlelib import ClassBrowser + ClassBrowser.ClassBrowser(self.flist, base, [head]) def open_path_browser(self, event=None): - from idlelib import pathbrowser - pathbrowser.PathBrowser(self.root) - return "break" - - def open_turtle_demo(self, event = None): - import subprocess - - cmd = [sys.executable, - '-c', - 'from turtledemo.__main__ import main; main()'] - subprocess.Popen(cmd, shell=False) - return "break" + from idlelib import PathBrowser + PathBrowser.PathBrowser(self.flist) def gotoline(self, lineno): if lineno is not None and lineno > 0: @@ -748,8 +711,13 @@ class EditorWindow(object): base, ext = os.path.splitext(os.path.basename(filename)) if os.path.normcase(ext) in (".py", ".pyw"): return True - line = self.text.get('1.0', '1.0 lineend') - return line.startswith('#!') and 'python' in line + try: + f = open(filename) + line = f.readline() + f.close() + except IOError: + return False + return line.startswith('#!') and line.find('python') >= 0 def close_hook(self): if self.flist: @@ -786,61 +754,34 @@ class EditorWindow(object): def ResetColorizer(self): "Update the color theme" - # Called from self.filename_change_hook and from configdialog.py + # Called from self.filename_change_hook and from configDialog.py self._rmcolorizer() self._addcolorizer() - EditorWindow.color_config(self.text) - - if self.code_context is not None: - self.code_context.update_highlight_colors() - - if self.line_numbers is not None: - self.line_numbers.update_colors() - - IDENTCHARS = string.ascii_letters + string.digits + "_" - - def colorize_syntax_error(self, text, pos): - text.tag_add("ERROR", pos) - char = text.get(pos) - if char and char in self.IDENTCHARS: - text.tag_add("ERROR", pos + " wordstart", pos) - if '\n' == text.get(pos): # error at line end - text.mark_set("insert", pos) - else: - text.mark_set("insert", pos + "+1c") - text.see(pos) - - def update_cursor_blink(self): - "Update the cursor blink configuration." - cursorblink = idleConf.GetOption( - 'main', 'EditorWindow', 'cursor-blink', type='bool') - if not cursorblink: - self.text['insertofftime'] = 0 - else: - # Restore the original value - self.text['insertofftime'] = idleConf.blink_off_time + theme = idleConf.CurrentTheme() + normal_colors = idleConf.GetHighlight(theme, 'normal') + cursor_color = idleConf.GetHighlight(theme, 'cursor', fgBg='fg') + select_colors = idleConf.GetHighlight(theme, 'hilite') + self.text.config( + foreground=normal_colors['foreground'], + background=normal_colors['background'], + insertbackground=cursor_color, + selectforeground=select_colors['foreground'], + selectbackground=select_colors['background'], + ) + if TkVersion >= 8.5: + self.text.config( + inactiveselectbackground=select_colors['background']) def ResetFont(self): "Update the text widgets' font if it is changed" - # Called from configdialog.py - - # Update the code context widget first, since its height affects - # the height of the text widget. This avoids double re-rendering. - if self.code_context is not None: - self.code_context.update_font() - # Next, update the line numbers widget, since its width affects - # the width of the text widget. - if self.line_numbers is not None: - self.line_numbers.update_font() - # Finally, update the main text widget. - new_font = idleConf.GetFont(self.root, 'main', 'EditorWindow') - self.text['font'] = new_font - self.set_width() + # Called from configDialog.py + + self.text['font'] = idleConf.GetFont(self.root, 'main','EditorWindow') def RemoveKeybindings(self): "Remove the keybindings before they are changed." - # Called from configdialog.py - self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet() + # Called from configDialog.py + self.Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet() for event, keylist in keydefs.items(): self.text.event_delete(event, *keylist) for extensionName in self.get_standard_extension_names(): @@ -851,8 +792,8 @@ class EditorWindow(object): def ApplyKeybindings(self): "Update the keybindings after they are changed" - # Called from configdialog.py - self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet() + # Called from configDialog.py + self.Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet() self.apply_bindings() for extensionName in self.get_standard_extension_names(): xkeydefs = idleConf.GetExtensionBindings(extensionName) @@ -860,12 +801,12 @@ class EditorWindow(object): self.apply_bindings(xkeydefs) #update menu accelerators menuEventDict = {} - for menu in self.mainmenu.menudefs: + for menu in self.Bindings.menudefs: menuEventDict[menu[0]] = {} for item in menu[1]: if item: menuEventDict[menu[0]][prepstr(item[0])[1]] = item[1] - for menubarItem in self.menudict: + for menubarItem in self.menudict.keys(): menu = self.menudict[menubarItem] end = menu.index(END) if end is None: @@ -887,7 +828,7 @@ class EditorWindow(object): def set_notabs_indentwidth(self): "Update the indentwidth if changed and not using tabs in this window" - # Called from configdialog.py + # Called from configDialog.py if not self.usetabs: self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces', type='int') @@ -917,7 +858,7 @@ class EditorWindow(object): if sys.platform[:3] == 'win': try: os.startfile(helpfile) - except OSError as why: + except WindowsError as why: tkMessageBox.showerror(title='Document Start Failure', message=str(why), parent=self.text) else: @@ -926,12 +867,9 @@ class EditorWindow(object): def update_recent_files_list(self, new_file=None): "Load and update the recent files list and menus" - # TODO: move to iomenu. rf_list = [] - file_path = self.recent_files_path - if file_path and os.path.exists(file_path): - with open(file_path, 'r', - encoding='utf_8', errors='replace') as rf_list_file: + if os.path.exists(self.recent_files_path): + with open(self.recent_files_path, 'r') as rf_list_file: rf_list = rf_list_file.readlines() if new_file: new_file = os.path.abspath(new_file) + '\n' @@ -946,27 +884,28 @@ class EditorWindow(object): rf_list = [path for path in rf_list if path not in bad_paths] ulchars = "1234567890ABCDEFGHIJK" rf_list = rf_list[0:len(ulchars)] - if file_path: - try: - with open(file_path, 'w', - encoding='utf_8', errors='replace') as rf_file: - rf_file.writelines(rf_list) - except OSError as err: - if not getattr(self.root, "recentfiles_message", False): - self.root.recentfiles_message = True - tkMessageBox.showwarning(title='IDLE Warning', - message="Cannot save Recent Files list to disk.\n" - f" {err}\n" - "Select OK to continue.", - parent=self.text) + try: + with open(self.recent_files_path, 'w') as rf_file: + rf_file.writelines(rf_list) + except IOError as err: + if not getattr(self.root, "recentfilelist_error_displayed", False): + self.root.recentfilelist_error_displayed = True + 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." + % str(err), + parent=self.text) # for each edit window instance, construct the recent files menu - for instance in self.top.instance_dict: + for instance in self.top.instance_dict.keys(): menu = instance.recent_files_menu menu.delete(0, END) # clear, and rebuild: for i, file_name in enumerate(rf_list): file_name = file_name.rstrip() # zap \n + # make unicode string to display non-ASCII chars correctly + ufile_name = self._filename_to_unicode(file_name) callback = instance.__recent_file_callback(file_name) - menu.add_command(label=ulchars[i] + " " + file_name, + menu.add_command(label=ulchars[i] + " " + ufile_name, command=callback, underline=0) @@ -985,7 +924,7 @@ class EditorWindow(object): elif long: title = long else: - title = "untitled" + title = "Untitled" icon = short or long or title if not self.get_saved(): title = "*%s*" % title @@ -1004,14 +943,19 @@ class EditorWindow(object): def short_title(self): filename = self.io.filename - return os.path.basename(filename) if filename else "untitled" + if filename: + filename = os.path.basename(filename) + else: + filename = "Untitled" + # return unicode string to display non-ASCII chars correctly + return self._filename_to_unicode(filename) def long_title(self): - return self.io.filename or "" + # return unicode string to display non-ASCII chars correctly + return self._filename_to_unicode(self.io.filename or "") def center_insert_event(self, event): self.center() - return "break" def center(self, mark="insert"): text = self.text @@ -1039,11 +983,11 @@ class EditorWindow(object): "Return (width, height, x, y)" geom = self.top.wm_geometry() m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom) - return list(map(int, m.groups())) + tuple = (map(int, m.groups())) + return tuple def close_event(self, event): self.close() - return "break" def maybesave(self): if self.io: @@ -1055,24 +999,21 @@ class EditorWindow(object): return self.io.maybesave() def close(self): - try: - reply = self.maybesave() - if str(reply) != "cancel": - self._close() - return reply - except AttributeError: # bpo-35379: close called twice - pass + reply = self.maybesave() + if str(reply) != "cancel": + self._close() + return reply def _close(self): if self.io.filename: self.update_recent_files_list(new_file=self.io.filename) - window.unregister_callback(self.postwindowsmenu) + WindowList.unregister_callback(self.postwindowsmenu) self.unload_extensions() self.io.close() self.io = None self.undo = None if self.color: - self.color.close() + self.color.close(False) self.color = None self.text = None self.tkinter_vars = None @@ -1088,7 +1029,7 @@ class EditorWindow(object): self.load_standard_extensions() def unload_extensions(self): - for ins in list(self.extensions.values()): + for ins in self.extensions.values(): if hasattr(ins, "close"): ins.close() self.extensions = {} @@ -1098,26 +1039,19 @@ class EditorWindow(object): try: self.load_extension(name) except: - print("Failed to load extension", repr(name)) + print "Failed to load extension", repr(name) + import traceback traceback.print_exc() def get_standard_extension_names(self): return idleConf.GetExtensions(editor_only=True) - extfiles = { # Map built-in config-extension section names to file names. - 'ZzDummy': 'zzdummy', - } - def load_extension(self, name): - fname = self.extfiles.get(name, name) try: - try: - mod = importlib.import_module('.' + fname, package=__package__) - except (ImportError, TypeError): - mod = importlib.import_module(fname) + mod = __import__(name, globals(), locals(), []) except ImportError: - print("\nFailed to import extension: ", name) - raise + print "\nFailed to import extension: ", name + return cls = getattr(mod, name) keydefs = idleConf.GetExtensionBindings(name) if hasattr(cls, "menudefs"): @@ -1126,7 +1060,7 @@ class EditorWindow(object): self.extensions[name] = ins if keydefs: self.apply_bindings(keydefs) - for vevent in keydefs: + for vevent in keydefs.keys(): methodname = vevent.replace("-", "_") while methodname[:1] == '<': methodname = methodname[1:] @@ -1138,7 +1072,7 @@ class EditorWindow(object): def apply_bindings(self, keydefs=None): if keydefs is None: - keydefs = self.mainmenu.default_keydefs + keydefs = self.Bindings.default_keydefs text = self.text text.keydefs = keydefs for event, keylist in keydefs.items(): @@ -1151,9 +1085,9 @@ class EditorWindow(object): Menus that are absent or None in self.menudict are ignored. """ if menudefs is None: - menudefs = self.mainmenu.menudefs + menudefs = self.Bindings.menudefs if keydefs is None: - keydefs = self.mainmenu.default_keydefs + keydefs = self.Bindings.default_keydefs menudict = self.menudict text = self.text for mname, entrylist in menudefs: @@ -1188,14 +1122,14 @@ class EditorWindow(object): value = var.get() return value else: - raise NameError(name) + raise NameError, name def setvar(self, name, value, vartype=None): var = self.get_var_obj(name, vartype) if var: var.set(value) else: - raise NameError(name) + raise NameError, name def get_var_obj(self, name, vartype=None): var = self.tkinter_vars.get(name) @@ -1236,31 +1170,34 @@ class EditorWindow(object): # Return the text widget's current view of what a tab stop means # (equivalent width in spaces). - def get_tk_tabwidth(self): + def get_tabwidth(self): current = self.text['tabs'] or TK_TABWIDTH_DEFAULT return int(current) # Set the text widget's current view of what a tab stop means. - def set_tk_tabwidth(self, newtabwidth): + def set_tabwidth(self, newtabwidth): text = self.text - if self.get_tk_tabwidth() != newtabwidth: - # Set text widget tab width + if self.get_tabwidth() != newtabwidth: pixels = text.tk.call("font", "measure", text["font"], "-displayof", text.master, "n" * newtabwidth) text.configure(tabs=pixels) -### begin autoindent code ### (configuration was moved to beginning of class) + # If ispythonsource and guess are true, guess a good value for + # indentwidth based on file content (if possible), and if + # indentwidth != tabwidth set usetabs false. + # In any case, adjust the Text widget's view of what a tab + # character means. - def set_indentation_params(self, is_py_src, guess=True): - if is_py_src and guess: + def set_indentation_params(self, ispythonsource, guess=True): + if guess and ispythonsource: i = self.guess_indent() if 2 <= i <= 8: self.indentwidth = i if self.indentwidth != self.tabwidth: self.usetabs = False - self.set_tk_tabwidth(self.tabwidth) + self.set_tabwidth(self.tabwidth) def smart_backspace_event(self, event): text = self.text @@ -1290,9 +1227,13 @@ class EditorWindow(object): assert have > 0 want = ((have - 1) // self.indentwidth) * self.indentwidth # Debug prompt is multilined.... + if self.context_use_ps1: + last_line_of_prompt = sys.ps1.split('\n')[-1] + else: + last_line_of_prompt = '' ncharsdeleted = 0 while 1: - if chars == self.prompt_last_line: # '' unless PyShell + if chars == last_line_of_prompt: break chars = chars[:-1] ncharsdeleted = ncharsdeleted + 1 @@ -1319,11 +1260,11 @@ class EditorWindow(object): try: if first and last: if index2line(first) != index2line(last): - return self.fregion.indent_region_event(event) + return self.indent_region_event(event) text.delete(first, last) text.mark_set("insert", first) prefix = text.get("insert linestart", "insert") - raw, effective = get_line_indent(prefix, self.tabwidth) + raw, effective = classifyws(prefix, self.tabwidth) if raw == len(prefix): # only whitespace to the left self.reindent_to(effective + self.indentwidth) @@ -1361,7 +1302,8 @@ class EditorWindow(object): indent = line[:i] # strip whitespace before insert point unless it's in the prompt i = 0 - while line and line[-1] in " \t" and line != self.prompt_last_line: + last_line_of_prompt = sys.ps1.split('\n')[-1] + while line and line[-1] in " \t" and line != last_line_of_prompt: line = line[:-1] i = i+1 if i: @@ -1375,14 +1317,15 @@ class EditorWindow(object): # adjust indentation for continuations and block # open/close first need to find the last stmt lno = index2line(text.index('insert')) - y = pyparse.Parser(self.indentwidth, self.tabwidth) - if not self.prompt_last_line: + y = PyParse.Parser(self.indentwidth, self.tabwidth) + if not self.context_use_ps1: for context in self.num_context_lines: startat = max(lno - context, 1) startatindex = repr(startat) + ".0" rawtext = text.get(startatindex, "insert") - y.set_code(rawtext) + y.set_str(rawtext) bod = y.find_good_parse_start( + self.context_use_ps1, self._build_char_in_string_func(startatindex)) if bod is not None or startat == 1: break @@ -1394,26 +1337,26 @@ class EditorWindow(object): else: startatindex = "1.0" rawtext = text.get(startatindex, "insert") - y.set_code(rawtext) + y.set_str(rawtext) y.set_lo(0) c = y.get_continuation_type() - if c != pyparse.C_NONE: + if c != PyParse.C_NONE: # The current stmt hasn't ended yet. - if c == pyparse.C_STRING_FIRST_LINE: + if c == PyParse.C_STRING_FIRST_LINE: # after the first line of a string; do not indent at all pass - elif c == pyparse.C_STRING_NEXT_LINES: + elif c == PyParse.C_STRING_NEXT_LINES: # inside a string which started before this line; # just mimic the current indent text.insert("insert", indent) - elif c == pyparse.C_BRACKET: + elif c == PyParse.C_BRACKET: # line up with the first (if any) element of the # last open bracket structure; else indent one # level beyond the indent of the line with the # last open bracket self.reindent_to(y.compute_bracket_indent()) - elif c == pyparse.C_BACKSLASH: + elif c == PyParse.C_BACKSLASH: # if more than one line in this stmt already, just # mimic the current indent; else if initial line # has a start on an assignment stmt, indent to @@ -1452,6 +1395,82 @@ class EditorWindow(object): return _icis(_startindex + "+%dc" % offset) return inner + def indent_region_event(self, event): + head, tail, chars, lines = self.get_region() + for pos in range(len(lines)): + line = lines[pos] + if line: + raw, effective = classifyws(line, self.tabwidth) + effective = effective + self.indentwidth + lines[pos] = self._make_blanks(effective) + line[raw:] + self.set_region(head, tail, chars, lines) + return "break" + + def dedent_region_event(self, event): + head, tail, chars, lines = self.get_region() + for pos in range(len(lines)): + line = lines[pos] + if line: + raw, effective = classifyws(line, self.tabwidth) + effective = max(effective - self.indentwidth, 0) + lines[pos] = self._make_blanks(effective) + line[raw:] + self.set_region(head, tail, chars, lines) + return "break" + + def comment_region_event(self, event): + head, tail, chars, lines = self.get_region() + for pos in range(len(lines) - 1): + line = lines[pos] + lines[pos] = '##' + line + self.set_region(head, tail, chars, lines) + + def uncomment_region_event(self, event): + head, tail, chars, lines = self.get_region() + for pos in range(len(lines)): + line = lines[pos] + if not line: + continue + if line[:2] == '##': + line = line[2:] + elif line[:1] == '#': + line = line[1:] + lines[pos] = line + self.set_region(head, tail, chars, lines) + + def tabify_region_event(self, event): + head, tail, chars, lines = self.get_region() + tabwidth = self._asktabwidth() + if tabwidth is None: return + for pos in range(len(lines)): + line = lines[pos] + if line: + raw, effective = classifyws(line, tabwidth) + ntabs, nspaces = divmod(effective, tabwidth) + lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:] + self.set_region(head, tail, chars, lines) + + def untabify_region_event(self, event): + head, tail, chars, lines = self.get_region() + tabwidth = self._asktabwidth() + if tabwidth is None: return + for pos in range(len(lines)): + lines[pos] = lines[pos].expandtabs(tabwidth) + self.set_region(head, tail, chars, lines) + + def toggle_tabs_event(self, event): + if self.askyesno( + "Toggle tabs", + "Turn tabs " + ("on", "off")[self.usetabs] + + "?\nIndent width " + + ("will be", "remains at")[self.usetabs] + " 8." + + "\n Note: a tab is always 8 columns", + parent=self.text): + self.usetabs = not self.usetabs + # Try to prevent inconsistent indentation. + # User must change indent width manually after using tabs. + self.indentwidth = 8 + return "break" + # XXX this isn't bound to anything -- see tabwidth comments ## def change_tabwidth_event(self, event): ## new = self._asktabwidth() @@ -1460,6 +1479,45 @@ class EditorWindow(object): ## self.set_indentation_params(0, guess=0) ## return "break" + def change_indentwidth_event(self, event): + new = self.askinteger( + "Indent width", + "New indent width (2-16)\n(Always use 8 when using tabs)", + parent=self.text, + initialvalue=self.indentwidth, + minvalue=2, + maxvalue=16) + if new and new != self.indentwidth and not self.usetabs: + self.indentwidth = new + return "break" + + def get_region(self): + text = self.text + first, last = self.get_selection_indices() + if first and last: + head = text.index(first + " linestart") + tail = text.index(last + "-1c lineend +1c") + else: + head = text.index("insert linestart") + tail = text.index("insert lineend +1c") + chars = text.get(head, tail) + lines = chars.split("\n") + return head, tail, chars, lines + + def set_region(self, head, tail, chars, lines): + text = self.text + newchars = "\n".join(lines) + if newchars == chars: + text.bell() + return + text.tag_remove("sel", "1.0", "end") + text.mark_set("insert", head) + text.undo_block_start() + text.delete(head, tail) + text.insert(head, newchars) + text.undo_block_stop() + text.tag_add("sel", head, "insert") + # Make string that displays as n leading blanks. def _make_blanks(self, n): @@ -1481,6 +1539,15 @@ class EditorWindow(object): text.insert("insert", self._make_blanks(column)) text.undo_block_stop() + def _asktabwidth(self): + return self.askinteger( + "Tab width", + "Columns per tab? (2-16)", + parent=self.text, + initialvalue=self.indentwidth, + minvalue=2, + maxvalue=16) + # Guess indentwidth from text content. # Return guessed indentwidth. This should not be believed unless # it's in a reasonable range (e.g., it will be 0 if no indented @@ -1489,40 +1556,37 @@ class EditorWindow(object): def guess_indent(self): opener, indented = IndentSearcher(self.text, self.tabwidth).run() if opener and indented: - raw, indentsmall = get_line_indent(opener, self.tabwidth) - raw, indentlarge = get_line_indent(indented, self.tabwidth) + raw, indentsmall = classifyws(opener, self.tabwidth) + raw, indentlarge = classifyws(indented, self.tabwidth) else: indentsmall = indentlarge = 0 return indentlarge - indentsmall - def toggle_line_numbers_event(self, event=None): - if self.line_numbers is None: - return - - if self.line_numbers.is_shown: - self.line_numbers.hide_sidebar() - menu_label = "Show" - else: - self.line_numbers.show_sidebar() - menu_label = "Hide" - self.update_menu_label(menu='options', index='*Line Numbers', - label=f'{menu_label} Line Numbers') - # "line.col" -> line, as an int def index2line(index): return int(float(index)) +# Look at the leading whitespace in s. +# Return pair (# of leading ws characters, +# effective # of leading blanks after expanding +# tabs to width tabwidth) + +def classifyws(s, tabwidth): + raw = effective = 0 + for ch in s: + if ch == ' ': + raw = raw + 1 + effective = effective + 1 + elif ch == '\t': + raw = raw + 1 + effective = (effective // tabwidth + 1) * tabwidth + else: + break + return raw, effective -_line_indent_re = re.compile(r'[ \t]*') -def get_line_indent(line, tabwidth): - """Return a line's indentation as (# chars, effective # of spaces). - - The effective # of spaces is the length after properly "expanding" - the tabs into spaces, as done by str.expandtabs(tabwidth). - """ - m = _line_indent_re.match(line) - return m.end(), len(m.group().expandtabs(tabwidth)) - +import tokenize +_tokenize = tokenize +del tokenize class IndentSearcher(object): @@ -1547,8 +1611,8 @@ class IndentSearcher(object): return self.text.get(mark, mark + " lineend+1c") def tokeneater(self, type, token, start, end, line, - INDENT=tokenize.INDENT, - NAME=tokenize.NAME, + INDENT=_tokenize.INDENT, + NAME=_tokenize.NAME, OPENERS=('class', 'def', 'for', 'if', 'try', 'while')): if self.finished: pass @@ -1559,19 +1623,17 @@ class IndentSearcher(object): self.finished = 1 def run(self): - save_tabsize = tokenize.tabsize - tokenize.tabsize = self.tabwidth + save_tabsize = _tokenize.tabsize + _tokenize.tabsize = self.tabwidth try: try: - tokens = tokenize.generate_tokens(self.readline) - for token in tokens: - self.tokeneater(*token) - except (tokenize.TokenError, SyntaxError): + _tokenize.tokenize(self.readline, self.tokeneater) + except (_tokenize.TokenError, SyntaxError): # since we cut off the tokenizer early, we can trigger # spurious errors pass finally: - tokenize.tabsize = save_tabsize + _tokenize.tabsize = save_tabsize return self.blkopenline, self.indentedline ### end autoindent code ### @@ -1595,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 (macosx.isCocoaTk() and eventname in { + if (not keylist) or (macosxSupport.isCocoaTk() and eventname in { "<<open-module>>", "<<goto-line>>", "<<change-indentwidth>>"}): @@ -1614,12 +1676,12 @@ def get_accelerator(keydefs, eventname): def fixwordbreaks(root): - # On Windows, tcl/tk breaks 'words' only on spaces, as in Command Prompt. - # We want Motif style everywhere. See #21474, msg218992 and followup. + # Make sure that Tk's double-click and next/previous word + # operations use our definition of a word (i.e. an identifier) tk = root.tk tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded - tk.call('set', 'tcl_wordchars', r'\w') - tk.call('set', 'tcl_nonwordchars', r'\W') + tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]') + tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]') def _editor_window(parent): # htest # @@ -1630,19 +1692,13 @@ def _editor_window(parent): # htest # filename = sys.argv[1] else: filename = None - macosx.setupApp(root, None) + macosxSupport.setupApp(root, None) edit = EditorWindow(root=root, filename=filename) - text = edit.text - text['height'] = 10 - for i in range(20): - text.insert('insert', ' '*i + str(i) + '\n') - # text.bind("<<close-all-windows>>", edit.close_event) + edit.text.bind("<<close-all-windows>>", edit.close_event) # Does not stop error, neither does following # edit.text.bind("<<close-window>>", edit.close_event) -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_editor', verbosity=2, exit=False) +if __name__ == '__main__': from idlelib.idle_test.htest import run run(_editor_window) diff --git a/Lib/idlelib/filelist.py b/Lib/idlelib/FileList.py index 0d20085..46979e3 100644 --- a/Lib/idlelib/filelist.py +++ b/Lib/idlelib/FileList.py @@ -1,13 +1,12 @@ -"idlelib.filelist" - import os -from tkinter import messagebox as tkMessageBox +from Tkinter import * +import tkMessageBox class FileList: # N.B. this import overridden in PyShellFileList. - from idlelib.editor import EditorWindow + from idlelib.EditorWindow import EditorWindow def __init__(self, root): self.root = root @@ -34,12 +33,7 @@ class FileList: # Don't create window, perform 'action', e.g. open in same window return action(filename) else: - edit = self.EditorWindow(self, filename, key) - if edit.good_load: - return edit - else: - edit._close() - return None + return self.EditorWindow(self, filename, key) def gotofileline(self, filename, lineno=None): edit = self.open(filename) @@ -50,7 +44,7 @@ class FileList: return self.EditorWindow(self, filename) def close_all_callback(self, *args, **kwds): - for edit in list(self.inversedict): + for edit in self.inversedict.keys(): reply = edit.close() if reply == "cancel": break @@ -60,7 +54,7 @@ class FileList: try: key = self.inversedict[edit] except KeyError: - print("Don't know this EditorWindow object. (close)") + print "Don't know this EditorWindow object. (close)" return if key: del self.dict[key] @@ -73,7 +67,7 @@ class FileList: try: key = self.inversedict[edit] except KeyError: - print("Don't know this EditorWindow object. (rename)") + print "Don't know this EditorWindow object. (rename)" return filename = edit.io.filename if not filename: @@ -104,28 +98,29 @@ class FileList: if not os.path.isabs(filename): try: pwd = os.getcwd() - except OSError: + except os.error: pass else: filename = os.path.join(pwd, filename) return os.path.normpath(filename) -def _test(): # TODO check and convert to htest - from tkinter import Tk - from idlelib.editor import fixwordbreaks +def _test(): + from idlelib.EditorWindow import fixwordbreaks from idlelib.run import fix_scaling + import sys root = Tk() fix_scaling(root) fixwordbreaks(root) root.withdraw() flist = FileList(root) - flist.new() + if sys.argv[1:]: + for filename in sys.argv[1:]: + flist.open(filename) + else: + flist.new() if flist.inversedict: root.mainloop() if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_filelist', verbosity=2) - -# _test() + _test() diff --git a/Lib/idlelib/FormatParagraph.py b/Lib/idlelib/FormatParagraph.py new file mode 100644 index 0000000..7a9d185 --- /dev/null +++ b/Lib/idlelib/FormatParagraph.py @@ -0,0 +1,195 @@ +"""Extension to format a paragraph or selection to a max width. + +Does basic, standard text formatting, and also understands Python +comment blocks. Thus, for editing Python source code, this +extension is really only suitable for reformatting these comment +blocks or triple-quoted strings. + +Known problems with comment reformatting: +* If there is a selection marked, and the first line of the + selection is not complete, the block will probably not be detected + as comments, and will have the normal "text formatting" rules + applied. +* If a comment block has leading whitespace that mixes tabs and + spaces, they will not be considered part of the same block. +* Fancy comments, like this bulleted list, aren't handled :-) +""" + +import re +from idlelib.configHandler import idleConf + +class FormatParagraph: + + menudefs = [ + ('format', [ # /s/edit/format dscherer@cmu.edu + ('Format Paragraph', '<<format-paragraph>>'), + ]) + ] + + def __init__(self, editwin): + self.editwin = editwin + + def close(self): + self.editwin = None + + 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 + at the max width, starting from the beginning selection. + + 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. + """ + 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: + data = text.get(first, last) + comment_header = get_comment_header(data) + else: + first, last, comment_header, data = \ + find_paragraph(text, text.index("insert")) + if comment_header: + newdata = reformat_comment(data, limit, comment_header) + else: + newdata = reformat_paragraph(data, limit) + text.tag_remove("sel", "1.0", "end") + + if newdata != data: + text.mark_set("insert", first) + text.undo_block_start() + text.delete(first, last) + text.insert(first, newdata) + text.undo_block_stop() + else: + text.mark_set("insert", last) + text.see("insert") + return "break" + +def find_paragraph(text, mark): + """Returns the start/stop indices enclosing the paragraph that mark is in. + + Also returns the comment format string, if any, and paragraph of text + between the start/stop indices. + """ + lineno, col = map(int, mark.split(".")) + line = text.get("%d.0" % lineno, "%d.end" % lineno) + + # Look for start of next paragraph if the index passed in is a blank line + while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line): + lineno = lineno + 1 + line = text.get("%d.0" % lineno, "%d.end" % lineno) + first_lineno = lineno + comment_header = get_comment_header(line) + comment_header_len = len(comment_header) + + # Once start line found, search for end of paragraph (a blank line) + while get_comment_header(line)==comment_header and \ + not is_all_white(line[comment_header_len:]): + lineno = lineno + 1 + line = text.get("%d.0" % lineno, "%d.end" % lineno) + last = "%d.0" % lineno + + # Search back to beginning of paragraph (first blank line before) + lineno = first_lineno - 1 + line = text.get("%d.0" % lineno, "%d.end" % lineno) + while lineno > 0 and \ + get_comment_header(line)==comment_header and \ + not is_all_white(line[comment_header_len:]): + lineno = lineno - 1 + line = text.get("%d.0" % lineno, "%d.end" % lineno) + first = "%d.0" % (lineno+1) + + return first, last, comment_header, text.get(first, last) + +# This should perhaps be replaced with textwrap.wrap +def reformat_paragraph(data, limit): + """Return data reformatted to specified width (limit).""" + lines = data.split("\n") + i = 0 + n = len(lines) + while i < n and is_all_white(lines[i]): + i = i+1 + if i >= n: + return data + indent1 = get_indent(lines[i]) + if i+1 < n and not is_all_white(lines[i+1]): + indent2 = get_indent(lines[i+1]) + else: + indent2 = indent1 + new = lines[:i] + partial = indent1 + while i < n and not is_all_white(lines[i]): + # XXX Should take double space after period (etc.) into account + words = re.split("(\s+)", lines[i]) + for j in range(0, len(words), 2): + word = words[j] + if not word: + continue # Can happen when line ends in whitespace + if len((partial + word).expandtabs()) > limit and \ + partial != indent1: + new.append(partial.rstrip()) + partial = indent2 + partial = partial + word + " " + if j+1 < len(words) and words[j+1] != " ": + partial = partial + " " + i = i+1 + new.append(partial.rstrip()) + # XXX Should reformat remaining paragraphs as well + new.extend(lines[i:]) + return "\n".join(new) + +def reformat_comment(data, limit, comment_header): + """Return data reformatted to specified width with comment header.""" + + # Remove header from the comment lines + lc = len(comment_header) + data = "\n".join(line[lc:] for line in data.split("\n")) + # Reformat to maxformatwidth chars or a 20 char width, + # whichever is greater. + format_width = max(limit - len(comment_header), 20) + newdata = reformat_paragraph(data, format_width) + # re-split and re-insert the comment header. + newdata = newdata.split("\n") + # If the block ends in a \n, we dont want the comment prefix + # inserted after it. (Im not sure it makes sense to reformat a + # comment block that is not made of complete lines, but whatever!) + # Can't think of a clean solution, so we hack away + block_suffix = "" + if not newdata[-1]: + block_suffix = "\n" + newdata = newdata[:-1] + return '\n'.join(comment_header+line for line in newdata) + block_suffix + +def is_all_white(line): + """Return True if line is empty or all whitespace.""" + + return re.match(r"^\s*$", line) is not None + +def get_indent(line): + """Return the initial space or tab indent of line.""" + return re.match(r"^([ \t]*)", line).group() + +def get_comment_header(line): + """Return string with leading whitespace and '#' from line or ''. + + A null return indicates that the line is not a comment line. A non- + null return, such as ' #', will be used to find the other lines of + a comment block with the same indent. + """ + m = re.match(r"^([ \t]*#*)", line) + if m is None: return "" + return m.group(1) + +if __name__ == "__main__": + 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 new file mode 100644 index 0000000..d86d50d --- /dev/null +++ b/Lib/idlelib/GrepDialog.py @@ -0,0 +1,159 @@ +from __future__ import print_function +import os +import fnmatch +import re # for htest +import sys +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() + engine = SearchEngine.get(root) + if not hasattr(engine, "_grepdialog"): + engine._grepdialog = GrepDialog(root, engine, flist) + dialog = engine._grepdialog + searchphrase = text.get("sel.first", "sel.last") + dialog.open(text, searchphrase, io) + +class GrepDialog(SearchDialogBase): + + title = "Find in Files Dialog" + icon = "Grep" + needwrapbutton = 0 + + def __init__(self, root, engine, flist): + SearchDialogBase.__init__(self, root, engine) + self.flist = flist + self.globvar = StringVar(root) + self.recvar = BooleanVar(root) + + def open(self, text, searchphrase, io=None): + SearchDialogBase.open(self, text, searchphrase) + if io: + path = io.filename or "" + else: + path = "" + dir, base = os.path.split(path) + head, tail = os.path.splitext(base) + if not tail: + tail = ".py" + self.globvar.set(os.path.join(dir, "*" + tail)) + + def create_entries(self): + SearchDialogBase.create_entries(self) + self.globent = self.make_entry("In files:", self.globvar)[0] + + def create_other_buttons(self): + f = self.make_frame()[0] + + btn = Checkbutton(f, anchor="w", + variable=self.recvar, + text="Recurse down subdirectories") + btn.pack(side="top", fill="both") + btn.select() + + def create_command_buttons(self): + SearchDialogBase.create_command_buttons(self) + self.make_button("Search Files", self.default_command, 1) + + def default_command(self, event=None): + prog = self.engine.getprog() + if not prog: + return + path = self.globvar.get() + if not path: + self.top.bell() + return + from idlelib.OutputWindow import OutputWindow # leave here! + save = sys.stdout + try: + sys.stdout = OutputWindow(self.flist) + self.grep_it(prog, path) + finally: + sys.stdout = save + + def grep_it(self, prog, path): + dir, base = os.path.split(path) + list = self.findfiles(dir, base, self.recvar.get()) + list.sort() + self.close() + pat = self.engine.getpat() + print("Searching %r in %s ..." % (pat, path)) + hits = 0 + try: + for fn in list: + try: + with open(fn) 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 IOError 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: + names = os.listdir(dir or os.curdir) + except os.error as msg: + print(msg) + return [] + list = [] + subdirs = [] + for name in names: + fn = os.path.join(dir, name) + if os.path.isdir(fn): + subdirs.append(fn) + else: + if fnmatch.fnmatch(name, base): + list.append(fn) + if rec: + for subdir in subdirs: + list.extend(self.findfiles(subdir, base, rec)) + return list + + def close(self, event=None): + if self.top: + 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__": + 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/HISTORY.txt b/Lib/idlelib/HISTORY.txt index 731fabd..01d73ed 100644 --- a/Lib/idlelib/HISTORY.txt +++ b/Lib/idlelib/HISTORY.txt @@ -11,7 +11,7 @@ What's New in IDLEfork 0.8.1? *Release date: 22-Jul-2001* - New tarball released as a result of the 'revitalisation' of the IDLEfork - project. + project. - This release requires python 2.1 or better. Compatibility with earlier versions of python (especially ancient ones like 1.5x) is no longer a @@ -26,8 +26,8 @@ What's New in IDLEfork 0.8.1? not working, but I believe this was the case with the previous IDLE fork release (0.7.1) as well. -- This release is being made now to mark the point at which IDLEfork is - launching into a new stage of development. +- This release is being made now to mark the point at which IDLEfork is + launching into a new stage of development. - IDLEfork CVS will now be branched to enable further development and exploration of the two "execution in a remote process" patches submitted by @@ -96,7 +96,7 @@ IDLEfork 0.7.1 - 29 May 2000 instead of the IDLE help; shift-TAB is now a synonym for unindent. - New modules: - + ExecBinding.py Executes program through loader loader.py Bootstraps user program protocol.py RPC protocol diff --git a/Lib/idlelib/hyperparser.py b/Lib/idlelib/HyperParser.py index 77baca7..6e45b16 100644 --- a/Lib/idlelib/hyperparser.py +++ b/Lib/idlelib/HyperParser.py @@ -4,38 +4,26 @@ HyperParser uses PyParser. PyParser mostly gives information on the proper indentation of code. HyperParser gives additional information on the structure of code. """ -from keyword import iskeyword -import string - -from idlelib import pyparse - -# 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)] +import string +import keyword +from idlelib import PyParse class HyperParser: + def __init__(self, editwin, index): "To initialize, analyze the surroundings of the given index." self.editwin = editwin self.text = text = editwin.text - parser = pyparse.Parser(editwin.indentwidth, editwin.tabwidth) + parser = PyParse.Parser(editwin.indentwidth, editwin.tabwidth) def index2line(index): return int(float(index)) lno = index2line(text.index(index)) - if not editwin.prompt_last_line: + if not editwin.context_use_ps1: for context in editwin.num_context_lines: startat = max(lno - context, 1) startatindex = repr(startat) + ".0" @@ -44,7 +32,7 @@ class HyperParser: # 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_code(text.get(startatindex, stopatindex)+' \n') + parser.set_str(text.get(startatindex, stopatindex)+' \n') bod = parser.find_good_parse_start( editwin._build_char_in_string_func(startatindex)) if bod is not None or startat == 1: @@ -60,12 +48,12 @@ class HyperParser: # 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_code(text.get(startatindex, stopatindex)+' \n') + parser.set_str(text.get(startatindex, stopatindex)+' \n') parser.set_lo(0) # We want what the parser has, minus the last newline and space. - self.rawtext = parser.code[:-2] - # Parser.code apparently preserves the statement we are in, so + self.rawtext = parser.str[:-2] + # 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 @@ -155,70 +143,25 @@ class HyperParser: return beforeindex, afterindex - # 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. + # Ascii chars that may be in a white space + _whitespace_chars = " \t\n\\" + # Ascii chars that may be in an identifier + _id_chars = string.ascii_letters + string.digits + "_" + # Ascii 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): i = pos - - # 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])] - ): + while i > limit and str[i-1] in self._id_chars: i -= 1 - - # 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 - + if (i < pos and (str[i] not in self._id_first_chars or + keyword.iskeyword(str[i:pos]))): + i = pos 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. @@ -308,5 +251,5 @@ class HyperParser: if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_hyperparser', verbosity=2) + import unittest + unittest.main('idlelib.idle_test.test_hyperparser', verbosity=2) diff --git a/Lib/idlelib/iomenu.py b/Lib/idlelib/IOBinding.py index 4b2833b..872bece 100644 --- a/Lib/idlelib/iomenu.py +++ b/Lib/idlelib/IOBinding.py @@ -1,92 +1,128 @@ +# changes by dscherer@cmu.edu +# - IOBinding.open() replaces the current window with the opened file, +# if the current window is both unmodified and unnamed +# - IOBinding.loadfile() interprets Windows, UNIX, and Macintosh +# end-of-line conventions, instead of relying on the standard library, +# which will only understand the local convention. + import codecs from codecs import BOM_UTF8 import os +import pipes import re -import shlex import sys import tempfile -import tkinter.filedialog as tkFileDialog -import tkinter.messagebox as tkMessageBox -from tkinter.simpledialog import askstring +from Tkinter import * +import tkFileDialog +import tkMessageBox +from SimpleDialog import SimpleDialog -import idlelib -from idlelib.config import idleConf +from idlelib.configHandler import idleConf -if idlelib.testing: # Set True by test.test_idle to avoid setlocale. - encoding = 'utf-8' - errors = 'surrogateescape' -else: - # Try setting the locale, so that we can find out - # what encoding to use - try: - import locale - locale.setlocale(locale.LC_CTYPE, "") - except (ImportError, locale.Error): - pass +# Try setting the locale, so that we can find out +# what encoding to use +try: + import locale + locale.setlocale(locale.LC_CTYPE, "") +except (ImportError, locale.Error): + pass - if sys.platform == 'win32': - encoding = 'utf-8' - errors = 'surrogateescape' - else: - try: - # Different things can fail here: the locale module may not be - # loaded, it may not offer nl_langinfo, or CODESET, or the - # resulting codeset may be unknown to Python. We ignore all - # these problems, falling back to ASCII - locale_encoding = locale.nl_langinfo(locale.CODESET) - if locale_encoding: - codecs.lookup(locale_encoding) - except (NameError, AttributeError, LookupError): - # Try getdefaultlocale: it parses environment variables, - # which may give a clue. Unfortunately, getdefaultlocale has - # bugs that can cause ValueError. - try: - locale_encoding = locale.getdefaultlocale()[1] - if locale_encoding: - codecs.lookup(locale_encoding) - except (ValueError, LookupError): - pass +# Encoding for file names +filesystemencoding = sys.getfilesystemencoding() - if locale_encoding: - encoding = locale_encoding.lower() - errors = 'strict' - else: - # POSIX locale or macOS +encoding = "ascii" +if sys.platform == 'win32': + # On Windows, we could use "mbcs". However, to give the user + # a portable encoding name, we need to find the code page + try: + encoding = locale.getdefaultlocale()[1] + codecs.lookup(encoding) + except LookupError: + pass +else: + try: + # Different things can fail here: the locale module may not be + # loaded, it may not offer nl_langinfo, or CODESET, or the + # resulting codeset may be unknown to Python. We ignore all + # these problems, falling back to ASCII + encoding = locale.nl_langinfo(locale.CODESET) + if encoding is None or encoding is '': + # situation occurs on Mac OS X encoding = 'ascii' - errors = 'surrogateescape' - # Encoding is used in multiple files; locale_encoding nowhere. - # The only use of 'encoding' below is in _decode as initial value - # of deprecated block asking user for encoding. - # Perhaps use elsewhere should be reviewed. - -coding_re = re.compile(r'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)', re.ASCII) -blank_re = re.compile(r'^[ \t\f]*(?:[#\r\n]|$)', re.ASCII) + codecs.lookup(encoding) + except (NameError, AttributeError, LookupError): + # Try getdefaultlocale well: it parses environment variables, + # which may give a clue. Unfortunately, getdefaultlocale has + # bugs that can cause ValueError. + try: + encoding = locale.getdefaultlocale()[1] + if encoding is None or encoding is '': + # situation occurs on Mac OS X + encoding = 'ascii' + codecs.lookup(encoding) + except (ValueError, LookupError): + pass -def coding_spec(data): +encoding = encoding.lower() + +coding_re = re.compile(r'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)') +blank_re = re.compile(r'^[ \t\f]*(?:[#\r\n]|$)') + +class EncodingMessage(SimpleDialog): + "Inform user that an encoding declaration is needed." + def __init__(self, master, enc): + self.should_edit = False + + self.root = top = Toplevel(master) + top.bind("<Return>", self.return_event) + top.bind("<Escape>", self.do_ok) + top.protocol("WM_DELETE_WINDOW", self.wm_delete_window) + top.wm_title("I/O Warning") + top.wm_iconname("I/O Warning") + self.top = top + + l1 = Label(top, + text="Non-ASCII found, yet no encoding declared. Add a line like") + l1.pack(side=TOP, anchor=W) + l2 = Entry(top, font="courier") + l2.insert(0, "# -*- coding: %s -*-" % enc) + # For some reason, the text is not selectable anymore if the + # widget is disabled. + # l2['state'] = DISABLED + l2.pack(side=TOP, anchor = W, fill=X) + l3 = Label(top, text="to your file\n" + "See Language Reference, 2.1.4 Encoding declarations.\n" + "Choose OK to save this file as %s\n" + "Edit your general options to silence this warning" % enc) + l3.pack(side=TOP, anchor = W) + + buttons = Frame(top) + buttons.pack(side=TOP, fill=X) + # Both return and cancel mean the same thing: do nothing + self.default = self.cancel = 0 + b1 = Button(buttons, text="Ok", default="active", + command=self.do_ok) + b1.pack(side=LEFT, fill=BOTH, expand=1) + b2 = Button(buttons, text="Edit my file", + command=self.do_edit) + b2.pack(side=LEFT, fill=BOTH, expand=1) + + self._set_transient(master) + + def do_ok(self): + self.done(0) + + def do_edit(self): + self.done(1) + +def coding_spec(str): """Return the encoding declaration according to PEP 263. - When checking encoded data, only the first two lines should be passed - in to avoid a UnicodeDecodeError if the rest of the data is not unicode. - The first two lines would contain the encoding specification. - - Raise a LookupError if the encoding is declared but unknown. + Raise LookupError if the encoding is declared but unknown. """ - if isinstance(data, bytes): - # This encoding might be wrong. However, the coding - # spec must be ASCII-only, so any non-ASCII characters - # around here will be ignored. Decoding to Latin-1 should - # never fail (except for memory outage) - lines = data.decode('iso-8859-1') - else: - lines = data - # consider only the first two lines - if '\n' in lines: - lst = lines.split('\n', 2)[:2] - elif '\r' in lines: - lst = lines.split('\r', 2)[:2] - else: - lst = [lines] + # Only consider the first two lines + lst = str.split("\n", 2)[:2] for line in lst: match = coding_re.match(line) if match is not None: @@ -96,18 +132,16 @@ def coding_spec(data): else: return None name = match.group(1) + # Check whether the encoding is known + import codecs try: codecs.lookup(name) except LookupError: # The standard encoding error does not indicate the encoding - raise LookupError("Unknown encoding: "+name) + raise LookupError, "Unknown encoding "+name return name - class IOBinding: -# One instance per editor Window so methods know which to save, close. -# Open returns focus to self.editwin if aborted. -# EditorWindow.open_module, others, belong here. def __init__(self, editwin): self.editwin = editwin @@ -207,61 +241,57 @@ class IOBinding: eol = r"(\r\n)|\n|\r" # \r\n (Windows), \n (UNIX), or \r (Mac) eol_re = re.compile(eol) - eol_convention = os.linesep # default + eol_convention = os.linesep # Default def loadfile(self, filename): try: # open the file in binary mode so that we can handle - # end-of-line convention ourselves. + # end-of-line convention ourselves. with open(filename, 'rb') as f: - two_lines = f.readline() + f.readline() - f.seek(0) - bytes = f.read() - except OSError as msg: + chars = f.read() + except IOError as msg: tkMessageBox.showerror("I/O Error", str(msg), parent=self.text) return False - chars, converted = self._decode(two_lines, bytes) - if chars is None: - tkMessageBox.showerror("Decoding Error", - "File %s\nFailed to Decode" % filename, - parent=self.text) - return False + + chars = self.decode(chars) # We now convert all end-of-lines to '\n's firsteol = self.eol_re.search(chars) if firsteol: self.eol_convention = firsteol.group(0) + if isinstance(self.eol_convention, unicode): + # Make sure it is an ASCII string + self.eol_convention = self.eol_convention.encode("ascii") chars = self.eol_re.sub(r"\n", chars) + self.text.delete("1.0", "end") self.set_filename(None) self.text.insert("1.0", chars) self.reset_undo() self.set_filename(filename) - if converted: - # We need to save the conversion results first - # before being able to execute the code - self.set_saved(False) self.text.mark_set("insert", "1.0") self.text.yview("insert") self.updaterecentfileslist(filename) return True - def _decode(self, two_lines, bytes): - "Create a Unicode string." - chars = None + def decode(self, chars): + """Create a Unicode string + + If that fails, let Tcl try its best + """ # Check presence of a UTF-8 signature first - if bytes.startswith(BOM_UTF8): + if chars.startswith(BOM_UTF8): try: - chars = bytes[3:].decode("utf-8") - except UnicodeDecodeError: + chars = chars[3:].decode("utf-8") + except UnicodeError: # has UTF-8 signature, but fails to decode... - return None, False + return chars else: # Indicates that this file originally had a BOM - self.fileencoding = 'BOM' - return chars, False + self.fileencoding = BOM_UTF8 + return chars # Next look for coding specification try: - enc = coding_spec(two_lines) + enc = coding_spec(chars) except LookupError as name: tkMessageBox.showerror( title="Error loading the file", @@ -269,49 +299,24 @@ class IOBinding: "installation. The file may not display correctly" % name, parent = self.text) enc = None - except UnicodeDecodeError: - return None, False if enc: try: - chars = str(bytes, enc) - self.fileencoding = enc - return chars, False - except UnicodeDecodeError: + return unicode(chars, enc) + except UnicodeError: pass - # Try ascii: - try: - chars = str(bytes, 'ascii') - self.fileencoding = None - return chars, False - except UnicodeDecodeError: - pass - # Try utf-8: + # If it is ASCII, we need not to record anything try: - chars = str(bytes, 'utf-8') - self.fileencoding = 'utf-8' - return chars, False - except UnicodeDecodeError: + return unicode(chars, 'ascii') + except UnicodeError: pass # Finally, try the locale's encoding. This is deprecated; # the user should declare a non-ASCII encoding try: - # Wait for the editor window to appear - self.editwin.text.update() - enc = askstring( - "Specify file encoding", - "The file's encoding is invalid for Python 3.x.\n" - "IDLE will convert it to UTF-8.\n" - "What is the current encoding of the file?", - initialvalue = encoding, - parent = self.editwin.text) - - if enc: - chars = str(bytes, enc) - self.fileencoding = None - return chars, True - except (UnicodeDecodeError, LookupError): + chars = unicode(chars, encoding) + self.fileencoding = encoding + except UnicodeError: pass - return None, False # None on failure + return chars def maybesave(self): if self.get_saved(): @@ -371,67 +376,99 @@ class IOBinding: return "break" def writefile(self, filename): - text = self.fixnewlines() - chars = self.encode(text) + self.fixlastline() + chars = self.encode(self.text.get("1.0", "end-1c")) + if self.eol_convention != "\n": + chars = chars.replace("\n", self.eol_convention) try: with open(filename, "wb") as f: f.write(chars) f.flush() os.fsync(f.fileno()) return True - except OSError as msg: + except IOError as msg: tkMessageBox.showerror("I/O Error", str(msg), parent=self.text) return False - def fixnewlines(self): - "Return text with final \n if needed and os eols." - if (self.text.get("end-2c") != '\n' - and not hasattr(self.editwin, "interp")): # Not shell. - self.text.insert("end-1c", "\n") - text = self.text.get("1.0", "end-1c") - if self.eol_convention != "\n": - text = text.replace("\n", self.eol_convention) - return text - def encode(self, chars): - if isinstance(chars, bytes): + if isinstance(chars, str): # This is either plain ASCII, or Tk was returning mixed-encoding # text to us. Don't try to guess further. return chars - # Preserve a BOM that might have been present on opening - if self.fileencoding == 'BOM': - return BOM_UTF8 + chars.encode("utf-8") # See whether there is anything non-ASCII in it. # If not, no need to figure out the encoding. try: return chars.encode('ascii') except UnicodeError: pass - # Check if there is an encoding declared + # If there is an encoding declared, try this first. try: - # a string, let coding_spec slice it to the first two lines enc = coding_spec(chars) failed = None except LookupError as msg: failed = msg enc = None - else: - if not enc: - # PEP 3120: default source encoding is UTF-8 - enc = 'utf-8' if enc: try: return chars.encode(enc) except UnicodeError: failed = "Invalid encoding '%s'" % enc - tkMessageBox.showerror( - "I/O Error", - "%s.\nSaving as UTF-8" % failed, - parent = self.text) - # Fallback: save as UTF-8, with BOM - ignoring the incorrect - # declared encoding - return BOM_UTF8 + chars.encode("utf-8") + if failed: + tkMessageBox.showerror( + "I/O Error", + "%s. Saving as UTF-8" % failed, + parent = self.text) + # If there was a UTF-8 signature, use that. This should not fail + if self.fileencoding == BOM_UTF8 or failed: + return BOM_UTF8 + chars.encode("utf-8") + # Try the original file encoding next, if any + if self.fileencoding: + try: + return chars.encode(self.fileencoding) + except UnicodeError: + tkMessageBox.showerror( + "I/O Error", + "Cannot save this as '%s' anymore. Saving as UTF-8" \ + % self.fileencoding, + parent = self.text) + return BOM_UTF8 + chars.encode("utf-8") + # Nothing was declared, and we had not determined an encoding + # on loading. Recommend an encoding line. + config_encoding = idleConf.GetOption("main","EditorWindow", + "encoding") + if config_encoding == 'utf-8': + # User has requested that we save files as UTF-8 + return BOM_UTF8 + chars.encode("utf-8") + ask_user = True + try: + chars = chars.encode(encoding) + enc = encoding + if config_encoding == 'locale': + ask_user = False + except UnicodeError: + chars = BOM_UTF8 + chars.encode("utf-8") + enc = "utf-8" + if not ask_user: + return chars + dialog = EncodingMessage(self.editwin.top, enc) + dialog.go() + if dialog.num == 1: + # User asked us to edit the file + encline = "# -*- coding: %s -*-\n" % enc + firstline = self.text.get("1.0", "2.0") + if firstline.startswith("#!"): + # Insert encoding after #! line + self.text.insert("2.0", encline) + else: + self.text.insert("1.0", encline) + return self.encode(self.text.get("1.0", "end-1c")) + return chars + + def fixlastline(self): + c = self.text.get("end-2c") + if c != '\n': + self.text.insert("end-1c", "\n") def print_window(self, event): confirm = tkMessageBox.askokcancel( @@ -465,7 +502,7 @@ class IOBinding: else: #no printing for this platform printPlatform = False if printPlatform: #we can try to print for this platform - command = command % shlex.quote(filename) + command = command % pipes.quote(filename) pipe = os.popen(command, "r") # things can get ugly on NT if there is no printer available. output = pipe.read().strip() @@ -486,11 +523,11 @@ class IOBinding: opendialog = None savedialog = None - filetypes = ( + filetypes = [ ("Python files", "*.py *.pyw", "TEXT"), ("Text files", "*.txt", "TEXT"), ("All files", "*"), - ) + ] defaultextension = '.py' if sys.platform == 'darwin' else '' @@ -500,6 +537,8 @@ class IOBinding: self.opendialog = tkFileDialog.Open(parent=self.text, filetypes=self.filetypes) filename = self.opendialog.show(initialdir=dir, initialfile=base) + if isinstance(filename, unicode): + filename = filename.encode(filesystemencoding) return filename def defaultfilename(self, mode="open"): @@ -510,7 +549,7 @@ class IOBinding: else: try: pwd = os.getcwd() - except OSError: + except os.error: pwd = "" return pwd, "" @@ -522,35 +561,38 @@ class IOBinding: filetypes=self.filetypes, defaultextension=self.defaultextension) filename = self.savedialog.show(initialdir=dir, initialfile=base) + if isinstance(filename, unicode): + filename = filename.encode(filesystemencoding) return filename def updaterecentfileslist(self,filename): "Update recent file list on all editor windows" - if self.editwin.flist: - self.editwin.update_recent_files_list(filename) + self.editwin.update_recent_files_list(filename) + def _io_binding(parent): # htest # - from tkinter import Toplevel, Text + from Tkinter import Toplevel, Text root = Toplevel(parent) root.title("Test IOBinding") - x, y = map(int, parent.geometry().split('+')[1:]) - root.geometry("+%d+%d" % (x, y + 175)) + 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-p>', self.print) + self.text.bind('<Control-p>', self.printer) self.text.bind("<Control-s>", self.save) self.text.bind("<Alt-s>", self.saveas) self.text.bind('<Control-c>', self.savecopy) def get_saved(self): return 0 def set_saved(self, flag): pass def reset_undo(self): pass + def update_recent_files_list(self, filename): pass def open(self, event): self.text.event_generate("<<open-window-from-file>>") - def print(self, event): + def printer(self, event): self.text.event_generate("<<print-window>>") def save(self, event): self.text.event_generate("<<save-window>>") @@ -566,8 +608,5 @@ def _io_binding(parent): # htest # IOBinding(editwin) if __name__ == "__main__": - from unittest import main - main('idlelib.idle_test.test_iomenu', verbosity=2, exit=False) - from idlelib.idle_test.htest import run run(_io_binding) diff --git a/Lib/idlelib/history.py b/Lib/idlelib/IdleHistory.py index ad44a96..078af29 100644 --- a/Lib/idlelib/history.py +++ b/Lib/idlelib/IdleHistory.py @@ -1,12 +1,11 @@ "Implement Idle Shell history mechanism with History class" -from idlelib.config import idleConf - +from idlelib.configHandler import idleConf class History: ''' Implement Idle Shell history mechanism. - store - Store source statement (called from pyshell.resetoutput). + store - Store source statement (called from PyShell.resetoutput). fetch - Fetch stored statement matching prefix already entered. history_next - Bound to <<history-next>> event (default Alt-N). history_prev - Bound to <<history-prev>> event (default Alt-P). @@ -39,7 +38,7 @@ class History: return "break" def fetch(self, reverse): - '''Fetch statement and replace current line in text widget. + '''Fetch statememt and replace current line in text widget. Set prefix and pointer as needed for successive fetches. Reset them to None, None when returning to the start line. @@ -100,7 +99,6 @@ class History: self.pointer = None self.prefix = None - if __name__ == "__main__": from unittest import main - main('idlelib.idle_test.test_history', verbosity=2, exit=False) + main('idlelib.idle_test.test_idlehistory', verbosity=2, exit=False) diff --git a/Lib/idlelib/multicall.py b/Lib/idlelib/MultiCall.py index dc02001..a157d7a 100644 --- a/Lib/idlelib/multicall.py +++ b/Lib/idlelib/MultiCall.py @@ -28,10 +28,11 @@ The order by which events are called is defined by these rules: unless this conflicts with the first rule. Each function will be called at most once for each event. """ -import re -import sys -import tkinter +import sys +import string +import re +import Tkinter # the event type constants, which define the meaning of mc_type MC_KEYPRESS=0; MC_KEYRELEASE=1; MC_BUTTONPRESS=2; MC_BUTTONRELEASE=3; @@ -56,12 +57,6 @@ _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: @@ -103,12 +98,7 @@ class _SimpleBinder: def __del__(self): if 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 + self.widget.unbind(self.widgetinst, self.sequence, self.handlerid) # An int in range(1 << len(_modifiers)) represents a combination of modifiers # (if the least significant bit is on, _modifiers[0] is on, and so on). @@ -237,11 +227,7 @@ class _ComplexBinder: def __del__(self): for seq, id in self.handlerids: - try: - self.widget.unbind(self.widgetinst, seq, id) - except tkinter.TclError as e: - if not APPLICATION_GONE in e.args[0]: - raise + self.widget.unbind(self.widgetinst, seq, id) # define the list of event types to be handled by MultiEvent. the order is # compatible with the definition of event type constants. @@ -272,16 +258,19 @@ def _parse_sequence(sequence): """ if not sequence or sequence[0] != '<' or sequence[-1] != '>': return None - words = sequence[1:-1].split('-') + words = string.split(sequence[1:-1], '-') + modifiers = 0 while words and words[0] in _modifier_names: modifiers |= 1 << _modifier_names[words[0]] del words[0] + if words and words[0] in _type_names: type = _type_names[words[0]] del words[0] else: return None + if _binder_classes[type] is _SimpleBinder: if modifiers or words: return None @@ -320,7 +309,7 @@ def MultiCallCreator(widget): return _multicall_dict[widget] class MultiCall (widget): - assert issubclass(widget, tkinter.Misc) + assert issubclass(widget, Tkinter.Misc) def __init__(self, *args, **kwargs): widget.__init__(self, *args, **kwargs) @@ -332,8 +321,7 @@ def MultiCallCreator(widget): for i in range(len(_types))] def bind(self, sequence=None, func=None, add=None): - #print("bind(%s, %s, %s)" % (sequence, func, add), - # file=sys.__stderr__) + #print "bind(%s, %s, %s) called." % (sequence, func, add) if type(sequence) is str and len(sequence) > 2 and \ sequence[:2] == "<<" and sequence[-2:] == ">>": if sequence in self.__eventinfo: @@ -361,8 +349,7 @@ def MultiCallCreator(widget): return widget.unbind(self, sequence, funcid) def event_add(self, virtual, *sequences): - #print("event_add(%s, %s)" % (repr(virtual), repr(sequences)), - # file=sys.__stderr__) + #print "event_add(%s,%s) was called"%(repr(virtual),repr(sequences)) if virtual not in self.__eventinfo: self.__eventinfo[virtual] = [None, []] @@ -370,7 +357,7 @@ def MultiCallCreator(widget): for seq in sequences: triplet = _parse_sequence(seq) if triplet is None: - #print("Tkinter event_add(%s)" % seq, file=sys.__stderr__) + #print >> sys.stderr, "Seq. %s was added by Tkinter."%seq widget.event_add(self, virtual, seq) else: if func is not None: @@ -384,7 +371,7 @@ def MultiCallCreator(widget): for seq in sequences: triplet = _parse_sequence(seq) if triplet is None: - #print("Tkinter event_delete: %s" % seq, file=sys.__stderr__) + #print >> sys.stderr, "Seq. %s was deleted by Tkinter."%seq widget.event_delete(self, virtual, seq) else: if func is not None: @@ -404,26 +391,23 @@ def MultiCallCreator(widget): func, triplets = self.__eventinfo[virtual] if func: for triplet in triplets: - try: - self.__binders[triplet[1]].unbind(triplet, func) - except tkinter.TclError as e: - if not APPLICATION_GONE in e.args[0]: - raise + self.__binders[triplet[1]].unbind(triplet, func) + _multicall_dict[widget] = MultiCall return MultiCall -def _multi_call(parent): # htest # - top = tkinter.Toplevel(parent) - top.title("Test MultiCall") - x, y = map(int, parent.geometry().split('+')[1:]) - top.geometry("+%d+%d" % (x, y + 175)) - text = MultiCallCreator(tkinter.Text)(top) +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]): def handler(event): - print(seq) + print seq text.bind("<<handler%d>>"%n[0], handler) text.event_add("<<handler%d>>"%n[0], seq) n[0] += 1 @@ -439,10 +423,8 @@ def _multi_call(parent): # htest # bindseq("<FocusOut>") bindseq("<Enter>") bindseq("<Leave>") + root.mainloop() if __name__ == "__main__": - from unittest import main - main('idlelib.idle_test.test_mainmenu', verbosity=2, exit=False) - from idlelib.idle_test.htest import run run(_multi_call) diff --git a/Lib/idlelib/MultiStatusBar.py b/Lib/idlelib/MultiStatusBar.py new file mode 100644 index 0000000..e3d59ee --- /dev/null +++ b/Lib/idlelib/MultiStatusBar.py @@ -0,0 +1,47 @@ +from Tkinter import * + +class MultiStatusBar(Frame): + + def __init__(self, master=None, **kw): + if master is None: + master = Tk() + Frame.__init__(self, master, **kw) + self.labels = {} + + def set_label(self, name, text='', side=LEFT, width=0): + if name not in self.labels: + 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 _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__': + from idlelib.idle_test.htest import run + run(_multistatus_bar) diff --git a/Lib/idlelib/NEWS.txt b/Lib/idlelib/NEWS.txt index 304cf63..d4560b8 100644 --- a/Lib/idlelib/NEWS.txt +++ b/Lib/idlelib/NEWS.txt @@ -1,265 +1,18 @@ -What's New in IDLE 3.9.0 (since 3.8.0) -Released on 2020-10-05? -====================================== +Since 2.7.13, only severe bugs are fixed on the 2.7 branch. - -bpo-38943: Fix autocomplete windows not always appearing on some -systems. Patch by Johnny Najera. - -bpo-38944: Excape key now closes IDLE completion windows. Patch by -Johnny Najera. - -bpo-38862: 'Strip Trailing Whitespace' on the Format menu removes extra -newlines at the end of non-shell files. - -bpo-38636: Fix IDLE Format menu tab toggle and file indent width. These -functions (default shortcuts Alt-T and Alt-U) were mistakenly disabled -in 3.7.5 and 3.8.0. - -bpo-4360: Add an option to toggle IDLE's cursor blink for shell, -editor, and output windows. See Settings, General, Window Preferences, -Cursor Blink. Patch by Zachary Spytz. - -bpo-26353: Stop adding newline when saving an IDLE shell window. - -bpo-38598: Do not try to compile IDLE shell or output windows. - - -What's New in IDLE 3.8.0 (since 3.7.0) -Released on 2019-10-14 -====================================== - -bpo-36698: IDLE no longer fails when writing non-encodable characters -to stderr. It now escapes them with a backslash, like the regular -Python interpreter. Add an errors field to the standard streams. - -bpo-13153: Improve tkinter's handing of non-BMP (astral) unicode -characters, such as 'rocket \U0001f680'. Whether a proper glyph or -replacement char is displayed depends on the OS and font. For IDLE, -astral chars in code interfere with editing. - -bpo-35379: When exiting IDLE, catch any AttributeError. One happens -when EditorWindow.close is called twice. Printing a traceback, when -IDLE is run from a terminal, is useless and annoying. - -bpo-38183: To avoid test issues, test_idle ignores the user config -directory. It no longer tries to create or access .idlerc or any files -within. Users must run IDLE to discover problems with saving settings. - -bpo-38077: IDLE no longer adds 'argv' to the user namespace when -initializing it. This bug only affected 3.7.4 and 3.8.0b2 to 3.8.0b4. - -bpo-38401: Shell restart lines now fill the window width, always start -with '=', and avoid wrapping unnecessarily. The line will still wrap -if the included file name is long relative to the width. - -bpo-37092: Add mousewheel scrolling for IDLE module, path, and stack -browsers. Patch by George Zhang. - -bpo-35771: To avoid occasional spurious test_idle failures on slower -machines, increase the ``hover_delay`` in test_tooltip. - -bpo-37824: Properly handle user input warnings in IDLE shell. -Cease turning SyntaxWarnings into SyntaxErrors. - -bpo-37929: IDLE Settings dialog now closes properly when there is no -shell window. - -bpo-37849: Fix completions list appearing too high or low when shown -above the current line. - -bpo-36419: Refactor autocompete and improve testing. - -bpo-37748: Reorder the Run menu. Put the most common choice, -Run Module, at the top. - -bpo-37692: Improve highlight config sample with example shell -interaction and better labels for shell elements. - -bpo-37628: Settings dialog no longer expands with font size. -The font and highlight sample boxes gain scrollbars instead. - -bpo-17535: Add optional line numbers for IDLE editor windows. - -bpo-37627: Initialize the Customize Run dialog with the command line -arguments most recently entered before. The user can optionally edit -before submitting them. - -bpo-33610: Code context always shows the correct context when toggled on. - -bpo-36390: Gather Format menu functions into format.py. Combine -paragraph.py, rstrip.py, and format methods from editor.py. - -bpo-37530: Optimize code context to reduce unneeded background activity. -Font and highlight changes now occur along with text changes instead -of after a random delay. - -bpo-27452: Cleanup config.py by inlining RemoveFile and simplifying -the handling of __file__ in CreateConfigHandlers/ - -bpo-26806: To compensate for stack frames added by IDLE and avoid -possible problems with low recursion limits, add 30 to limits in the -user code execution process. Subtract 30 when reporting recursion -limits to make this addition mostly transparent. - -bpo-37325: Fix tab focus traversal order for help source and custom -run dialogs. - -bpo-37321: Both subprocess connection error messages now refer to -the 'Startup failure' section of the IDLE doc. - -bpo-37177: Properly attach search dialogs to their main window so -that they behave like other dialogs and do not get hidden behind -their main window. - -bpo-37039: Adjust "Zoom Height" to individual screens by momentarily -maximizing the window on first use with a particular screen. Changing -screen settings may invalidate the saved height. While a window is -maximized, "Zoom Height" has no effect. - -bpo-35763: Make calltip reminder about '/' meaning positional-only less -obtrusive by only adding it when there is room on the first line. - -bpo-5680: Add 'Run Customized' to the Run menu to run a module with -customized settings. Any command line arguments entered are added -to sys.argv. One can suppress the normal Shell main module restart. - -bpo-35610: Replace now redundant editor.context_use_ps1 with -.prompt_last_line. This finishes change started in bpo-31858. - -bpo-32411: Stop sorting dict created with desired line order. - -bpo-37038: Make idlelib.run runnable; add test clause. - -bpo-36958: Print any argument other than None or int passed to -SystemExit or sys.exit(). +What's New in IDLE 2.7.17? +========================== +*Release date: 2019-07-??* bpo-36807: When saving a file, call file.flush() and os.fsync() so bits are flushed to e.g. a USB drive. -bpo-36429: Fix starting IDLE with pyshell. -Add idlelib.pyshell alias at top; remove pyshell alias at bottom. -Remove obsolete __name__=='__main__' command. - -bpo-30348: Increase test coverage of idlelib.autocomplete by 30%. -Patch by Louie Lu. - -bpo-23205: Add tests and refactor grep's findfiles. - -bpo-36405: Use dict unpacking in idlelib. - -bpo-36396: Remove fgBg param of idlelib.config.GetHighlight(). -This param was only used twice and changed the return type. - -bpo-23216: IDLE: Add docstrings to search modules. - -bpo-36176: Fix IDLE autocomplete & calltip popup colors. -Prevent conflicts with Linux dark themes -(and slightly darken calltip background). - -bpo-36152: Remove colorizer.ColorDelegator.close_when_done and the -corresponding argument of .close(). In IDLE, both have always been -None or False since 2007. - -bpo-36096: Make colorizer state variables instance-only. - -bpo-32129: Avoid blurry IDLE application icon on macOS with Tk 8.6. -Patch by Kevin Walzer. - -bpo-24310: Document settings dialog font tab sample. - -bpo-35689: Add docstrings and tests for colorizer. - -bpo-35833: Revise IDLE doc for control codes sent to Shell. -Add a code example block. - -bpo-35770: IDLE macosx deletes Options => Configure IDLE. -It previously deleted Window => Zoom Height by mistake. -(Zoom Height is now on the Options menu). On Mac, the settings -dialog is accessed via Preferences on the IDLE menu. - -bpo-35769: Change new file name from 'Untitled' to 'untitled'. - -bpo-35660: Fix imports in window module. - -bpo-35641: Properly format calltip for function without docstring. -bpo-33987: Use ttk Frame for ttk widgets. +What's New in IDLE 2.7.16? +========================== +*Release date: 2019-03-02* -bpo-34055: Fix erroneous 'smart' indents and newlines in IDLE Shell. - -bpo-28097: Add Previous/Next History entries to Shell menu. - -bpo-35591: Find Selection now works when selection not found. - -bpo-35598: Update config_key: use PEP 8 names and ttk widgets, -make some objects global, and add tests. - -bpo-35196: Speed up squeezer line counting. - -bpo-35208: Squeezer now counts wrapped lines before newlines. - -bpo-35555: Gray out Code Context menu entry when it's not applicable. - -bpo-22703: Improve the Code Context and Zoom Height menu labels. -The Code Context menu label now toggles between Show/Hide Code Context. -The Zoom Height menu now toggles between Zoom/Restore Height. -Zoom Height has moved from the Window menu to the Options menu. - -bpo-35521: Document the editor code context feature. -Add some internal references within the IDLE doc. - -bpo-34864: When starting IDLE on MacOS, warn if the system setting -"Prefer tabs when opening documents" is "Always". As previous -documented for this issue, running IDLE with this setting causes -problems. If the setting is changed while IDLE is running, -there will be no warning until IDLE is restarted. - -bpo-35213: Where appropriate, use 'macOS' in idlelib. - -bpo-34864: Document two IDLE on MacOS issues. The System Preferences -Dock "prefer tabs always" setting disables some IDLE features. -Menus are a bit different than as described for Windows and Linux. - -bpo-35202: Remove unused imports in idlelib. - -bpo-33000: Document that IDLE's shell has no line limit. -A program that runs indefinitely can overfill memory. - -bpo-23220: Explain how IDLE's Shell displays output. -Add new subsection "User output in Shell". - -bpo-35099: Improve the doc about IDLE running user code. -"IDLE -- console differences" is renamed "Running user code". -It mostly covers the implications of using custom sys.stdxxx objects. - -bpo-35097: Add IDLE doc subsection explaining editor windows. -Topics include opening, title and status bars, .py* extension, and running. - -Issue 35093: Document the IDLE document viewer in the IDLE doc. -Add a paragraph in "Help and preferences", "Help sources" subsection. - -bpo-1529353: Explain Shell text squeezing in the IDLE doc. - -bpo-35088: Update idlelib.help.copy_string docstring. -We now use git and backporting instead of hg and forward merging. - -bpo-35087: Update idlelib help files for the current doc build. -The main change is the elimination of chapter-section numbers. - -bpo-1529353: Output over N lines (50 by default) is squeezed down to a button. -N can be changed in the PyShell section of the General page of the -Settings dialog. Fewer, but possibly extra long, lines can be squeezed by -right clicking on the output. Squeezed output can be expanded in place -by double-clicking the button or into the clipboard or a separate window -by right-clicking the button. - -bpo-34548: Use configured color theme for read-only text views. - -bpo-33839: Refactor ToolTip and CallTip classes; add documentation -and tests. - -bpo-34047: Fix mouse wheel scrolling direction on macOS. +bpo-31500: Default fonts now are scaled on HiDPI displays. bpo-34275: Make calltips always visible on Mac. Patch by Kevin Walzer. @@ -267,428 +20,22 @@ Patch by Kevin Walzer. bpo-34120: Fix freezing after closing some dialogs on Mac. This is one of multiple regressions from using newer tcl/tk. -bpo-33975: Avoid small type when running htests. -Since part of the purpose of human-viewed tests is to determine that -widgets look right, it is important that they look the same for -testing as when running IDLE. - -bpo-33905: Add test for idlelib.stackview.StackBrowser. - -bpo-33924: Change mainmenu.menudefs key 'windows' to 'window'. -Every other menudef key is the lowercase version of the -corresponding main menu entry (in this case, 'Window'). - -bpo-33906: Rename idlelib.windows as window -Match Window on the main menu and remove last plural module name. -Change imports, test, and attribute references to match new name. - -bpo-33917: Fix and document idlelib/idle_test/template.py. -The revised file compiles, runs, and tests OK. idle_test/README.txt -explains how to use it to create new IDLE test files. - -bpo-33904: In rstrip module, rename class RstripExtension as Rstrip. -bpo-33907: For consistency and clarity, rename calltip objects. -Module calltips and its class CallTips are now calltip and Calltip. -In module calltip_w, class CallTip is now CalltipWindow. +What's New in IDLE 2.7.13? +========================== +*Release date: 2016-12-17* -bpo-33855: Minimally test all IDLE modules. -Standardize the test file format. Add missing test files that import -the tested module and perform at least one test. Check and record the -coverage of each test. +- Issue #27854: Make Help => IDLE Help work again on Windows. + Include idlelib/help.html in 2.7 Windows installer. -bpo-33856: Add 'help' to Shell's initial welcome message. - - -What's New in IDLE 3.7.0 (since 3.6.0) -Released on 2018-06-27 -====================================== - -bpo-33656: On Windows, add API call saying that tk scales for DPI. -On Windows 8.1+ or 10, with DPI compatibility properties of the Python -binary unchanged, and a monitor resolution greater than 96 DPI, this -should make text and lines sharper and some colors brighter. -On other systems, it should have no effect. If you have a custom theme, -you may want to adjust a color or two. If perchance it make text worse -on your monitor, you can disable the ctypes.OleDLL call near the top of -pyshell.py and report the problem on python-list or idle-dev@python.org. - -bpo-33768: Clicking on a context line moves that line to the top -of the editor window. - -bpo-33763: Replace the code context label widget with a text widget. - -bpo-33664: Scroll IDLE editor text by lines. -(Previously, the mouse wheel and scrollbar slider moved text by a fixed -number of pixels, resulting in partial lines at the top of the editor -box.) This change also applies to the shell and grep output windows, -but currently not to read-only text views. - -bpo-33679: Enable theme-specific color configuration for Code Context. -(Previously, there was one code context foreground and background font -color setting, default or custom, on the extensions tab, that applied -to all themes.) For built-in themes, the foreground is the same as -normal text and the background is a contrasting gray. Context colors for -custom themes are set on the Hightlights tab along with other colors. -When one starts IDLE from a console and loads a custom theme without -definitions for 'context', one will see a warning message on the -console. - -bpo-33642: Display up to maxlines non-blank lines for Code Context. -If there is no current context, show a single blank line. (Previously, -the Code Contex had numlines lines, usually with some blank.) The use -of a new option, 'maxlines' (default 15), avoids possible interference -with user settings of the old option, 'numlines' (default 3). - -bpo-33628: Cleanup codecontext.py and its test. - -bpo-32831: Add docstrings and tests for codecontext.py. -Coverage is 100%. Patch by Cheryl Sabella. - -bpo-33564: Code context now recognizes async as a block opener. - -bpo-21474: Update word/identifier definition from ascii to unicode. -In text and entry boxes, this affects selection by double-click, -movement left/right by control-left/right, and deletion left/right -by control-BACKSPACE/DEL. - -bpo-33204: Consistently color invalid string prefixes. -A 'u' string prefix cannot be paired with either 'r' or 'f'. -IDLE now consistently colors as much of the prefix, starting at the -right, as is valid. Revise and extend colorizer test. - -bpo-32984: Set __file__ while running a startup file. -Like Python, IDLE optionally runs 1 startup file in the Shell window -before presenting the first interactive input prompt. For IDLE, -option -s runs a file named in environmental variable IDLESTARTUP or -PYTHONSTARTUP; -r file runs file. Python sets __file__ to the startup -file name before running the file and unsets it before the first -prompt. IDLE now does the same when run normally, without the -n -option. - -bpo-32940: Replace StringTranslatePseudoMapping with faster code. - -bpo-32916: Change 'str' to 'code' in idlelib.pyparse and users. - -bpo-32905: Remove unused code in pyparse module. - -bpo-32874: IDLE - add pyparse tests with 97% coverage. - -bpo-32837: IDLE - require encoding argument for textview.view_file. -Using the system and place-dependent default encoding for open() -is a bad idea for IDLE's system and location-independent files. - -bpo-32826: Add "encoding=utf-8" to open() in IDLE's test_help_about. -GUI test test_file_buttons() only looks at initial ascii-only lines, -but failed on systems where open() defaults to 'ascii' because -readline() internally reads and decodes far enough ahead to encounter -a non-ascii character in CREDITS.txt. - -bpo-32765: Update configdialog General tab create page docstring. -Add new widgets to the widget list. - -bpo-32207: Improve tk event exception tracebacks in IDLE. -When tk event handling is driven by IDLE's run loop, a confusing -and distracting queue.EMPTY traceback context is no longer added -to tk event exception tracebacks. The traceback is now the same -as when event handling is driven by user code. Patch based on -a suggestion by Serhiy Storchaka. - -bpo-32164: Delete unused file idlelib/tabbedpages.py. -Use of TabbedPageSet in configdialog was replaced by ttk.Notebook. - -bpo-32100: Fix old and new bugs in pathbrowser; improve tests. -Patch mostly by Cheryl Sabella. - -bpo-31860: The font sample in the settings dialog is now editable. -Edits persist while IDLE remains open. -Patch by Serhiy Storchake and Terry Jan Reedy. - -bpo-31858: Restrict shell prompt manipulation to the shell. -Editor and output windows only see an empty last prompt line. This -simplifies the code and fixes a minor bug when newline is inserted. -Sys.ps1, if present, is read on Shell start-up, but is not set or changed. -Patch by Terry Jan Reedy. - -bpo-28603: Fix a TypeError that caused a shell restart when printing -a traceback that includes an exception that is unhashable. -Patch by Zane Bitter. - -bpo-13802: Use non-Latin characters in the Font settings sample. -Even if one selects a font that defines a limited subset of the unicode -Basic Multilingual Plane, tcl/tk will use other fonts that define a -character. The expanded example give users of non-Latin characters -a better idea of what they might see in the shell and editors. - -To make room for the expanded sample, frames on the Font tab are -re-arranged. The Font/Tabs help explains a bit about the additions. -Patch by Terry Jan Reedy - -bpo-31460: Simplify the API of IDLE's Module Browser. -Passing a widget instead of an flist with a root widget opens the -option of creating a browser frame that is only part of a window. -Passing a full file name instead of pieces assumed to come from a -.py file opens the possibility of browsing python files that do not -end in .py. - -bpo-31649: Make _htest and _utest parameters keyword-only. -These are used to adjust code for human and unit tests. - -bpo-31459: Rename module browser from Class Browser to Module Browser. -The original module-level class and method browser became a module -browser, with the addition of module-level functions, years ago. -Nested classes and functions were added yesterday. For back- -compatibility, the virtual event <<open-class-browser>>, which -appears on the Keys tab of the Settings dialog, is not changed. -Patch by Cheryl Sabella. - -bpo-1612262: Module browser now shows nested classes and functions. -Original patches for code and tests by Guilherme Polo and -Cheryl Sabella, respectively. Revisions by Terry Jan Reedy. - -bpo-31500: Tk's default fonts now are scaled on HiDPI displays. -This affects all dialogs. Patch by Serhiy Storchaka. - -bpo-31493: Fix code context update and font update timers. -Canceling timers prevents a warning message when test_idle completes. - -bpo-31488: Update non-key options in former extension classes. -When applying configdialog changes, call .reload for each feature class. -Change ParenMatch so updated options affect existing instances attached -to existing editor windows. - -bpo-31477: Improve rstrip entry in IDLE doc. -Strip Trailing Whitespace strips more than blank spaces. -Multiline string literals are not skipped. - -bpo-31480: fix tests to pass with zzdummy extension disabled. (#3590) -To see the example in action, enable it on options extensions tab. - -bpo-31421: Document how IDLE runs tkinter programs. -IDLE calls tcl/tk update in the background in order to make live -interaction and experimentation with tkinter applications much easier. - -bpo-31414: Fix tk entry box tests by deleting first. -Adding to an int entry is not the same as deleting and inserting -because int('') will fail. Patch by Terry Jan Reedy. - -bpo-27099: Convert IDLE's built-in 'extensions' to regular features. - About 10 IDLE features were implemented as supposedly optional -extensions. Their different behavior could be confusing or worse for -users and not good for maintenance. Hence the conversion. - The main difference for users is that user configurable key bindings -for builtin features are now handled uniformly. Now, editing a binding -in a keyset only affects its value in the keyset. All bindings are -defined together in the system-specific default keysets in config- -extensions.def. All custom keysets are saved as a whole in config- -extension.cfg. All take effect as soon as one clicks Apply or Ok. - The affected events are '<<force-open-completions>>', -'<<expand-word>>', '<<force-open-calltip>>', '<<flash-paren>>', -'<<format-paragraph>>', '<<run-module>>', '<<check-module>>', and -'<<zoom-height>>'. Any (global) customizations made before 3.6.3 will -not affect their keyset-specific customization after 3.6.3. and vice -versa. - Initial patch by Charles Wohlganger, revised by Terry Jan Reedy. - -bpo-31051: Rearrange condigdialog General tab. -Sort non-Help options into Window (Shell+Editor) and Editor (only). -Leave room for the addition of new options. -Patch by Terry Jan Reedy. - -bpo-30617: Add docstrings and tests for outwin subclass of editor. -Move some data and functions from the class to module level. -Patch by Cheryl Sabella. - -bpo-31287: Do not modify tkinter.messagebox in test_configdialog. -Instead, mask it with an instance mock that can be deleted. -Patch by Terry Jan Reedy. - -bpo-30781: Use ttk widgets in ConfigDialog pages. -These should especially look better on MacOSX. -Patches by Terry Jan Reedy and Cheryl Sabella. - -bpo-31206: Factor HighPage(Frame) class from ConfigDialog. -Patch by Cheryl Sabella. - -bp0-31001: Add tests for configdialog highlight tab. -Patch by Cheryl Sabella. - -bpo-31205: Factor KeysPage(Frame) class from ConfigDialog. -The slightly modified tests continue to pass. -Patch by Cheryl Sabella. - -bpo-31002: Add tests for configdialog keys tab. -Patch by Cheryl Sabella. - -bpo-19903: Change calltipes to use inspect.signature. -Idlelib.calltips.get_argspec now uses inspect.signature instead of -inspect.getfullargspec, like help() does. This improves the signature -in the call tip in a few different cases, including builtins converted -to provide a signature. A message is added if the object is not -callable, has an invalid signature, or if it has positional-only -parameters. Patch by Louie Lu. - -bop-31083: Add an outline of a TabPage class in configdialog. -Add template as comment. Update existing classes to match outline. -Initial patch by Cheryl Sabella. - -bpo-31050: Factor GenPage(Frame) class from ConfigDialog. -The slightly modified tests for the General tab continue to pass. -Patch by Cheryl Sabella. - -bpo-31004: Factor FontPage(Frame) class from ConfigDialog. -The slightly modified tests continue to pass. The General test -broken by the switch to ttk.Notebook is fixed. -Patch mostly by Cheryl Sabella. - -bpo-30781: IDLE - Use ttk Notebook in ConfigDialog. -This improves navigation by tabbing. -Patch by Terry Jan Reedy. - -bpo-31060: IDLE - Finish rearranging methods of ConfigDialog. -Grouping methods pertaining to each tab and the buttons will aid -writing tests and improving the tabs and will enable splitting the -groups into classes. -Patch by Terry Jan Reedy. - -bpo-30853: IDLE -- Factor a VarTrace class out of ConfigDialog. -Instance tracers manages pairs consisting of a tk variable and a -callback function. When tracing is turned on, setting the variable -calls the function. Test coverage for the new class is 100%. -Patch by Terry Jan Reedy. - -bpo-31003: IDLE: Add more tests for General tab. -Patch by Terry Jan Reedy. - -bpo-30993: IDLE - Improve configdialog font page and tests. -*In configdialog: Document causal pathways in create_font_tab -docstring. Simplify some attribute names. Move set_samples calls to -var_changed_font (idea from Cheryl Sabella). Move related functions to -positions after the create widgets function. -* In test_configdialog: Fix test_font_set so not order dependent. Fix -renamed test_indent_scale so it tests the widget. Adjust tests for -movement of set_samples call. Add tests for load functions. Put all -font tests in one class and tab indent tests in another. Except for -two lines, these tests completely cover the related functions. -Patch by Terry Jan Reedy. - -bpo-30981: IDLE -- Add more configdialog font page tests. - -bpo-28523: IDLE: replace 'colour' with 'color' in configdialog. - -bpo-30917: Add tests for idlelib.config.IdleConf. -Increase coverage from 46% to 96%. -Patch by Louie Lu. - -bpo-30913: Document ConfigDialog tk Vars, methods, and widgets in docstrings -This will facilitate improving the dialog and splitting up the class. -Original patch by Cheryl Sabella. - -bpo-30899: Add tests for ConfigParser subclasses in config. -Coverage is 100% for those classes and ConfigChanges. -Patch by Louie Lu. - -bpo-30881: Add docstrings to browser.py. -Patch by Cheryl Sabella. - -bpo-30851: Remove unused tk variables in configdialog. -One is a duplicate, one is set but cannot be altered by users. -Patch by Cheryl Sabella. - -bpo-30870: Select font option with Up and Down keys, as well as with mouse. -Added test increases configdialog coverage to 60% -Patches mostly by Louie Lu. - -bpo-8231: Call config.IdleConf.GetUserCfgDir only once per process. - -bpo-30779: Factor ConfigChanges class from configdialog, put in config; test. -* In config, put dump test code in a function; run it and unittest in - 'if __name__ == '__main__'. -* Add class config.ConfigChanges based on changes_class_v4.py on bpo issue. -* Add class test_config.ChangesTest, partly using configdialog_tests_v1.py. -* Revise configdialog to use ConfigChanges; see tracker msg297804. -* Revise test_configdialog to match configdialog changes. -* Remove configdialog functions unused or moved to ConfigChanges. -Cheryl Sabella contributed parts of the patch. - -bpo-30777: Configdialog - add docstrings and improve comments. -Patch by Cheryl Sabella. - -bpo-30495: Improve textview with docstrings, PEP8 names, and more tests. -Split TextViewer class into ViewWindow, ViewFrame, and TextFrame classes -so that instances of the latter two can be placed with other widgets -within a multiframe window. -Patches by Cheryl Sabella and Terry Jan Reedy. - -bpo-30723: Make several improvements to parenmatch. -* Add 'parens' style to highlight both opener and closer. -* Make 'default' style, which is not default, a synonym for 'opener'. -* Make time-delay work the same with all styles. -* Add help for config dialog extensions tab, including parenmatch. -* Add new tests. -Original patch by Charles Wohlganger. Revisions by Terry Jan Reedy - -bpo-30674: Grep -- Add docstrings. Patch by Cheryl Sabella. - -bpo-21519: IDLE's basic custom key entry dialog now detects -duplicates properly. Original patch by Saimadhav Heblikar. - -bpo-29910: IDLE no longer deletes a character after commenting out a -region by a key shortcut. Add "return 'break'" for this and other -potential conflicts between IDLE and default key bindings. -Patch by Serhiy Storchaka. - -bpo-30728: Modernize idlelib.configdialog: -* replace import * with specific imports; -* lowercase method and attribute lines. -Patch by Cheryl Sabella. - -bpo-6739: Verify user-entered key sequences by trying to bind them -with to a tk widget. Add tests for all 3 validation functions. -Original patch by G Polo. Tests added by Cheryl Sabella. -Code revised and more tests added by Terry Jan Reedy - -bpo-24813: Add icon to help_about and make other changes. - -bpo-15786: Fix several problems with IDLE's autocompletion box. -The following should now work: clicking on selection box items; -using the scrollbar; selecting an item by hitting Return. -Hangs on MacOSX should no longer happen. Patch by Louie Lu. - -bpo-25514: Add doc subsubsection about IDLE failure to start. -Popup no-connection message directs users to this section. - -bpo-30642: Fix reference leaks in IDLE tests. -Patches by Louie Lu and Terry Jan Reedy. - -bpo-30495: Add docstrings for textview.py and use PEP8 names. -Patches by Cheryl Sabella and Terry Jan Reedy. - -bpo-30290: Help-about: use pep8 names and add tests. -Increase coverage to 100%. -Patches by Louie Lu, Cheryl Sabella, and Terry Jan Reedy. - -bpo-30303: Add _utest option to textview; add new tests. -Increase coverage to 100%. -Patches by Louie Lu and Terry Jan Reedy. - -Issue #29071: IDLE colors f-string prefixes but not invalid ur prefixes. - -Issue #28572: Add 10% to coverage of IDLE's test_configdialog. -Update and augment description of the configuration system. - - -What's New in IDLE 3.6.0 (since 3.5.0) -Released on 2016-12-23 -====================================== +- Issue #25507: Add back import needed for 2.x encoding warning box. + Add pointer to 'Encoding declaration' in Language Reference. - Issue #15308: Add 'interrupt execution' (^C) to Shell menu. Patch by Roger Serwy, updated by Bayard Randel. - Issue #27922: Stop IDLE tests from 'flashing' gui widgets on the screen. -- Issue #27891: Consistently group and sort imports within idlelib modules. - - Issue #17642: add larger font sizes for classroom projection. - Add version to title of IDLE help window. @@ -696,145 +43,55 @@ Released on 2016-12-23 - Issue #25564: In section on IDLE -- console differences, mention that using exec means that __builtins__ is defined for each statement. -- Issue #27821: Fix 3.6.0a3 regression that prevented custom key sets - from being selected when no custom theme was defined. - - Issue #27714: text_textview and test_autocomplete now pass when re-run in the same process. This occurs when test_idle fails when run with the -w option but without -jn. Fix warning from test_config. -- Issue #27621: Put query response validation error messages in the query - box itself instead of in a separate messagebox. Redo tests to match. - Add Mac OSX refinements. Original patch by Mark Roseman. - -- Issue #27620: Escape key now closes Query box as cancelled. - -- Issue #27609: IDLE: tab after initial whitespace should tab, not - autocomplete. This fixes problem with writing docstrings at least - twice indented. - -- Issue #27609: Explicitly return None when there are also non-None - returns. In a few cases, reverse a condition and eliminate a return. - -- Issue #25507: IDLE no longer runs buggy code because of its tkinter imports. - Users must include the same imports required to run directly in Python. - -- Issue #27173: Add 'IDLE Modern Unix' to the built-in key sets. - Make the default key set depend on the platform. - Add tests for the changes to the config module. - - Issue #27452: add line counter and crc to IDLE configHandler test dump. -- Issue #27477: IDLE search dialogs now use ttk widgets. - -- Issue #27173: Add 'IDLE Modern Unix' to the built-in key sets. - Make the default key set depend on the platform. - Add tests for the changes to the config module. - -- Issue #27452: make command line "idle-test> python test_help.py" work. - __file__ is relative when python is started in the file's directory. - -- Issue #27452: add line counter and crc to IDLE configHandler test dump. - -- Issue #27380: IDLE: add query.py with base Query dialog and ttk widgets. - Module had subclasses SectionName, ModuleName, and HelpSource, which are - used to get information from users by configdialog and file =>Load Module. - Each subclass has itw own validity checks. Using ModuleName allows users - to edit bad module names instead of starting over. - Add tests and delete the two files combined into the new one. - -- Issue #27372: Test_idle no longer changes the locale. - - Issue #27365: Allow non-ascii chars in IDLE NEWS.txt, for contributor names. - Issue #27245: IDLE: Cleanly delete custom themes and key bindings. Previously, when IDLE was started from a console or by import, a cascade of warnings was emitted. Patch by Serhiy Storchaka. -- Issue #24137: Run IDLE, test_idle, and htest with tkinter default root disabled. - Fix code and tests that fail with this restriction. - Fix htests to not create a second and redundant root and mainloop. -- Issue #27310: Fix IDLE.app failure to launch on OS X due to vestigial import. +What's New in IDLE 2.7.12? +========================== +*Release date: 2016-06-25* - Issue #5124: Paste with text selected now replaces the selection on X11. This matches how paste works on Windows, Mac, most modern Linux apps, and ttk widgets. Original patch by Serhiy Storchaka. -- Issue #24750: Switch all scrollbars in IDLE to ttk versions. - Where needed, minimal tests are added to cover changes. - -- Issue #24759: IDLE requires tk 8.5 and availability ttk widgets. - Delete now unneeded tk version tests and code for older versions. - Add test for IDLE syntax colorizer. - -- Issue #27239: idlelib.macosx.isXyzTk functions initialize as needed. - -- Issue #27262: move Aqua unbinding code, which enable context menus, to macosx. - - Issue #24759: Make clear in idlelib.idle_test.__init__ that the directory is a private implementation of test.test_idle and tool for maintainers. -- Issue #27196: Stop 'ThemeChanged' warnings when running IDLE tests. - These persisted after other warnings were suppressed in #20567. - Apply Serhiy Storchaka's update_idletasks solution to four test files. - Record this additional advice in idle_test/README.txt - -- Issue #20567: Revise idle_test/README.txt with advice about avoiding - tk warning messages from tests. Apply advice to several IDLE tests. - -- Issue # 24225: Update idlelib/README.txt with new file names - and event handlers. - -- Issue #27156: Remove obsolete code not used by IDLE. Replacements: - 1. help.txt, replaced by help.html, is out-of-date and should not be used. - Its dedicated viewer has be replaced by the html viewer in help.py. - 2. 'import idlever; I = idlever.IDLE_VERSION' is the same as - 'import sys; I = version[:version.index(' ')]' - 3. After 'ob = stackviewer.VariablesTreeItem(*args)', - 'ob.keys()' == 'list(ob.object.keys). - 4. In macosc, runningAsOSXAPP == isAquaTk; idCarbonAquaTk == isCarbonTk - -- Issue #27117: Make colorizer htest and turtledemo work with dark themes. - Move code for configuring text widget colors to a new function. - -- Issue #24225: Rename many idlelib/*.py and idle_test/test_*.py files. - Edit files to replace old names with new names when the old name - referred to the module rather than the class it contained. - See the issue and IDLE section in What's New in 3.6 for more. - - Issue #26673: When tk reports font size as 0, change to size 10. Such fonts on Linux prevented the configuration dialog from opening. -- Issue #21939: Add test for IDLE's percolator. - Original patch by Saimadhav Heblikar. - -- Issue #21676: Add test for IDLE's replace dialog. - Original patch by Saimadhav Heblikar. - -- Issue #18410: Add test for IDLE's search dialog. - Original patch by Westley MartÃnez. - -- Issue #21703: Add test for undo delegator. Patch mostly by - Saimadhav Heblikar . - - Issue #27044: Add ConfigDialog.remove_var_callbacks to stop memory leaks. -- Issue #23977: Add more asserts to test_delegator. - -- Issue #20640: Add tests for idlelib.configHelpSourceEdit. - Patch by Saimadhav Heblikar. - - In the 'IDLE-console differences' section of the IDLE doc, clarify how running with IDLE affects sys.modules and the standard streams. - Issue #25507: fix incorrect change in IOBinding that prevented printing. + Change also prevented saving shell window with non-ascii characters. Augment IOBinding htest to include all major IOBinding functions. -- Issue #25905: Revert unwanted conversion of ' to ’ RIGHT SINGLE QUOTATION +- Issue #25905: Revert unwanted conversion of ' to ’ RIGHT SINGLE QUOTATION MARK in README.txt and open this and NEWS.txt with 'ascii'. Re-encode CREDITS.txt to utf-8 and open it with 'utf-8'. +- Issue #26417: Prevent spurious errors and incorrect defaults when + installing IDLE 2.7 on OS X: default configuration settings are + no longer installed from OS X specific copies. + + +What's New in IDLE 2.7.11? +========================== +*Release date: 2015-12-06* + - 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. @@ -929,11 +186,6 @@ Released on 2016-12-23 - Issue #24790: Remove extraneous code (which also create 2 & 3 conflicts). - -What's New in IDLE 3.5.0? -========================= -*Release date: 2015-09-13* - - Issue #23672: Allow Idle to edit and run files with astral chars in name. Patch by Mohd Sanad Zaki Rizvi. @@ -947,8 +199,15 @@ What's New in IDLE 3.5.0? - 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. +- Issue #15809: IDLE shell now uses locale encoding instead of Latin1 for + decoding unicode literals. + + +What's New in IDLE 2.7.10? +========================= +*Release date: 2015-05-23* + +- Issue #23583: Fixed writing unicode to standard output stream in IDLE. - Issue #20577: Configuration of the max line length for the FormatParagraph extension has been moved from the General tab of the Idle preferences dialog @@ -958,6 +217,17 @@ What's New in IDLE 3.5.0? - Issue #16893: Update Idle doc chapter to match current Idle and add new information. +- Issue #23180: Rename IDLE "Windows" menu item to "Window". + Patch by Al Sweigart. + + +What's New in IDLE 2.7.9? +========================= +*Release date: 2014-12-10* + +- Issue #16893: Update Idle doc chapter to match current Idle and add new + information. + - Issue #3068: Add Idle extension configuration dialog to Options menu. Changes are written to HOME/.idlerc/config-extensions.cfg. Original patch by Tal Einat. @@ -975,8 +245,8 @@ What's New in IDLE 3.5.0? - 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 #23180: Rename IDLE "Windows" menu item to "Window". - Patch by Al Sweigart. +- Issue #22221: IDLE now ignores the source encoding declaration on the second + line if the first line contains anything except a comment. - Issue #17390: Adjust Editor window title; remove 'Python', move version to end. @@ -984,11 +254,10 @@ What's New in IDLE 3.5.0? - Issue #14105: Idle debugger breakpoints no longer disappear when inserting or deleting lines. -- 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 #21765: Add support for non-ascii identifiers to HyperParser. +What's New in IDLE 2.7.8? +========================= +*Release date: 2014-06-29* - Issue #21940: Add unittest for WidgetRedirector. Initial patch by Saimadhav Heblikar. @@ -1012,8 +281,10 @@ What's New in IDLE 3.5.0? - Issue #18409: Add unittest for AutoComplete. Patch by Phil Webster. -- Issue #21477: htest.py - Improve framework, complete set of tests. - Patches by Saimadhav Heblikar + +What's New in IDLE 2.7.7? +========================= +*Release date: 2014-05-31* - 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 @@ -1023,58 +294,157 @@ What's New in IDLE 3.5.0? - Issue #21284: Paragraph reformat test passes after user changes reformat width. +- Issue #20406: Use Python application icons for Idle window title bars. + Patch mostly by Serhiy Storchaka. + +- Issue #21029: Occurrences of "print" are now consistently colored as + being a keyword (the colorizer doesn't know if print functions are + enabled in the source). + +- Issue #17721: Remove non-functional configuration dialog help button until we + make it actually gives some help when clicked. Patch by Guilherme Simões. + +- Issue #17390: Add Python version to Idle editor window title bar. + Original patches by Edmond Burnett and Kent Johnson. + +- Issue #20058: sys.stdin.readline() in IDLE now always returns only one line. + +- Issue #19481: print() of unicode, str or bytearray subclass instance in IDLE + no more hangs. + +- Issue #18270: Prevent possible IDLE AttributeError on OS X when no initial + shell window is present. + - 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.4.0? +What's New in IDLE 2.7.6? +========================= +*Release date: 2013-11-10* + +- Issue #19426: Fixed the opening of Python source file with specified encoding. + +- Issue #18873: IDLE now detects Python source code encoding only in comment + lines. + +- Issue #18988: The "Tab" key now works when a word is already autocompleted. + +- Issue #18489: Add tests for SearchEngine. Original patch by Phil Webster. + +- 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 #18226: Add docstrings and unittests for FormatParagraph.py. + Original patches by Todd Rovito and Phil Webster. + +- 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 #18539: Calltips now work for float default arguments. + +- Issue #7136: In the Idle File menu, "New Window" is renamed "New File". + Patch by Tal Einat, Roget Serwy, and Todd Rovito. + +- Issue #8515: Set __file__ when run file in IDLE. + Initial patch by Bruce Frederiksen. + +- Issue #5492: Avoid traceback when exiting IDLE caused by a race condition. + +- Issue #17511: Keep IDLE find dialog open after clicking "Find Next". + Original patch by Sarah K. + +- Issue #15392: Create a unittest framework for IDLE. + Preliminary patch by Rajagopalasarma Jayakrishnan + See Lib/idlelib/idle_test/README.txt for how to run Idle tests. + +- Issue #14146: Highlight source line while debugging on Windows. + +- Issue #17532: Always include Options menu for IDLE on OS X. + Patch by Guilherme Simões. + + +What's New in IDLE 2.7.5? ========================= -*Release date: 2014-03-16* +*Release date: 2013-05-12* + +- Issue #17838: Allow sys.stdin to be reassigned. + +- Issue #14735: Update IDLE docs to omit "Control-z on Windows". + +- Issue #17585: Fixed IDLE regression. Now closes when using exit() or quit(). + +- Issue #17657: Show full Tk version in IDLE's about dialog. + Patch by Todd Rovito. + +- Issue #17613: Prevent traceback when removing syntax colorizer in IDLE. + +- Issue #1207589: Backwards-compatibility patch for right-click menu in IDLE. + +- Issue #16887: IDLE now accepts Cancel in tabify/untabify dialog box. + +- Issue #14254: IDLE now handles readline correctly across shell restarts. + +- Issue #17614: IDLE no longer raises exception when quickly closing a file. + +- Issue #6698: IDLE now opens just an editor window when configured to do so. + +- Issue #8900: Using keyboard shortcuts in IDLE to open a file no longer + raises an exception. + +- Issue #6649: Fixed missing exit status in IDLE. Patch by Guilherme Polo. - 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. +What's New in IDLE 2.7.4? +========================= +*Release date: 2013-04-06* -- Issue #16226: Fix IDLE Path Browser crash. - (Patch by Roger Serwy) +- Issue #17625: In IDLE, close the replace dialog after it is used. -- Issue #15853: Prevent IDLE crash on OS X when opening Preferences menu - with certain versions of Tk 8.5. Initial patch by Kevin Walzer. +- IDLE was displaying spurious SystemExit tracebacks when running scripts + that terminated by raising SystemExit (i.e. unittest and turtledemo). +- Issue #9290: In IDLE the sys.std* streams now implement io.TextIOBase + interface and support all mandatory methods and properties. -What's New in IDLE 3.3.0? -========================= -*Release date: 2012-09-29* +- Issue #16829: IDLE printing no longer fails if there are spaces or other + special characters in the file path. -- Issue #17625: Close the replace dialog after it is used. +- Issue #16819: IDLE method completion now correctly works for unicode literals. -- Issue #7163: Propagate return value of sys.stdout.write. +- Issue #16504: IDLE now catches SyntaxErrors raised by tokenizer. Patch by + Roger Serwy. -- Issue #15318: Prevent writing to sys.stdin. +- Issue #1207589: Add Cut/Copy/Paste items to IDLE right click Context Menu + Patch by Todd Rovito. -- Issue #4832: Modify IDLE to save files with .py extension by - default on Windows and OS X (Tk 8.5) as it already does with X11 Tk. +- Issue #13052: Fix IDLE crashing when replace string in Search/Replace dialog + ended with '\'. Patch by Roger Serwy. -- Issue #13532, #15319: Check that arguments to sys.stdout.write are strings. +- Issue #9803: Don't close IDLE on saving if breakpoint is open. + Patch by Roger Serwy. -- Issue # 12510: Attempt to get certain tool tips no longer crashes IDLE. - Erroneous tool tips have been corrected. Default added for callables. +- Issue #14958: Change IDLE systax highlighting to recognize all string and byte + literals currently supported in Python 2.7. -- Issue #10365: File open dialog now works instead of crashing even when - parent window is closed while dialog is open. +- Issue #14962: Update text coloring in IDLE shell window after changing + options. Patch by Roger Serwy. -- Issue 14876: use user-selected font for highlight configuration. +- Issue #10997: Prevent a duplicate entry in IDLE's "Recent Files" menu. -- Issue #14937: Perform auto-completion of filenames in strings even for - non-ASCII filenames. Likewise for identifiers. +- Issue #12510: Attempting to get invalid tooltip no longer closes IDLE. + Original patch by Roger Serwy. -- Issue #8515: Set __file__ when run file in IDLE. - Initial patch by Bruce Frederiksen. +- Issue #10365: File open dialog now works instead of crashing + even when parent window is closed. Patch by Roger Serwy. -- IDLE can be launched as `python -m idlelib` +- Issue #14876: Use user-selected font for highlight configuration. + Patch by Roger Serwy. - Issue #14409: IDLE now properly executes commands in the Shell window when it cannot read the normal config files on startup and @@ -1082,49 +452,737 @@ What's New in IDLE 3.3.0? There was previously a bug in one of the defaults. - Issue #3573: IDLE hangs when passing invalid command line args - (directory(ies) instead of file(s)). + (directory(ies) instead of file(s)) (Patch by Guilherme Polo) + +- Issue #5219: Prevent event handler cascade in IDLE. + +- Issue #15318: Prevent writing to sys.stdin. + +- Issue #13532, #15319: Check that arguments to sys.stdout.write are strings. + +- Issue #10365: File open dialog now works instead of crashing even when + parent window is closed while dialog is open. - Issue #14018: Update checks for unstable system Tcl/Tk versions on OS X to include versions shipped with OS X 10.7 and 10.8 in addition to 10.6. +- Issue #15853: Prevent IDLE crash on OS X when opening Preferences menu + with certain versions of Tk 8.5. Initial patch by Kevin Walzer. + + +What's New in IDLE 2.7.3? +========================= +*Release date: 2012-04-09* + +- Issue #964437 Make IDLE help window non-modal. + Patch by Guilherme Polo and Roger Serwy. + +- Issue #13933: IDLE auto-complete did not work with some imported + module, like hashlib. (Patch by Roger Serwy) + +- Issue #13506: Add '' to path for IDLE Shell when started and restarted with Restart Shell. + Original patches by Marco Scataglini and Roger Serwy. + +- Issue #4625: If IDLE cannot write to its recent file or breakpoint + files, display a message popup and continue rather than crash. + (original patch by Roger Serwy) + +- Issue #8793: Prevent IDLE crash when given strings with invalid hex escape + sequences. -What's New in IDLE 3.2.1? +- Issue #13296: Fix IDLE to clear compile __future__ flags on shell restart. + (Patch by Roger Serwy) + +- Issue #14409: IDLE now properly executes commands in the Shell window + when it cannot read the normal config files on startup and + has to use the built-in default key bindings. + There was previously a bug in one of the defaults. + +- Issue #3573: IDLE hangs when passing invalid command line args + (directory(ies) instead of file(s)). + + +What's New in IDLE 2.7.2? ========================= -*Release date: 15-May-11* +*Release date: 2011-06-11* + +- Issue #11718: IDLE's open module dialog couldn't find the __init__.py + file in a package. + +- Issue #12590: IDLE editor window now always displays the first line + when opening a long file. With Tk 8.5, the first line was hidden. + +- Issue #11088: don't crash when using F5 to run a script in IDLE on MacOSX + with Tk 8.5. + +- Issue #10940: Workaround an IDLE hang on Mac OS X 10.6 when using the + menu accelerators for Open Module, Go to Line, and New Indent Width. + The accelerators still work but no longer appear in the menu items. + +- Issue #10907: Warn OS X 10.6 IDLE users to use ActiveState Tcl/Tk 8.5, rather + than the currently problematic Apple-supplied one, when running with the + 64-/32-bit installer variant. + +- Issue #11052: Correct IDLE menu accelerators on Mac OS X for Save + commands. + +- Issue #6075: IDLE on Mac OS X now works with both Carbon AquaTk and + Cocoa AquaTk. + +- Issue #10404: Use ctl-button-1 on OSX for the context menu in Idle. + +- Issue #10107: Warn about unsaved files in IDLE on OSX. + +- Issue #10406: Enable Rstrip IDLE extension on OSX (just like on other + platforms). - Issue #6378: Further adjust idle.bat to start associated Python - Issue #11896: Save on Close failed despite selecting "Yes" in dialog. -- Issue #1028: Ctrl-space binding to show completions was causing IDLE to exit. - Tk < 8.5 was sending invalid Unicode null; replaced with valid null. +- Issue #4676: <Home> toggle failing on Tk 8.5, causing IDLE exits and + strange selection behavior. Improve selection extension behaviour. -- Issue #4676: <Home> toggle failing on Tk 8.5, causing IDLE exits and strange selection - behavior. Improve selection extension behaviour. +- Issue #3851 <Home> toggle non-functional when NumLock set on Windows. -- Issue #3851: <Home> toggle non-functional when NumLock set on Windows. +What's New in Python 2.7.1? +=========================== +*Release date: 2010-11-27* -What's New in IDLE 3.1b1? -========================= -*Release date: 06-May-09* +- Issue #6378: idle.bat now runs with the appropriate Python version rather than + the system default. Patch by Sridhar Ratnakumar. + + +What's New in IDLE 2.7? +======================= +*Release date: 2010-07-03* + +- Issue #5150: IDLE's format menu now has an option to strip trailing + whitespace. + +- Issue #5847: Remove -n switch on "Edit with IDLE" menu item. + +- idle.py modified and simplified to better support developing experimental + versions of IDLE which are not installed in the standard location. + +- Issue #5559: OutputWindow/PyShell right click menu "Go to file/line" + wasn't working with file paths containing spaces. + +- Issue #5783: Windows: Version string for the .chm help file changed, + file not being accessed Patch by Guilherme Polo/ -- Issue #5707: Use of 'filter' in keybindingDialog.py was causing custom key assignment to - fail. Patch by Amaury Forgeot d'Arc. +- Issue #1529142: Allow multiple IDLE GUI/subprocess pairs to exist + simultaneously. Thanks to David Scherer for suggesting the use of an + ephemeral port for the GUI. Patch by Weeble. -- Issue #4815: Offer conversion to UTF-8 if source files have - no encoding declaration and are not encoded in UTF-8. +- Remove port spec from run.py and fix bug where subprocess fails to + extract port from command line when warnings are present. -- Issue #4008: Fix problems with non-ASCII source files. +- Issue #5129: Tk 8.5 Text widget requires 'wordprocessor' tabstyle attr + to handle mixed space/tab properly. Patch by Guilherme Polo. -- Issue #4323: Always encode source as UTF-8 without asking - the user (unless a different encoding is declared); remove - user configuration of source encoding; all according to - PEP 3120. +- Issue #3549: On MacOS the preferences menu was not present + + +What's New in IDLE 2.6? +======================= +*Release date: 01-Oct-2008* - Issue #2665: On Windows, an IDLE installation upgraded from an old version would not start if a custom theme was defined. ------------------------------------------------------------------------- -Refer to NEWS2x.txt and HISTORY.txt for information on earlier releases. ------------------------------------------------------------------------- +- Home / Control-A toggles between left margin and end of leading white + space. Patch 1196903 Jeff Shute. + +- Improved AutoCompleteWindow logic. Patch 2062 Tal Einat. + +- Autocompletion of filenames now support alternate separators, e.g. the + '/' char on Windows. Patch 2061 Tal Einat. + +- Configured selection highlighting colors were ignored; updating highlighting + in the config dialog would cause non-Python files to be colored as if they + were Python source; improve use of ColorDelagator. Patch 1334. Tal Einat. + +- ScriptBinding event handlers weren't returning 'break'. Patch 2050, Tal Einat. + +- There was an error on exit if no sys.exitfunc was defined. Issue 1647. + +- Could not open files in .idlerc directory if latter was hidden on Windows. + Issue 1743, Issue 1862. + +- Configure Dialog: improved layout for keybinding. Patch 1457 Tal Einat. + +- tabpage.py updated: tabbedPages.py now supports multiple dynamic rows + of tabs. Patch 1612746 Tal Einat. + +- Add confirmation dialog before printing. Patch 1717170 Tal Einat. + +- Show paste position if > 80 col. Patch 1659326 Tal Einat. + +- Update cursor color without restarting. Patch 1725576 Tal Einat. + +- Allow keyboard interrupt only when user code is executing in subprocess. + Patch 1225 Tal Einat (reworked from IDLE-Spoon). + +- configDialog cleanup. Patch 1730217 Tal Einat. + +- textView cleanup. Patch 1718043 Tal Einat. + +- Clean up EditorWindow close. + +- Patch 1693258: Fix for duplicate "preferences" menu-OS X. Backport of r56204. + +- OSX: Avoid crash for those versions of Tcl/Tk which don't have a console + +- Bug in idlelib.MultiCall: Options dialog was crashing IDLE if there was an + option in config-extensions w/o a value. Patch #1672481, Tal Einat + +- Corrected some bugs in AutoComplete. Also, Page Up/Down in ACW implemented; + mouse and cursor selection in ACWindow implemented; double Tab inserts + current selection and closes ACW (similar to double-click and Return); scroll + wheel now works in ACW. Added AutoComplete instructions to IDLE Help. + +- AutoCompleteWindow moved below input line, will move above if there + isn't enough space. Patch 1621265 Tal Einat + +- Calltips now 'handle' tuples in the argument list (display '<tuple>' :) + Suggested solution by Christos Georgiou, Bug 791968. + +- Add 'raw' support to configHandler. Patch 1650174 Tal Einat. + +- Avoid hang when encountering a duplicate in a completion list. Bug 1571112. + +- Patch #1362975: Rework CodeContext indentation algorithm to + avoid hard-coding pixel widths. + +- Bug #813342: Start the IDLE subprocess with -Qnew if the parent + is started with that option. + +- Honor the "Cancel" action in the save dialog (Debian bug #299092) + +- Some syntax errors were being caught by tokenize during the tabnanny + check, resulting in obscure error messages. Do the syntax check + first. Bug 1562716, 1562719 + +- IDLE's version number takes a big jump to match the version number of + the Python release of which it's a part. + + +What's New in IDLE 1.2? +======================= +*Release date: 19-SEP-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'. + +- IDLE honors new quit() and exit() commands from site.py Quitter() object. + Patch 1540892, Jim Jewett + +- The 'with' statement is now a Code Context block opener. + Patch 1540851, Jim Jewett + +- Retrieval of previous shell command was not always preserving indentation + (since 1.2a1) Patch 1528468 Tal Einat. + +- Changing tokenize (39046) to detect dedent broke tabnanny check (since 1.2a1) + +- ToggleTab dialog was setting indent to 8 even if cancelled (since 1.2a1). + +- When used w/o subprocess, all exceptions were preceded by an error + message claiming they were IDLE internal errors (since 1.2a1). + +- Bug #1525817: Don't truncate short lines in IDLE's tool tips. + +- Bug #1517990: IDLE keybindings on MacOS X now work correctly + +- Bug #1517996: IDLE now longer shows the default Tk menu when a + path browser, class browser or debugger is the frontmost window on MacOS X + +- EditorWindow.test() was failing. Bug 1417598 + +- EditorWindow failed when used stand-alone if sys.ps1 not set. + Bug 1010370 Dave Florek + +- Tooltips failed on new-syle class __init__ args. Bug 1027566 Loren Guthrie + +- Avoid occasional failure to detect closing paren properly. + Patch 1407280 Tal Einat + +- Rebinding Tab key was inserting 'tab' instead of 'Tab'. Bug 1179168. + +- Colorizer now handles #<builtin> correctly, also unicode strings and + 'as' keyword in comment directly following import command. Closes 1325071. + Patch 1479219 Tal Einat + +- Patch #1162825: Support non-ASCII characters in IDLE window titles. + +- Source file f.flush() after writing; trying to avoid lossage if user + kills GUI. + +- Options / Keys / Advanced dialog made functional. Also, allow binding + of 'movement' keys. + +- 'syntax' patch adds improved calltips and a new class attribute listbox. + MultiCall module allows binding multiple actions to an event. + Patch 906702 Noam Raphael + +- Better indentation after first line of string continuation. + IDLEfork Patch 681992, Noam Raphael + +- Fixed CodeContext alignment problem, following suggestion from Tal Einat. + +- Increased performance in CodeContext extension Patch 936169 Noam Raphael + +- Mac line endings were incorrect when pasting code from some browsers + when using X11 and the Fink distribution. Python Bug 1263656. + +- <Enter> when cursor is on a previous command retrieves that command. Instead + of replacing the input line, the previous command is now appended to the + input line. Indentation is preserved, and undo is enabled. + Patch 1196917 Jeff Shute + +- Clarify "tab/space" Error Dialog and "Tab Width" Dialog associated with + the Untabify command. + +- Corrected "tab/space" Error Dialog to show correct menu for Untabify. + Patch 1196980 Jeff Shute + +- New files are colorized by default, and colorizing is removed when + saving as non-Python files. Patch 1196895 Jeff Shute + Closes Python Bugs 775012 and 800432, partial fix IDLEfork 763524 + +- Improve subprocess link error notification. + +- run.py: use Queue's blocking feature instead of sleeping in the main + loop. Patch # 1190163 Michiel de Hoon + +- Add config-main option to make the 'history' feature non-cyclic. + Default remains cyclic. Python Patch 914546 Noam Raphael. + +- Removed ability to configure tabs indent from Options dialog. This 'feature' + has never worked and no one has complained. It is still possible to set a + default tabs (v. spaces) indent 'manually' via config-main.def (or to turn on + tabs for the current EditorWindow via the Format menu) but IDLE will + encourage indentation via spaces. + +- Enable setting the indentation width using the Options dialog. + Bug # 783877 + +- Add keybindings for del-word-left and del-word-right. + +- Discourage using an indent width other than 8 when using tabs to indent + Python code. + +- Restore use of EditorWindow.set_indentation_params(), was dead code since + Autoindent was merged into EditorWindow. This allows IDLE to conform to the + indentation width of a loaded file. (But it still will not switch to tabs + even if the file uses tabs.) Any change in indent width is local to that + window. + +- Add Tabnanny check before Run/F5, not just when Checking module. + +- If an extension can't be loaded, print warning and skip it instead of + erroring out. + +- Improve error handling when .idlerc can't be created (warn and exit). + +- The GUI was hanging if the shell window was closed while a raw_input() + was pending. Restored the quit() of the readline() mainloop(). + http://mail.python.org/pipermail/idle-dev/2004-December/002307.html + +- 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. + +- 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. + +- 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. + +- 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. + +- Improve keyboard focus binding, especially in Windows menu. Improve + window raising, especially in the Windows menu and in the debugger. + IDLEfork 763524. + +- If user passes a non-existent filename on the commandline, just + open a new file, don't raise a dialog. IDLEfork 854928. + +- 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. + +- 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. + +- CodeContext hint pane visibility state is now persistent across sessions. + The pane no longer appears in the shell window. Added capability to limit + extensions to shell window or editor windows. Noam Raphael addition + to Patch 936169. + +- Paragraph reformat width is now a configurable parameter in the + Options GUI. + +- New Extension: CodeContext. Provides block structuring hints for code + which has scrolled above an edit window. Patch 936169 Noam Raphael. + +- If nulls somehow got into the strings in recent-files.lst + EditorWindow.update_recent_files_list() was failing. Python Bug 931336. + +- If the normal background is changed via Configure/Highlighting, it will + update immediately, thanks to the previously mentioned patch by Nigel Rowe. + +- Add a highlight theme for builtin keywords. Python Patch 805830 Nigel Rowe + This also fixed IDLEfork bug [ 693418 ] Normal text background color not + refreshed and Python bug [897872 ] Unknown color name on HP-UX + +- rpc.py:SocketIO - Large modules were generating large pickles when downloaded + to the execution server. The return of the OK response from the subprocess + initialization was interfering and causing the sending socket to be not + ready. Add an IO ready test to fix this. Moved the polling IO ready test + into pollpacket(). + +- Fix typo in rpc.py, s/b "pickle.PicklingError" not "pickle.UnpicklingError". + +- Added a Tk error dialog to run.py inform the user if the subprocess can't + connect to the user GUI process. Added a timeout to the GUI's listening + socket. Added Tk error dialogs to PyShell.py to announce a failure to bind + the port or connect to the subprocess. Clean up error handling during + connection initiation phase. This is an update of Python Patch 778323. + +- Print correct exception even if source file changed since shell was + restarted. IDLEfork Patch 869012 Noam Raphael + +- Keybindings with the Shift modifier now work correctly. So do bindings which + use the Space key. Limit unmodified user keybindings to the function keys. + Python Bug 775353, IDLEfork Bugs 755647, 761557 + +- After an exception, run.py was not setting the exception vector. Noam + Raphael suggested correcting this so pdb's postmortem pm() would work. + IDLEfork Patch 844675 + +- IDLE now does not fail to save the file anymore if the Tk buffer is not a + Unicode string, yet eol_convention is. Python Bugs 774680, 788378 + +- IDLE didn't start correctly when Python was installed in "Program Files" on + W2K and XP. Python Bugs 780451, 784183 + +- config-main.def documentation incorrectly referred to idle- instead of + config- filenames. SF 782759 Also added note about .idlerc location. + + +What's New in IDLE 1.0? +======================= +*Release date: 29-Jul-2003* + +- Calltip error when docstring was None Python Bug 775541 + +- Updated extend.txt, help.txt, and config-extensions.def to correctly + reflect the current status of the configuration system. Python Bug 768469 + +- Fixed: Call Tip Trimming May Loop Forever. Python Patch 769142 (Daniels) + +- Replaced apply(f, args, kwds) with f(*args, **kwargs) to improve performance + Python Patch 768187 + +- Break or continue statements outside a loop were causing IDLE crash + Python Bug 767794 + +- Convert Unicode strings from readline to IOBinding.encoding. Also set + sys.std{in|out|err}.encoding, for both the local and the subprocess case. + SF IDLEfork patch 682347. + +- Extend AboutDialog.ViewFile() to support file encodings. Make the CREDITS + file Latin-1. + +- Updated the About dialog to reflect re-integration into Python. Provide + buttons to display Python's NEWS, License, and Credits, plus additional + buttons for IDLE's README and NEWS. + +- TextViewer() now has a third parameter which allows inserting text into the + viewer instead of reading from a file. + +- (Created the .../Lib/idlelib directory in the Python CVS, which is a clone of + IDLEfork modified to install in the Python environment. The code in the + interrupt module has been moved to thread.interrupt_main(). ) + +- Printing the Shell window was failing if it was not saved first SF 748975 + +- When using the Search in Files dialog, if the user had a selection + highlighted in his Editor window, insert it into the dialog search field. + +- The Python Shell entry was disappearing from the Windows menu. + +- Update the Windows file list when a file name change occurs + +- Change to File / Open Module: always pop up the dialog, using the current + selection as the default value. This is easier to use habitually. + +- Avoided a problem with starting the subprocess when 'localhost' doesn't + resolve to the user's loopback interface. SF 747772 + +- Fixed an issue with highlighted errors never de-colorizing. SF 747677. Also + improved notification of Tabnanny Token Error. + +- File / New will by default save in the directory of the Edit window from + which it was initiated. SF 748973 Guido van Rossum patch. + + +What's New in IDLEfork 0.9b1? +============================= +*Release date: 02-Jun-2003* + +- The current working directory of the execution environment (and shell + following completion of execution) is now that of the module being run. + +- Added the delete-exitfunc option to config-main.def. (This option is not + included in the Options dialog.) Setting this to True (the default) will + cause IDLE to not run sys.exitfunc/atexit when the subprocess exits. + +- IDLE now preserves the line ending codes when editing a file produced on + a different platform. SF 661759, SF 538584 + +- Reduced default editor font size to 10 point and increased window height + to provide a better initial impression on Windows. + +- Options / Fonts/Tabs / Set Base Editor Font: List box was not highlighting + the default font when first installed on Windows. SF 661676 + +- Added Autosave feature: when user runs code from edit window, if the file + has been modified IDLE will silently save it if Autosave is enabled. The + option is set in the Options dialog, and the default is to prompt the + user to save the file. SF 661318 Bruce Sherwood patch. + +- Improved the RESTART annotation in the shell window when the user restarts + the shell while it is generating output. Also improved annotation when user + repeatedly hammers the Ctrl-F6 restart. + +- Allow IDLE to run when not installed and cwd is not the IDLE directory + SF Patch 686254 "Run IDLEfork from any directory without set-up" - Raphael + +- When a module is run from an EditorWindow: if its directory is not in + sys.path, prepend it. This allows the module to import other modules in + the same directory. Do the same for a script run from the command line. + +- Correctly restart the subprocess if it is running user code and the user + attempts to run some other module or restarts the shell. Do the same if + the link is broken and it is possible to restart the subprocess and re- + connect to the GUI. SF RFE 661321. + +- Improved exception reporting when running commands or scripts from the + command line. + +- Added a -n command line switch to start IDLE without the subprocess. + Removed the Shell menu when running in that mode. Updated help messages. + +- Added a comment to the shell startup header to indicate when IDLE is not + using the subprocess. + +- Restore the ability to run without the subprocess. This can be important for + some platforms or configurations. (Running without the subprocess allows the + debugger to trace through parts of IDLE itself, which may or may not be + desirable, depending on your point of view. In addition, the traditional + reload/import tricks must be use if user source code is changed.) This is + helpful for developing IDLE using IDLE, because one instance can be used to + edit the code and a separate instance run to test changes. (Multiple + concurrent IDLE instances with subprocesses is a future feature) + +- Improve the error message a user gets when saving a file with non-ASCII + characters and no source encoding is specified. Done by adding a dialog + 'EncodingMessage', which contains the line to add in a fixed-font entry + widget, and which has a button to add that line to the file automatically. + Also, add a configuration option 'EditorWindow/encoding', which has three + possible values: none, utf-8, and locale. None is the default: IDLE will show + this dialog when non-ASCII characters are encountered. utf-8 means that files + with non-ASCII characters are saved as utf-8-with-bom. locale means that + files are saved in the locale's encoding; the dialog is only displayed if the + source contains characters outside the locale's charset. SF 710733 - Loewis + +- Improved I/O response by tweaking the wait parameter in various + calls to signal.signal(). + +- Implemented a threaded subprocess which allows interrupting a pass + loop in user code using the 'interrupt' extension. User code runs + in MainThread, while the RPCServer is handled by SockThread. This is + necessary because Windows doesn't support signals. + +- Implemented the 'interrupt' extension module, which allows a subthread + to raise a KeyboardInterrupt in the main thread. + +- Attempting to save the shell raised an error related to saving + breakpoints, which are not implemented in the shell + +- Provide a correct message when 'exit' or 'quit' are entered at the + IDLE command prompt SF 695861 + +- Eliminate extra blank line in shell output caused by not flushing + stdout when user code ends with an unterminated print. SF 695861 + +- Moved responsibility for exception formatting (i.e. pruning IDLE internal + calls) out of rpc.py into the client and server. + +- Exit IDLE cleanly even when doing subprocess I/O + +- Handle subprocess interrupt with an RPC message. + +- Restart the subprocess if it terminates itself. (VPython programs do that) + +- Support subclassing of exceptions, including in the shell, by moving the + 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. + +- Improved formatting in IDLE Help. + +- Run menu: Replace "Run Script" with "Run Module". + +- Code encountering an unhandled exception under the debugger now shows + the correct traceback, with IDLE internal levels pruned out. + +- If an exception occurs entirely in IDLE, don't prune the IDLE internal + modules from the traceback displayed. + +- Class Browser and Path Browser now use Alt-Key-2 for vertical zoom. + +- IDLE icons will now install correctly even when setup.py is run from the + build directory + +- Class Browser now compatible with Python2.3 version of pyclbr.py + +- Left cursor move in presence of selected text now moves from left end + of the selection. + +- Add Meta keybindings to "IDLE Classic Windows" to handle reversed + Alt/Meta on some Linux distros. + +- Change default: IDLE now starts with Python Shell. + +- Removed the File Path from the Additional Help Sources scrolled list. + +- Add capability to access Additional Help Sources on the web if the + Help File Path begins with //http or www. (Otherwise local path is + validated, as before.) + +- Additional Help Sources were not being posted on the Help menu in the + order entered. Implement sorting the list by [HelpFiles] 'option' + number. + +- Add Browse button to New Help Source dialog. Arrange to start in + Python/Doc if platform is Windows, otherwise start in current directory. + +- Put the Additional Help Sources directly on the Help menu instead of in + an Extra Help cascade menu. Rearrange the Help menu so the Additional + Help Sources come last. Update help.txt appropriately. + +- Fix Tk root pop-ups in configSectionNameDialog.py and configDialog.py + +- Uniform capitalization in General tab of ConfigDialog, update the doc string. + +- Fix bug in ConfigDialog where SaveAllChangedConfig() was unexpectedly + deleting Additional Help Sources from the user's config file. + +- Make configHelpSourceEdit OK button the default and bind <Return> + +- Fix Tk root pop-ups in configHelpSourceEdit: error dialogs not attached + to parents. + +- Use os.startfile() to open both Additional Help and Python Help on the + Windows platform. The application associated with the file type will act as + the viewer. Windows help files (.chm) are now supported via the + Settings/General/Additional Help facility. + +- If Python Help files are installed locally on Linux, use them instead of + accessing python.org. + +- Make the methods for finding the Python help docs more robust, and make + them work in the installed configuration, also. + +- On the Save Before Run dialog, make the OK button the default. One + less mouse action! + +- Add a method: EditorWindow.get_geometry() for future use in implementing + window location persistence. + +- Removed the "Help/Advice" menu entry. Thanks, David! We'll remember! + +- Change the "Classic Windows" theme's paste key to be <ctrl-v>. + +- Rearrange the Shell menu to put Stack Viewer entries adjacent. + +- Add the ability to restart the subprocess interpreter from the shell window; + add an associated menu entry "Shell/Restart" with binding Control-F6. Update + IDLE help. + +- Upon a restart, annotate the shell window with a "restart boundary". Add a + shell window menu "Shell/View Restart" with binding F6 to jump to the most + recent restart boundary. + +- Add Shell menu to Python Shell; change "Settings" to "Options". + +- Remove incorrect comment in setup.py: IDLEfork is now installed as a package. + +- Add INSTALL.txt, HISTORY.txt, NEWS.txt to installed configuration. + +- In installer text, fix reference to Visual Python, should be VPython. + Properly credit David Scherer. + +- Modified idle, idle.py, idle.pyw to improve exception handling. + + +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 + Idle-dev and/or the Sourceforge CVS. + +- Adapted to the Mac platform. + +- Overhauled the IDLE startup options and revised the idle -h help message, + which provides details of command line usage. + +- Multiple bug fixes and usability enhancements. + +- Introduced the new RPC implementation, which includes a debugger. The output + of user code is to the shell, and the shell may be used to inspect the + environment after the run has finished. (In version 0.8.1 the shell + environment was separate from the environment of the user code.) + +- Introduced the configuration GUI and a new About dialog. + +- Removed David Scherer's Remote Procedure Call code and replaced with Guido + van Rossum's. GvR code has support for the IDLE debugger and uses the shell + to inspect the environment of code Run from an Edit window. Files removed: + ExecBinding.py, loader.py, protocol.py, Remote.py, spawn.py + +-------------------------------------------------------------------- +Refer to HISTORY.txt for additional information on earlier releases. +-------------------------------------------------------------------- + + + + + diff --git a/Lib/idlelib/NEWS2x.txt b/Lib/idlelib/NEWS2x.txt deleted file mode 100644 index 6751ca5..0000000 --- a/Lib/idlelib/NEWS2x.txt +++ /dev/null @@ -1,660 +0,0 @@ -What's New in IDLE 2.7? (Merged into 3.1 before 2.7 release.) -======================= -*Release date: XX-XXX-2010* - -- idle.py modified and simplified to better support developing experimental - versions of IDLE which are not installed in the standard location. - -- OutputWindow/PyShell right click menu "Go to file/line" wasn't working with - file paths containing spaces. Bug 5559. - -- Windows: Version string for the .chm help file changed, file not being - accessed Patch 5783 Guilherme Polo - -- Allow multiple IDLE GUI/subprocess pairs to exist simultaneously. Thanks to - David Scherer for suggesting the use of an ephemeral port for the GUI. - Patch 1529142 Weeble. - -- Remove port spec from run.py and fix bug where subprocess fails to - extract port from command line when warnings are present. - -- Tk 8.5 Text widget requires 'wordprocessor' tabstyle attr to handle - mixed space/tab properly. Issue 5129, patch by Guilherme Polo. - -- Issue #3549: On MacOS the preferences menu was not present - -- IDLE would print a "Unhandled server exception!" message when internal - debugging is enabled. - -- Issue #4455: IDLE failed to display the windows list when two windows have - the same title. - -- Issue #4383: When IDLE cannot make the connection to its subprocess, it would - fail to properly display the error message. - -- help() was not paging to the shell. Issue1650. - -- CodeContext was not importing. - -- Corrected two 3.0 compatibility errors reported by Mark Summerfield: - http://mail.python.org/pipermail/python-3000/2007-December/011491.html - -- Shell was not colorizing due to bug introduced at r57998, Bug 1586. - -- Issue #1585: IDLE uses non-existent xrange() function. - -- Windows EOL sequence not converted correctly, encoding error. - Caused file save to fail. Bug 1130. - -- IDLE converted to Python 3000 syntax. - -- Strings became Unicode. - -- CallTips module now uses the inspect module to produce the argspec. - -- IDLE modules now use absolute import instead of implied relative import. - -- atexit call replaces sys.exitfunc. The functionality of delete-exitfunc flag - in config-main.cfg remains unchanged: if set, registered exit functions will - be cleared before IDLE exits. - - -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 - would not start if a custom theme was defined. - -- Home / Control-A toggles between left margin and end of leading white - space. issue1196903, patch by Jeff Shute. - -- Improved AutoCompleteWindow logic. issue2062, patch by Tal Einat. - -- Autocompletion of filenames now support alternate separators, e.g. the - '/' char on Windows. issue2061 Patch by Tal Einat. - -- Configured selection highlighting colors were ignored; updating highlighting - in the config dialog would cause non-Python files to be colored as if they - were Python source; improve use of ColorDelagator. Patch 1334. Tal Einat. - -- ScriptBinding event handlers weren't returning 'break'. Patch 2050, Tal Einat - -- There was an error on exit if no sys.exitfunc was defined. Issue 1647. - -- Could not open files in .idlerc directory if latter was hidden on Windows. - Issue 1743, Issue 1862. - -- Configure Dialog: improved layout for keybinding. Patch 1457 Tal Einat. - -- tabpage.py updated: tabbedPages.py now supports multiple dynamic rows - of tabs. Patch 1612746 Tal Einat. - -- Add confirmation dialog before printing. Patch 1717170 Tal Einat. - -- Show paste position if > 80 col. Patch 1659326 Tal Einat. - -- Update cursor color without restarting. Patch 1725576 Tal Einat. - -- Allow keyboard interrupt only when user code is executing in subprocess. - Patch 1225 Tal Einat (reworked from IDLE-Spoon). - -- configDialog cleanup. Patch 1730217 Tal Einat. - -- textView cleanup. Patch 1718043 Tal Einat. - -- Clean up EditorWindow close. - -- Patch 1693258: Fix for duplicate "preferences" menu-OS X. Backport of r56204. - -- OSX: Avoid crash for those versions of Tcl/Tk which don't have a console - -- Bug in idlelib.MultiCall: Options dialog was crashing IDLE if there was an - option in config-extensions w/o a value. Patch #1672481, Tal Einat - -- Corrected some bugs in AutoComplete. Also, Page Up/Down in ACW implemented; - mouse and cursor selection in ACWindow implemented; double Tab inserts - current selection and closes ACW (similar to double-click and Return); scroll - wheel now works in ACW. Added AutoComplete instructions to IDLE Help. - -- AutoCompleteWindow moved below input line, will move above if there - isn't enough space. Patch 1621265 Tal Einat - -- Calltips now 'handle' tuples in the argument list (display '<tuple>' :) - Suggested solution by Christos Georgiou, Bug 791968. - -- Add 'raw' support to configHandler. Patch 1650174 Tal Einat. - -- Avoid hang when encountering a duplicate in a completion list. Bug 1571112. - -- Patch #1362975: Rework CodeContext indentation algorithm to - avoid hard-coding pixel widths. - -- Bug #813342: Start the IDLE subprocess with -Qnew if the parent - is started with that option. - -- Honor the "Cancel" action in the save dialog (Debian bug #299092) - -- Some syntax errors were being caught by tokenize during the tabnanny - check, resulting in obscure error messages. Do the syntax check - first. Bug 1562716, 1562719 - -- IDLE's version number takes a big jump to match the version number of - the Python release of which it's a part. - - -What's New in IDLE 1.2? -======================= -*Release date: 19-SEP-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'. - -- IDLE honors new quit() and exit() commands from site.py Quitter() object. - Patch 1540892, Jim Jewett - -- The 'with' statement is now a Code Context block opener. - Patch 1540851, Jim Jewett - -- Retrieval of previous shell command was not always preserving indentation - (since 1.2a1) Patch 1528468 Tal Einat. - -- Changing tokenize (39046) to detect dedent broke tabnanny check (since 1.2a1) - -- ToggleTab dialog was setting indent to 8 even if cancelled (since 1.2a1). - -- When used w/o subprocess, all exceptions were preceded by an error - message claiming they were IDLE internal errors (since 1.2a1). - -- Bug #1525817: Don't truncate short lines in IDLE's tool tips. - -- Bug #1517990: IDLE keybindings on MacOS X now work correctly - -- Bug #1517996: IDLE now longer shows the default Tk menu when a - path browser, class browser or debugger is the frontmost window on MacOS X - -- EditorWindow.test() was failing. Bug 1417598 - -- EditorWindow failed when used stand-alone if sys.ps1 not set. - Bug 1010370 Dave Florek - -- Tooltips failed on new-syle class __init__ args. Bug 1027566 Loren Guthrie - -- Avoid occasional failure to detect closing paren properly. - Patch 1407280 Tal Einat - -- Rebinding Tab key was inserting 'tab' instead of 'Tab'. Bug 1179168. - -- Colorizer now handles #<builtin> correctly, also unicode strings and - 'as' keyword in comment directly following import command. Closes 1325071. - Patch 1479219 Tal Einat - -- Patch #1162825: Support non-ASCII characters in IDLE window titles. - -- Source file f.flush() after writing; trying to avoid lossage if user - kills GUI. - -- Options / Keys / Advanced dialog made functional. Also, allow binding - of 'movement' keys. - -- 'syntax' patch adds improved calltips and a new class attribute listbox. - MultiCall module allows binding multiple actions to an event. - Patch 906702 Noam Raphael - -- Better indentation after first line of string continuation. - IDLEfork Patch 681992, Noam Raphael - -- Fixed CodeContext alignment problem, following suggestion from Tal Einat. - -- Increased performance in CodeContext extension Patch 936169 Noam Raphael - -- Mac line endings were incorrect when pasting code from some browsers - when using X11 and the Fink distribution. Python Bug 1263656. - -- <Enter> when cursor is on a previous command retrieves that command. Instead - of replacing the input line, the previous command is now appended to the - input line. Indentation is preserved, and undo is enabled. - Patch 1196917 Jeff Shute - -- Clarify "tab/space" Error Dialog and "Tab Width" Dialog associated with - the Untabify command. - -- Corrected "tab/space" Error Dialog to show correct menu for Untabify. - Patch 1196980 Jeff Shute - -- New files are colorized by default, and colorizing is removed when - saving as non-Python files. Patch 1196895 Jeff Shute - Closes Python Bugs 775012 and 800432, partial fix IDLEfork 763524 - -- Improve subprocess link error notification. - -- run.py: use Queue's blocking feature instead of sleeping in the main - loop. Patch # 1190163 Michiel de Hoon - -- Add config-main option to make the 'history' feature non-cyclic. - Default remains cyclic. Python Patch 914546 Noam Raphael. - -- Removed ability to configure tabs indent from Options dialog. This 'feature' - has never worked and no one has complained. It is still possible to set a - default tabs (v. spaces) indent 'manually' via config-main.def (or to turn on - tabs for the current EditorWindow via the Format menu) but IDLE will - encourage indentation via spaces. - -- Enable setting the indentation width using the Options dialog. - Bug # 783877 - -- Add keybindings for del-word-left and del-word-right. - -- Discourage using an indent width other than 8 when using tabs to indent - Python code. - -- Restore use of EditorWindow.set_indentation_params(), was dead code since - Autoindent was merged into EditorWindow. This allows IDLE to conform to the - indentation width of a loaded file. (But it still will not switch to tabs - even if the file uses tabs.) Any change in indent width is local to that - window. - -- Add Tabnanny check before Run/F5, not just when Checking module. - -- If an extension can't be loaded, print warning and skip it instead of - erroring out. - -- Improve error handling when .idlerc can't be created (warn and exit). - -- The GUI was hanging if the shell window was closed while a raw_input() - was pending. Restored the quit() of the readline() mainloop(). - http://mail.python.org/pipermail/idle-dev/2004-December/002307.html - -- 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. - -- 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. - -- 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. - -- 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. - -- Improve keyboard focus binding, especially in Windows menu. Improve - window raising, especially in the Windows menu and in the debugger. - IDLEfork 763524. - -- If user passes a non-existent filename on the commandline, just - open a new file, don't raise a dialog. IDLEfork 854928. - -- 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. - -- 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. - -- CodeContext hint pane visibility state is now persistent across sessions. - The pane no longer appears in the shell window. Added capability to limit - extensions to shell window or editor windows. Noam Raphael addition - to Patch 936169. - -- Paragraph reformat width is now a configurable parameter in the - Options GUI. - -- New Extension: CodeContext. Provides block structuring hints for code - which has scrolled above an edit window. Patch 936169 Noam Raphael. - -- If nulls somehow got into the strings in recent-files.lst - EditorWindow.update_recent_files_list() was failing. Python Bug 931336. - -- If the normal background is changed via Configure/Highlighting, it will - update immediately, thanks to the previously mentioned patch by Nigel Rowe. - -- Add a highlight theme for builtin keywords. Python Patch 805830 Nigel Rowe - This also fixed IDLEfork bug [ 693418 ] Normal text background color not - refreshed and Python bug [897872 ] Unknown color name on HP-UX - -- rpc.py:SocketIO - Large modules were generating large pickles when downloaded - to the execution server. The return of the OK response from the subprocess - initialization was interfering and causing the sending socket to be not - ready. Add an IO ready test to fix this. Moved the polling IO ready test - into pollpacket(). - -- Fix typo in rpc.py, s/b "pickle.PicklingError" not "pickle.UnpicklingError". - -- Added a Tk error dialog to run.py inform the user if the subprocess can't - connect to the user GUI process. Added a timeout to the GUI's listening - socket. Added Tk error dialogs to PyShell.py to announce a failure to bind - the port or connect to the subprocess. Clean up error handling during - connection initiation phase. This is an update of Python Patch 778323. - -- Print correct exception even if source file changed since shell was - restarted. IDLEfork Patch 869012 Noam Raphael - -- Keybindings with the Shift modifier now work correctly. So do bindings which - use the Space key. Limit unmodified user keybindings to the function keys. - Python Bug 775353, IDLEfork Bugs 755647, 761557 - -- After an exception, run.py was not setting the exception vector. Noam - Raphael suggested correcting this so pdb's postmortem pm() would work. - IDLEfork Patch 844675 - -- IDLE now does not fail to save the file anymore if the Tk buffer is not a - Unicode string, yet eol_convention is. Python Bugs 774680, 788378 - -- IDLE didn't start correctly when Python was installed in "Program Files" on - W2K and XP. Python Bugs 780451, 784183 - -- config-main.def documentation incorrectly referred to idle- instead of - config- filenames. SF 782759 Also added note about .idlerc location. - - -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. - -- Calltip error when docstring was None Python Bug 775541 - -- Updated extend.txt, help.txt, and config-extensions.def to correctly - reflect the current status of the configuration system. Python Bug 768469 - -- Fixed: Call Tip Trimming May Loop Forever. Python Patch 769142 (Daniels) - -- Replaced apply(f, args, kwds) with f(*args, **kwargs) to improve performance - Python Patch 768187 - -- Break or continue statements outside a loop were causing IDLE crash - Python Bug 767794 - -- Convert Unicode strings from readline to IOBinding.encoding. Also set - sys.std{in|out|err}.encoding, for both the local and the subprocess case. - SF IDLEfork patch 682347. - -- Extend AboutDialog.ViewFile() to support file encodings. Make the CREDITS - file Latin-1. - -- Updated the About dialog to reflect re-integration into Python. Provide - buttons to display Python's NEWS, License, and Credits, plus additional - buttons for IDLE's README and NEWS. - -- TextViewer() now has a third parameter which allows inserting text into the - viewer instead of reading from a file. - -- (Created the .../Lib/idlelib directory in the Python CVS, which is a clone of - IDLEfork modified to install in the Python environment. The code in the - interrupt module has been moved to thread.interrupt_main(). ) - -- Printing the Shell window was failing if it was not saved first SF 748975 - -- When using the Search in Files dialog, if the user had a selection - highlighted in his Editor window, insert it into the dialog search field. - -- The Python Shell entry was disappearing from the Windows menu. - -- Update the Windows file list when a file name change occurs - -- Change to File / Open Module: always pop up the dialog, using the current - selection as the default value. This is easier to use habitually. - -- Avoided a problem with starting the subprocess when 'localhost' doesn't - resolve to the user's loopback interface. SF 747772 - -- Fixed an issue with highlighted errors never de-colorizing. SF 747677. Also - improved notification of Tabnanny Token Error. - -- File / New will by default save in the directory of the Edit window from - which it was initiated. SF 748973 Guido van Rossum patch. - - -What's New in IDLEfork 0.9b1? -============================= -*Release date: 02-Jun-2003* - -- The current working directory of the execution environment (and shell - following completion of execution) is now that of the module being run. - -- Added the delete-exitfunc option to config-main.def. (This option is not - included in the Options dialog.) Setting this to True (the default) will - cause IDLE to not run sys.exitfunc/atexit when the subprocess exits. - -- IDLE now preserves the line ending codes when editing a file produced on - a different platform. SF 661759, SF 538584 - -- Reduced default editor font size to 10 point and increased window height - to provide a better initial impression on Windows. - -- Options / Fonts/Tabs / Set Base Editor Font: List box was not highlighting - the default font when first installed on Windows. SF 661676 - -- Added Autosave feature: when user runs code from edit window, if the file - has been modified IDLE will silently save it if Autosave is enabled. The - option is set in the Options dialog, and the default is to prompt the - user to save the file. SF 661318 Bruce Sherwood patch. - -- Improved the RESTART annotation in the shell window when the user restarts - the shell while it is generating output. Also improved annotation when user - repeatedly hammers the Ctrl-F6 restart. - -- Allow IDLE to run when not installed and cwd is not the IDLE directory - SF Patch 686254 "Run IDLEfork from any directory without set-up" - Raphael - -- When a module is run from an EditorWindow: if its directory is not in - sys.path, prepend it. This allows the module to import other modules in - the same directory. Do the same for a script run from the command line. - -- Correctly restart the subprocess if it is running user code and the user - attempts to run some other module or restarts the shell. Do the same if - the link is broken and it is possible to restart the subprocess and re- - connect to the GUI. SF RFE 661321. - -- Improved exception reporting when running commands or scripts from the - command line. - -- Added a -n command line switch to start IDLE without the subprocess. - Removed the Shell menu when running in that mode. Updated help messages. - -- Added a comment to the shell startup header to indicate when IDLE is not - using the subprocess. - -- Restore the ability to run without the subprocess. This can be important for - some platforms or configurations. (Running without the subprocess allows the - debugger to trace through parts of IDLE itself, which may or may not be - desirable, depending on your point of view. In addition, the traditional - reload/import tricks must be use if user source code is changed.) This is - helpful for developing IDLE using IDLE, because one instance can be used to - edit the code and a separate instance run to test changes. (Multiple - concurrent IDLE instances with subprocesses is a future feature) - -- Improve the error message a user gets when saving a file with non-ASCII - characters and no source encoding is specified. Done by adding a dialog - 'EncodingMessage', which contains the line to add in a fixed-font entry - widget, and which has a button to add that line to the file automatically. - Also, add a configuration option 'EditorWindow/encoding', which has three - possible values: none, utf-8, and locale. None is the default: IDLE will show - this dialog when non-ASCII characters are encountered. utf-8 means that files - with non-ASCII characters are saved as utf-8-with-bom. locale means that - files are saved in the locale's encoding; the dialog is only displayed if the - source contains characters outside the locale's charset. SF 710733 - Loewis - -- Improved I/O response by tweaking the wait parameter in various - calls to signal.signal(). - -- Implemented a threaded subprocess which allows interrupting a pass - loop in user code using the 'interrupt' extension. User code runs - in MainThread, while the RPCServer is handled by SockThread. This is - necessary because Windows doesn't support signals. - -- Implemented the 'interrupt' extension module, which allows a subthread - to raise a KeyboardInterrupt in the main thread. - -- Attempting to save the shell raised an error related to saving - breakpoints, which are not implemented in the shell - -- Provide a correct message when 'exit' or 'quit' are entered at the - IDLE command prompt SF 695861 - -- Eliminate extra blank line in shell output caused by not flushing - stdout when user code ends with an unterminated print. SF 695861 - -- Moved responsibility for exception formatting (i.e. pruning IDLE internal - calls) out of rpc.py into the client and server. - -- Exit IDLE cleanly even when doing subprocess I/O - -- Handle subprocess interrupt with an RPC message. - -- Restart the subprocess if it terminates itself. (VPython programs do that) - -- Support subclassing of exceptions, including in the shell, by moving the - 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. - -- Improved formatting in IDLE Help. - -- Run menu: Replace "Run Script" with "Run Module". - -- Code encountering an unhandled exception under the debugger now shows - the correct traceback, with IDLE internal levels pruned out. - -- If an exception occurs entirely in IDLE, don't prune the IDLE internal - modules from the traceback displayed. - -- Class Browser and Path Browser now use Alt-Key-2 for vertical zoom. - -- IDLE icons will now install correctly even when setup.py is run from the - build directory - -- Class Browser now compatible with Python2.3 version of pyclbr.py - -- Left cursor move in presence of selected text now moves from left end - of the selection. - -- Add Meta keybindings to "IDLE Classic Windows" to handle reversed - Alt/Meta on some Linux distros. - -- Change default: IDLE now starts with Python Shell. - -- Removed the File Path from the Additional Help Sources scrolled list. - -- Add capability to access Additional Help Sources on the web if the - Help File Path begins with //http or www. (Otherwise local path is - validated, as before.) - -- Additional Help Sources were not being posted on the Help menu in the - order entered. Implement sorting the list by [HelpFiles] 'option' - number. - -- Add Browse button to New Help Source dialog. Arrange to start in - Python/Doc if platform is Windows, otherwise start in current directory. - -- Put the Additional Help Sources directly on the Help menu instead of in - an Extra Help cascade menu. Rearrange the Help menu so the Additional - Help Sources come last. Update help.txt appropriately. - -- Fix Tk root pop-ups in configSectionNameDialog.py and configDialog.py - -- Uniform capitalization in General tab of ConfigDialog, update the doc string. - -- Fix bug in ConfigDialog where SaveAllChangedConfig() was unexpectedly - deleting Additional Help Sources from the user's config file. - -- Make configHelpSourceEdit OK button the default and bind <Return> - -- Fix Tk root pop-ups in configHelpSourceEdit: error dialogs not attached - to parents. - -- Use os.startfile() to open both Additional Help and Python Help on the - Windows platform. The application associated with the file type will act as - the viewer. Windows help files (.chm) are now supported via the - Settings/General/Additional Help facility. - -- If Python Help files are installed locally on Linux, use them instead of - accessing python.org. - -- Make the methods for finding the Python help docs more robust, and make - them work in the installed configuration, also. - -- On the Save Before Run dialog, make the OK button the default. One - less mouse action! - -- Add a method: EditorWindow.get_geometry() for future use in implementing - window location persistence. - -- Removed the "Help/Advice" menu entry. Thanks, David! We'll remember! - -- Change the "Classic Windows" theme's paste key to be <ctrl-v>. - -- Rearrange the Shell menu to put Stack Viewer entries adjacent. - -- Add the ability to restart the subprocess interpreter from the shell window; - add an associated menu entry "Shell/Restart" with binding Control-F6. Update - IDLE help. - -- Upon a restart, annotate the shell window with a "restart boundary". Add a - shell window menu "Shell/View Restart" with binding F6 to jump to the most - recent restart boundary. - -- Add Shell menu to Python Shell; change "Settings" to "Options". - -- Remove incorrect comment in setup.py: IDLEfork is now installed as a package. - -- Add INSTALL.txt, HISTORY.txt, NEWS.txt to installed configuration. - -- In installer text, fix reference to Visual Python, should be VPython. - Properly credit David Scherer. - -- Modified idle, idle.py, idle.pyw to improve exception handling. - - -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 - Idle-dev and/or the Sourceforge CVS. - -- Adapted to the Mac platform. - -- Overhauled the IDLE startup options and revised the idle -h help message, - which provides details of command line usage. - -- Multiple bug fixes and usability enhancements. - -- Introduced the new RPC implementation, which includes a debugger. The output - of user code is to the shell, and the shell may be used to inspect the - environment after the run has finished. (In version 0.8.1 the shell - environment was separate from the environment of the user code.) - -- Introduced the configuration GUI and a new About dialog. - -- Removed David Scherer's Remote Procedure Call code and replaced with Guido - van Rossum's. GvR code has support for the IDLE debugger and uses the shell - to inspect the environment of code Run from an Edit window. Files removed: - ExecBinding.py, loader.py, protocol.py, Remote.py, spawn.py - --------------------------------------------------------------------- -Refer to HISTORY.txt for additional information on earlier releases. --------------------------------------------------------------------- diff --git a/Lib/idlelib/debugobj.py b/Lib/idlelib/ObjectBrowser.py index 5a4c997..e69365c 100644 --- a/Lib/idlelib/debugobj.py +++ b/Lib/idlelib/ObjectBrowser.py @@ -8,9 +8,12 @@ # XXX TO DO: # - for classes/modules, add "open source" to object browser -from reprlib import Repr -from idlelib.tree import TreeItem, TreeNode, ScrolledCanvas +import re + +from idlelib.TreeWidget import TreeItem, TreeNode, ScrolledCanvas + +from repr import Repr myrepr = Repr() myrepr.maxstring = 100 @@ -56,6 +59,15 @@ class ObjectTreeItem(TreeItem): sublist.append(item) return sublist +class InstanceTreeItem(ObjectTreeItem): + def IsExpandable(self): + return True + def GetSubList(self): + sublist = ObjectTreeItem.GetSubList(self) + sublist.insert(0, + make_objecttreeitem("__class__ =", self.object.__class__)) + return sublist + class ClassTreeItem(ObjectTreeItem): def IsExpandable(self): return True @@ -71,7 +83,7 @@ class ClassTreeItem(ObjectTreeItem): class AtomicObjectTreeItem(ObjectTreeItem): def IsExpandable(self): - return False + return 0 class SequenceTreeItem(ObjectTreeItem): def IsExpandable(self): @@ -93,21 +105,25 @@ class SequenceTreeItem(ObjectTreeItem): class DictTreeItem(SequenceTreeItem): def keys(self): - keys = list(self.object.keys()) + keys = self.object.keys() try: keys.sort() except: pass return keys +from types import * + dispatch = { - int: AtomicObjectTreeItem, - float: AtomicObjectTreeItem, - str: AtomicObjectTreeItem, - tuple: SequenceTreeItem, - list: SequenceTreeItem, - dict: DictTreeItem, - type: ClassTreeItem, + IntType: AtomicObjectTreeItem, + LongType: AtomicObjectTreeItem, + FloatType: AtomicObjectTreeItem, + StringType: AtomicObjectTreeItem, + TupleType: SequenceTreeItem, + ListType: SequenceTreeItem, + DictType: DictTreeItem, + InstanceType: InstanceTreeItem, + ClassType: ClassTreeItem, } def make_objecttreeitem(labeltext, object, setfunction=None): @@ -119,24 +135,22 @@ def make_objecttreeitem(labeltext, object, setfunction=None): return c(labeltext, object, setfunction) -def _object_browser(parent): # htest # +def _object_browser(parent): import sys - from tkinter import Toplevel - top = Toplevel(parent) - top.title("Test debug object browser") - x, y = map(int, parent.geometry().split('+')[1:]) - top.geometry("+%d+%d" % (x + 100, y + 175)) - top.configure(bd=0, bg="yellow") - top.focus_set() - sc = ScrolledCanvas(top, bg="white", highlightthickness=0, takefocus=1) + 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) sc.frame.pack(expand=1, fill="both") item = make_objecttreeitem("sys", sys) node = TreeNode(sc.canvas, None, item) node.update() + root.mainloop() if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_debugobj', verbosity=2, exit=False) - from idlelib.idle_test.htest import run run(_object_browser) diff --git a/Lib/idlelib/OutputWindow.py b/Lib/idlelib/OutputWindow.py new file mode 100644 index 0000000..63dc737 --- /dev/null +++ b/Lib/idlelib/OutputWindow.py @@ -0,0 +1,149 @@ +from Tkinter import * +from idlelib.EditorWindow import EditorWindow +import re +import tkMessageBox +from idlelib import IOBinding + +class OutputWindow(EditorWindow): + + """An editor window that can serve as an output file. + + Also the future base class for the Python shell window. + This class has no input facilities. + """ + + def __init__(self, *args): + EditorWindow.__init__(self, *args) + self.text.bind("<<goto-file-line>>", self.goto_file_line) + + # Customize EditorWindow + + def ispythonsource(self, filename): + # No colorization needed + return 0 + + def short_title(self): + return "Output" + + def maybesave(self): + # Override base class method -- don't ask any questions + if self.get_saved(): + return "yes" + else: + return "no" + + # Act as output file + + def write(self, s, tags=(), mark="insert"): + # Tk assumes that byte strings are Latin-1; + # we assume that they are in the locale's encoding + if isinstance(s, str): + try: + s = unicode(s, IOBinding.encoding) + except UnicodeError: + # some other encoding; let Tcl deal with it + pass + self.text.insert(mark, s, tags) + self.text.see(mark) + self.text.update() + + def writelines(self, lines): + for line in lines: + self.write(line) + + def flush(self): + pass + + # Our own right-button menu + + rmenu_specs = [ + ("Cut", "<<cut>>", "rmenu_check_cut"), + ("Copy", "<<copy>>", "rmenu_check_copy"), + ("Paste", "<<paste>>", "rmenu_check_paste"), + (None, None, None), + ("Go to file/line", "<<goto-file-line>>", None), + ] + + file_line_pats = [ + # order of patterns matters + r'file "([^"]*)", line (\d+)', + r'([^\s]+)\((\d+)\)', + r'^(\s*\S.*?):\s*(\d+):', # Win filename, maybe starting with spaces + r'([^\s]+):\s*(\d+):', # filename or path, ltrim + r'^\s*(\S.*?):\s*(\d+):', # Win abs path with embedded spaces, ltrim + ] + + file_line_progs = None + + def goto_file_line(self, event=None): + if self.file_line_progs is None: + l = [] + for pat in self.file_line_pats: + l.append(re.compile(pat, re.IGNORECASE)) + self.file_line_progs = l + # x, y = self.event.x, self.event.y + # self.text.mark_set("insert", "@%d,%d" % (x, y)) + line = self.text.get("insert linestart", "insert lineend") + result = self._file_line_helper(line) + if not result: + # Try the previous line. This is handy e.g. in tracebacks, + # where you tend to right-click on the displayed source line + line = self.text.get("insert -1line linestart", + "insert -1line lineend") + result = self._file_line_helper(line) + if not result: + tkMessageBox.showerror( + "No special line", + "The line you point at doesn't look like " + "a valid file name followed by a line number.", + parent=self.text) + return + filename, lineno = result + edit = self.flist.open(filename) + edit.gotoline(lineno) + + def _file_line_helper(self, line): + for prog in self.file_line_progs: + match = prog.search(line) + if match: + filename, lineno = match.group(1, 2) + try: + f = open(filename, "r") + f.close() + break + except IOError: + continue + else: + return None + try: + return filename, int(lineno) + except TypeError: + return None + +# These classes are currently not used but might come in handy + +class OnDemandOutputWindow: + + tagdefs = { + # XXX Should use IdlePrefs.ColorPrefs + "stdout": {"foreground": "blue"}, + "stderr": {"foreground": "#007700"}, + } + + def __init__(self, flist): + self.flist = flist + self.owin = None + + def write(self, s, tags, mark): + if not self.owin: + self.setup() + self.owin.write(s, tags, mark) + + def setup(self): + self.owin = owin = OutputWindow(self.flist) + text = owin.text + for tag, cnf in self.tagdefs.items(): + if cnf: + text.tag_configure(tag, **cnf) + text.tag_raise('sel') + self.write = self.owin.write diff --git a/Lib/idlelib/parenmatch.py b/Lib/idlelib/ParenMatch.py index 3fd7aad..47e10f3 100644 --- a/Lib/idlelib/parenmatch.py +++ b/Lib/idlelib/ParenMatch.py @@ -1,35 +1,54 @@ -"""ParenMatch -- for parenthesis matching. +"""ParenMatch -- An IDLE extension for parenthesis matching. When you hit a right paren, the cursor should move briefly to the left paren. Paren here is used generically; the matching applies to parentheses, square brackets, and curly braces. """ -from idlelib.hyperparser import HyperParser -from idlelib.config import idleConf + +from idlelib.HyperParser import HyperParser +from idlelib.configHandler import idleConf _openers = {')':'(',']':'[','}':'{'} CHECK_DELAY = 100 # milliseconds class ParenMatch: - """Highlight matching openers and closers, (), [], and {}. + """Highlight matching parentheses + + There are three supported style of paren matching, based loosely + on the Emacs options. The style is select based on the + HILITE_STYLE attribute; it can be changed used the set_style + method. - There are three supported styles of paren matching. When a right - paren (opener) is typed: + The supported styles are: - opener -- highlight the matching left paren (closer); - parens -- highlight the left and right parens (opener and closer); - expression -- highlight the entire expression from opener to closer. - (For back compatibility, 'default' is a synonym for 'opener'). + default -- When a right paren is typed, highlight the matching + left paren for 1/2 sec. - Flash-delay is the maximum milliseconds the highlighting remains. - Any cursor movement (key press or click) before that removes the - highlight. If flash-delay is 0, there is no maximum. + expression -- When a right paren is typed, highlight the entire + expression from the left paren to the right paren. TODO: - - Augment bell() with mismatch warning in status window. - - Highlight when cursor is moved to the right of a closer. - This might be too expensive to check. + - extend IDLE with configuration dialog to change options + - implement rest of Emacs highlight styles (see below) + - print mismatch warning in IDLE status window + + Note: In Emacs, there are several styles of highlight where the + matching paren is highlighted whenever the cursor is immediately + to the right of a right paren. I don't know how to do that in Tk, + so I haven't bothered. """ + menudefs = [ + ('edit', [ + ("Show surrounding parens", "<<flash-paren>>"), + ]) + ] + STYLE = idleConf.GetOption('extensions','ParenMatch','style', + default='expression') + FLASH_DELAY = idleConf.GetOption('extensions','ParenMatch','flash-delay', + type='int',default=500) + HILITE_CONFIG = idleConf.GetHighlight(idleConf.CurrentTheme(),'hilite') + BELL = idleConf.GetOption('extensions','ParenMatch','bell', + type='bool',default=1) RESTORE_VIRTUAL_EVENT_NAME = "<<parenmatch-check-restore>>" # We want the restore event be called before the usual return and @@ -47,42 +66,41 @@ class ParenMatch: self.restore_event) self.counter = 0 self.is_restore_active = 0 - - @classmethod - def reload(cls): - cls.STYLE = idleConf.GetOption( - 'extensions','ParenMatch','style', default='opener') - cls.FLASH_DELAY = idleConf.GetOption( - 'extensions','ParenMatch','flash-delay', type='int',default=500) - cls.BELL = idleConf.GetOption( - 'extensions','ParenMatch','bell', type='bool', default=1) - cls.HILITE_CONFIG = idleConf.GetHighlight(idleConf.CurrentTheme(), - 'hilite') + self.set_style(self.STYLE) def activate_restore(self): - "Activate mechanism to restore text from highlighting." if not self.is_restore_active: for seq in self.RESTORE_SEQUENCES: self.text.event_add(self.RESTORE_VIRTUAL_EVENT_NAME, seq) self.is_restore_active = True def deactivate_restore(self): - "Remove restore event bindings." if self.is_restore_active: for seq in self.RESTORE_SEQUENCES: self.text.event_delete(self.RESTORE_VIRTUAL_EVENT_NAME, seq) self.is_restore_active = False + def set_style(self, style): + self.STYLE = style + if style == "default": + self.create_tag = self.create_tag_default + self.set_timeout = self.set_timeout_last + elif style == "expression": + self.create_tag = self.create_tag_expression + self.set_timeout = self.set_timeout_none + def flash_paren_event(self, event): - "Handle editor 'show surrounding parens' event (menu or shortcut)." indices = (HyperParser(self.editwin, "insert") .get_surrounding_brackets()) - self.finish_paren_event(indices) - return "break" + if indices is None: + self.warn_mismatched() + return + self.activate_restore() + self.create_tag(indices) + self.set_timeout_last() def paren_closed_event(self, event): - "Handle user input of closer." - # If user bound non-closer to <<paren-closed>>, quit. + # If it was a shortcut and not really a closing paren, quit. closer = self.text.get("insert-1c") if closer not in _openers: return @@ -90,22 +108,14 @@ class ParenMatch: if not hp.is_in_code(): return indices = hp.get_surrounding_brackets(_openers[closer], True) - self.finish_paren_event(indices) - return # Allow calltips to see ')' - - def finish_paren_event(self, indices): - if indices is None and self.BELL: - self.text.bell() + if indices is None: + self.warn_mismatched() return self.activate_restore() - # self.create_tag(indices) - self.tagfuncs.get(self.STYLE, self.create_tag_expression)(self, indices) - # self.set_timeout() - (self.set_timeout_last if self.FLASH_DELAY else - self.set_timeout_none)() + self.create_tag(indices) + self.set_timeout() def restore_event(self, event=None): - "Remove effect of doing match." self.text.tag_delete("paren") self.deactivate_restore() self.counter += 1 # disable the last timer, if there is one. @@ -114,23 +124,18 @@ class ParenMatch: if timer_count == self.counter: self.restore_event() + def warn_mismatched(self): + if self.BELL: + self.text.bell() + # any one of the create_tag_XXX methods can be used depending on # the style - def create_tag_opener(self, indices): + def create_tag_default(self, indices): """Highlight the single paren that matches""" self.text.tag_add("paren", indices[0]) self.text.tag_config("paren", self.HILITE_CONFIG) - def create_tag_parens(self, indices): - """Highlight the left and right parens""" - if self.text.get(indices[1]) in (')', ']', '}'): - rightindex = indices[1]+"+1c" - else: - rightindex = indices[1] - self.text.tag_add("paren", indices[0], indices[0]+"+1c", rightindex+"-1c", rightindex) - self.text.tag_config("paren", self.HILITE_CONFIG) - def create_tag_expression(self, indices): """Highlight the entire expression""" if self.text.get(indices[1]) in (')', ']', '}'): @@ -140,13 +145,6 @@ class ParenMatch: self.text.tag_add("paren", indices[0], rightindex) self.text.tag_config("paren", self.HILITE_CONFIG) - tagfuncs = { - 'opener': create_tag_opener, - 'default': create_tag_opener, - 'parens': create_tag_parens, - 'expression': create_tag_expression, - } - # any one of the set_timeout_XXX methods can be used depending on # the style @@ -166,7 +164,7 @@ class ParenMatch: self.editwin.text_frame.after(CHECK_DELAY, callme, callme) def set_timeout_last(self): - """The last highlight created will be removed after FLASH_DELAY millisecs""" + """The last highlight created will be removed after .5 sec""" # associate a counter with an event; only disable the "paren" # tag if the event is for the most recent timer. self.counter += 1 @@ -175,9 +173,6 @@ class ParenMatch: lambda self=self, c=self.counter: self.handle_restore_timer(c)) -ParenMatch.reload() - - if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_parenmatch', verbosity=2) + import unittest + unittest.main('idlelib.idle_test.test_parenmatch', verbosity=2) diff --git a/Lib/idlelib/pathbrowser.py b/Lib/idlelib/PathBrowser.py index 6de242d..ae26714 100644 --- a/Lib/idlelib/pathbrowser.py +++ b/Lib/idlelib/PathBrowser.py @@ -1,21 +1,20 @@ -import importlib.machinery import os import sys +import imp -from idlelib.browser import ModuleBrowser, ModuleBrowserTreeItem -from idlelib.tree import TreeItem +from idlelib.TreeWidget import TreeItem +from idlelib.ClassBrowser import ClassBrowser, ModuleBrowserTreeItem +from idlelib.PyShell import PyShellFileList -class PathBrowser(ModuleBrowser): +class PathBrowser(ClassBrowser): - def __init__(self, master, *, _htest=False, _utest=False): + def __init__(self, flist, _htest=False): """ _htest - bool, change box location when running htest """ - self.master = master self._htest = _htest - self._utest = _utest - self.init() + self.init(flist) def settitle(self): "Set window titles." @@ -25,7 +24,6 @@ class PathBrowser(ModuleBrowser): def rootnode(self): return PathBrowserTreeItem() - class PathBrowserTreeItem(TreeItem): def GetText(self): @@ -38,7 +36,6 @@ class PathBrowserTreeItem(TreeItem): sublist.append(item) return sublist - class DirBrowserTreeItem(TreeItem): def __init__(self, dir, packages=[]): @@ -54,7 +51,7 @@ class DirBrowserTreeItem(TreeItem): def GetSubList(self): try: names = os.listdir(self.dir or os.curdir) - except OSError: + except os.error: return [] packages = [] for name in names: @@ -73,7 +70,6 @@ class DirBrowserTreeItem(TreeItem): return sublist def ispackagedir(self, file): - " Return true for directories that are packages." if not os.path.isdir(file): return False init = os.path.join(file, "__init__.py") @@ -81,11 +77,9 @@ class DirBrowserTreeItem(TreeItem): def listmodules(self, allnames): modules = {} - suffixes = importlib.machinery.EXTENSION_SUFFIXES[:] - suffixes += importlib.machinery.SOURCE_SUFFIXES - suffixes += importlib.machinery.BYTECODE_SUFFIXES + suffixes = imp.get_suffixes() sorted = [] - for suff in suffixes: + for suff, mode, flag in suffixes: i = -len(suff) for name in allnames[:]: normed_name = os.path.normcase(name) @@ -98,9 +92,9 @@ class DirBrowserTreeItem(TreeItem): sorted.sort() return sorted - def _path_browser(parent): # htest # - PathBrowser(parent, _htest=True) + flist = PyShellFileList(parent) + PathBrowser(flist, _htest=True) parent.mainloop() if __name__ == "__main__": diff --git a/Lib/idlelib/percolator.py b/Lib/idlelib/Percolator.py index db70304..e0e8cad 100644 --- a/Lib/idlelib/percolator.py +++ b/Lib/idlelib/Percolator.py @@ -1,6 +1,5 @@ -from idlelib.delegator import Delegator -from idlelib.redirector import WidgetRedirector - +from idlelib.WidgetRedirector import WidgetRedirector +from idlelib.Delegator import Delegator class Percolator: @@ -17,10 +16,8 @@ class Percolator: while self.top is not self.bottom: self.removefilter(self.top) self.top = None - self.bottom.setdelegate(None) - self.bottom = None - self.redir.close() - self.redir = None + self.bottom.setdelegate(None); self.bottom = None + self.redir.close(); self.redir = None self.text = None def insert(self, index, chars, tags=None): @@ -55,49 +52,52 @@ class Percolator: filter.setdelegate(None) -def _percolator(parent): # htest # - import tkinter as tk - +def _percolator(parent): + import Tkinter as tk + import re class Tracer(Delegator): def __init__(self, name): self.name = name Delegator.__init__(self, None) - def insert(self, *args): - print(self.name, ": insert", args) + print self.name, ": insert", args self.delegate.insert(*args) - def delete(self, *args): - print(self.name, ": delete", args) + print self.name, ": delete", args self.delegate.delete(*args) - - box = tk.Toplevel(parent) - box.title("Test Percolator") - x, y = map(int, parent.geometry().split('+')[1:]) - box.geometry("+%d+%d" % (x, y + 175)) - text = tk.Text(box) + 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) - pin = p.insertfilter - pout = p.removefilter t1 = Tracer("t1") t2 = Tracer("t2") def toggle1(): - (pin if var1.get() else pout)(t1) + if var1.get() == 0: + var1.set(1) + p.insertfilter(t1) + elif var1.get() == 1: + var1.set(0) + p.removefilter(t1) + def toggle2(): - (pin if var2.get() else pout)(t2) + 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(parent) - cb1 = tk.Checkbutton(box, text="Tracer1", command=toggle1, variable=var1) + var1 = tk.IntVar() + cb1 = tk.Checkbutton(root, text="Tracer1", command=toggle1, variable=var1) cb1.pack() - var2 = tk.IntVar(parent) - cb2 = tk.Checkbutton(box, text="Tracer2", command=toggle2, variable=var2) + var2 = tk.IntVar() + cb2 = tk.Checkbutton(root, text="Tracer2", command=toggle2, variable=var2) cb2.pack() if __name__ == "__main__": - from unittest import main - main('idlelib.idle_test.test_percolator', verbosity=2, exit=False) - from idlelib.idle_test.htest import run run(_percolator) diff --git a/Lib/idlelib/pyparse.py b/Lib/idlelib/PyParse.py index feb57cb..1a9db67 100644 --- a/Lib/idlelib/pyparse.py +++ b/Lib/idlelib/PyParse.py @@ -1,22 +1,15 @@ -"""Define partial Python code Parser used by editor and hyperparser. - -Instances of ParseMap are used with str.translate. - -The following bound search and match functions are defined: -_synchre - start of popular statement; -_junkre - whitespace or comment line; -_match_stringre: string, possibly without closer; -_itemre - line that may have bracket structure start; -_closere - line that must be followed by dedent. -_chew_ordinaryre - non-special characters. -""" import re +import sys -# Reason last statement is continued (or C_NONE if it's not). +# Reason last stmt is continued (or C_NONE if it's not). (C_NONE, C_BACKSLASH, C_STRING_FIRST_LINE, C_STRING_NEXT_LINES, C_BRACKET) = range(5) -# Find what looks like the start of a popular statement. +if 0: # for throwaway debugging output + def dump(*stuff): + sys.__stdout__.write(" ".join(map(str, stuff)) + "\n") + +# Find what looks like the start of a popular stmt. _synchre = re.compile(r""" ^ @@ -76,7 +69,7 @@ _itemre = re.compile(r""" [^\s#\\] # if we match, m.end()-1 is the interesting char """, re.VERBOSE).match -# Match start of statements that should be followed by a dedent. +# Match start of stmts that should be followed by a dedent. _closere = re.compile(r""" \s* @@ -98,29 +91,23 @@ _chew_ordinaryre = re.compile(r""" [^[\](){}#'"\\]+ """, re.VERBOSE).match +# Build translation table to map uninteresting chars to "x", open +# brackets to "(", and close brackets to ")". -class ParseMap(dict): - r"""Dict subclass that maps anything not in dict to 'x'. - - This is designed to be used with str.translate in study1. - Anything not specifically mapped otherwise becomes 'x'. - Example: replace everything except whitespace with 'x'. - - >>> keepwhite = ParseMap((ord(c), ord(c)) for c in ' \t\n\r') - >>> "a + b\tc\nd".translate(keepwhite) - 'x x x\tx\nx' - """ - # Calling this triples access time; see bpo-32940 - def __missing__(self, key): - return 120 # ord('x') - - -# Map all ascii to 120 to avoid __missing__ call, then replace some. -trans = ParseMap.fromkeys(range(128), 120) -trans.update((ord(c), ord('(')) for c in "({[") # open brackets => '('; -trans.update((ord(c), ord(')')) for c in ")}]") # close brackets => ')'. -trans.update((ord(c), ord(c)) for c in "\"'\\\n#") # Keep these. +_tran = ['x'] * 256 +for ch in "({[": + _tran[ord(ch)] = '(' +for ch in ")}]": + _tran[ord(ch)] = ')' +for ch in "\"'\\\n#": + _tran[ord(ch)] = ch +_tran = ''.join(_tran) +del ch +try: + UnicodeType = type(unicode("")) +except NameError: + UnicodeType = None class Parser: @@ -128,26 +115,38 @@ class Parser: self.indentwidth = indentwidth self.tabwidth = tabwidth - def set_code(self, s): - assert len(s) == 0 or s[-1] == '\n' - self.code = s + def set_str(self, str): + assert len(str) == 0 or str[-1] == '\n' + if type(str) is UnicodeType: + # 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 = str + str = [] + push = str.append + for raw in map(ord, uniphooey): + push(raw < 127 and chr(raw) or "x") + str = "".join(str) + self.str = str self.study_level = 0 + # Return index of a good place to begin parsing, as close to the + # end of the string as possible. This will be the start of some + # popular stmt like "if" or "def". Return None if none found: + # the caller should pass more prior context then, if possible, or + # if not (the entire program text up until the point of interest + # has already been tried) pass 0 to set_lo. + # + # This will be reliable iff given a reliable is_char_in_string + # function, meaning that when it says "no", it's absolutely + # guaranteed that the char is not in a string. + def find_good_parse_start(self, is_char_in_string=None, _synchre=_synchre): - """ - Return index of a good place to begin parsing, as close to the - end of the string as possible. This will be the start of some - popular stmt like "if" or "def". Return None if none found: - the caller should pass more prior context then, if possible, or - if not (the entire program text up until the point of interest - has already been tried) pass 0 to set_lo(). - - This will be reliable iff given a reliable is_char_in_string() - function, meaning that when it says "no", it's absolutely - guaranteed that the char is not in a string. - """ - code, pos = self.code, None + str, pos = self.str, None if not is_char_in_string: # no clue -- make the caller pass everything @@ -156,13 +155,13 @@ class Parser: # Peek back from the end for a good place to start, # but don't try too often; pos will be left None, or # bumped to a legitimate synch point. - limit = len(code) + limit = len(str) for tries in range(5): - i = code.rfind(":\n", 0, limit) + i = str.rfind(":\n", 0, limit) if i < 0: break - i = code.rfind('\n', 0, i) + 1 # start of colon line (-1+1=0) - m = _synchre(code, i, limit) + i = str.rfind('\n', 0, i) + 1 # start of colon line + m = _synchre(str, i, limit) if m and not is_char_in_string(m.start()): pos = m.start() break @@ -176,7 +175,7 @@ class Parser: # going to have to parse the whole thing to be sure, so # give it one last try from the start, but stop wasting # time here regardless of the outcome. - m = _synchre(code) + m = _synchre(str) if m and not is_char_in_string(m.start()): pos = m.start() return pos @@ -185,7 +184,7 @@ class Parser: # matches. i = pos + 1 while 1: - m = _synchre(code, i) + m = _synchre(str, i) if m: s, i = m.span() if not is_char_in_string(s): @@ -194,22 +193,19 @@ class Parser: break return pos - def set_lo(self, lo): - """ Throw away the start of the string. + # Throw away the start of the string. Intended to be called with + # find_good_parse_start's result. - Intended to be called with the result of find_good_parse_start(). - """ - assert lo == 0 or self.code[lo-1] == '\n' + def set_lo(self, lo): + assert lo == 0 or self.str[lo-1] == '\n' if lo > 0: - self.code = self.code[lo:] + self.str = self.str[lo:] - def _study1(self): - """Find the line numbers of non-continuation lines. + # As quickly as humanly possible <wink>, find the line numbers (0- + # based) of the non-continuation lines. + # Creates self.{goodlines, continuation}. - As quickly as humanly possible <wink>, find the line numbers (0- - based) of the non-continuation lines. - Creates self.{goodlines, continuation}. - """ + def _study1(self): if self.study_level >= 1: return self.study_level = 1 @@ -218,15 +214,15 @@ class Parser: # to "(", all close brackets to ")", then collapse runs of # uninteresting characters. This can cut the number of chars # by a factor of 10-40, and so greatly speed the following loop. - code = self.code - code = code.translate(trans) - code = code.replace('xxxxxxxx', 'x') - code = code.replace('xxxx', 'x') - code = code.replace('xx', 'x') - code = code.replace('xx', 'x') - code = code.replace('\nx', '\n') - # Replacing x\n with \n would be incorrect because - # x may be preceded by a backslash. + str = self.str + str = str.translate(_tran) + str = str.replace('xxxxxxxx', 'x') + str = str.replace('xxxx', 'x') + str = str.replace('xx', 'x') + str = str.replace('xx', 'x') + str = str.replace('\nx', '\n') + # note that replacing x\n with \n would be incorrect, because + # x may be preceded by a backslash # March over the squashed version of the program, accumulating # the line numbers of non-continued stmts, and determining @@ -235,9 +231,9 @@ class Parser: level = lno = 0 # level is nesting level; lno is line number self.goodlines = goodlines = [0] push_good = goodlines.append - i, n = 0, len(code) + i, n = 0, len(str) while i < n: - ch = code[i] + ch = str[i] i = i+1 # cases are checked in decreasing order of frequency @@ -264,19 +260,19 @@ class Parser: if ch == '"' or ch == "'": # consume the string quote = ch - if code[i-1:i+2] == quote * 3: + if str[i-1:i+2] == quote * 3: quote = quote * 3 firstlno = lno w = len(quote) - 1 i = i+w while i < n: - ch = code[i] + ch = str[i] i = i+1 if ch == 'x': continue - if code[i-1:i+w] == quote: + if str[i-1:i+w] == quote: i = i+w break @@ -291,7 +287,7 @@ class Parser: if ch == '\\': assert i < n - if code[i] == '\n': + if str[i] == '\n': lno = lno + 1 i = i+1 continue @@ -302,7 +298,7 @@ class Parser: # didn't break out of the loop, so we're still # inside a string if (lno - 1) == firstlno: - # before the previous \n in code, we were in the first + # before the previous \n in str, we were in the first # line of the string continuation = C_STRING_FIRST_LINE else: @@ -311,13 +307,13 @@ class Parser: if ch == '#': # consume the comment - i = code.find('\n', i) + i = str.find('\n', i) assert i >= 0 continue assert ch == '\\' assert i < n - if code[i] == '\n': + if str[i] == '\n': lno = lno + 1 if i+1 == n: continuation = C_BACKSLASH @@ -341,45 +337,44 @@ class Parser: self._study1() return self.continuation + # study1 was sufficient to determine the continuation status, + # but doing more requires looking at every character. study2 + # does this for the last interesting statement in the block. + # Creates: + # self.stmt_start, stmt_end + # slice indices of last interesting stmt + # self.stmt_bracketing + # the bracketing structure of the last interesting stmt; + # for example, for the statement "say(boo) or die", stmt_bracketing + # will be [(0, 0), (3, 1), (8, 0)]. Strings and comments are + # treated as brackets, for the matter. + # self.lastch + # last non-whitespace character before optional trailing + # comment + # self.lastopenbracketpos + # if continuation is C_BRACKET, index of last open bracket + def _study2(self): - """ - study1 was sufficient to determine the continuation status, - but doing more requires looking at every character. study2 - does this for the last interesting statement in the block. - Creates: - self.stmt_start, stmt_end - slice indices of last interesting stmt - self.stmt_bracketing - the bracketing structure of the last interesting stmt; for - example, for the statement "say(boo) or die", - stmt_bracketing will be ((0, 0), (0, 1), (2, 0), (2, 1), - (4, 0)). Strings and comments are treated as brackets, for - the matter. - self.lastch - last interesting character before optional trailing comment - self.lastopenbracketpos - if continuation is C_BRACKET, index of last open bracket - """ if self.study_level >= 2: return self._study1() self.study_level = 2 # Set p and q to slice indices of last interesting stmt. - code, goodlines = self.code, self.goodlines - i = len(goodlines) - 1 # Index of newest line. - p = len(code) # End of goodlines[i] + str, goodlines = self.str, self.goodlines + i = len(goodlines) - 1 + p = len(str) # index of newest line while i: assert p - # Make p be the index of the stmt at line number goodlines[i]. + # p is the index of the stmt at line number goodlines[i]. # Move p back to the stmt at line number goodlines[i-1]. q = p for nothing in range(goodlines[i-1], goodlines[i]): # tricky: sets p to 0 if no preceding newline - p = code.rfind('\n', 0, p-1) + 1 - # The stmt code[p:q] isn't a continuation, but may be blank + p = str.rfind('\n', 0, p-1) + 1 + # The stmt str[p:q] isn't a continuation, but may be blank # or a non-indenting comment line. - if _junkre(code, p): + if _junkre(str, p): i = i-1 else: break @@ -397,21 +392,21 @@ class Parser: bracketing = [(p, 0)] while p < q: # suck up all except ()[]{}'"#\\ - m = _chew_ordinaryre(code, p, q) + m = _chew_ordinaryre(str, p, q) if m: # we skipped at least one boring char newp = m.end() # back up over totally boring whitespace i = newp - 1 # index of last boring char - while i >= p and code[i] in " \t\n": + while i >= p and str[i] in " \t\n": i = i-1 if i >= p: - lastch = code[i] + lastch = str[i] p = newp if p >= q: break - ch = code[p] + ch = str[p] if ch in "([{": push_stack(p) @@ -438,14 +433,14 @@ class Parser: # have to. bracketing.append((p, len(stack)+1)) lastch = ch - p = _match_stringre(code, p, q).end() + p = _match_stringre(str, p, q).end() bracketing.append((p, len(stack))) continue if ch == '#': # consume comment and trailing newline bracketing.append((p, len(stack)+1)) - p = code.find('\n', p, q) + 1 + p = str.find('\n', p, q) + 1 assert p > 0 bracketing.append((p, len(stack))) continue @@ -453,78 +448,76 @@ class Parser: assert ch == '\\' p = p+1 # beyond backslash assert p < q - if code[p] != '\n': + if str[p] != '\n': # the program is invalid, but can't complain - lastch = ch + code[p] + lastch = ch + str[p] p = p+1 # beyond escaped char # end while p < q: self.lastch = lastch - self.lastopenbracketpos = stack[-1] if stack else None + if stack: + self.lastopenbracketpos = stack[-1] self.stmt_bracketing = tuple(bracketing) - def compute_bracket_indent(self): - """Return number of spaces the next line should be indented. + # Assuming continuation is C_BRACKET, return the number + # of spaces the next line should be indented. - Line continuation must be C_BRACKET. - """ + def compute_bracket_indent(self): self._study2() assert self.continuation == C_BRACKET j = self.lastopenbracketpos - code = self.code - n = len(code) - origi = i = code.rfind('\n', 0, j) + 1 + str = self.str + n = len(str) + origi = i = str.rfind('\n', 0, j) + 1 j = j+1 # one beyond open bracket # find first list item; set i to start of its line while j < n: - m = _itemre(code, j) + m = _itemre(str, j) if m: j = m.end() - 1 # index of first interesting char extra = 0 break else: # this line is junk; advance to next line - i = j = code.find('\n', j) + 1 + i = j = str.find('\n', j) + 1 else: # nothing interesting follows the bracket; # reproduce the bracket line's indentation + a level j = i = origi - while code[j] in " \t": + while str[j] in " \t": j = j+1 extra = self.indentwidth - return len(code[i:j].expandtabs(self.tabwidth)) + extra + return len(str[i:j].expandtabs(self.tabwidth)) + extra - def get_num_lines_in_stmt(self): - """Return number of physical lines in last stmt. + # Return number of physical lines in last stmt (whether or not + # it's an interesting stmt! this is intended to be called when + # continuation is C_BACKSLASH). - The statement doesn't have to be an interesting statement. This is - intended to be called when continuation is C_BACKSLASH. - """ + def get_num_lines_in_stmt(self): self._study1() goodlines = self.goodlines return goodlines[-1] - goodlines[-2] - def compute_backslash_indent(self): - """Return number of spaces the next line should be indented. + # Assuming continuation is C_BACKSLASH, return the number of spaces + # the next line should be indented. Also assuming the new line is + # the first one following the initial line of the stmt. - Line continuation must be C_BACKSLASH. Also assume that the new - line is the first one following the initial line of the stmt. - """ + def compute_backslash_indent(self): self._study2() assert self.continuation == C_BACKSLASH - code = self.code + str = self.str i = self.stmt_start - while code[i] in " \t": + while str[i] in " \t": i = i+1 startpos = i # See whether the initial line starts an assignment stmt; i.e., # look for an = operator - endpos = code.find('\n', startpos) + 1 + endpos = str.find('\n', startpos) + 1 found = level = 0 while i < endpos: - ch = code[i] + ch = str[i] if ch in "([{": level = level + 1 i = i+1 @@ -533,14 +526,12 @@ class Parser: level = level - 1 i = i+1 elif ch == '"' or ch == "'": - i = _match_stringre(code, i, endpos).end() + i = _match_stringre(str, i, endpos).end() elif ch == '#': - # This line is unreachable because the # makes a comment of - # everything after it. break elif level == 0 and ch == '=' and \ - (i == 0 or code[i-1] not in "=<>!") and \ - code[i+1] != '=': + (i == 0 or str[i-1] not in "=<>!") and \ + str[i+1] != '=': found = 1 break else: @@ -550,49 +541,54 @@ class Parser: # found a legit =, but it may be the last interesting # thing on the line i = i+1 # move beyond the = - found = re.match(r"\s*\\", code[i:endpos]) is None + found = re.match(r"\s*\\", str[i:endpos]) is None if not found: # oh well ... settle for moving beyond the first chunk # of non-whitespace chars i = startpos - while code[i] not in " \t\n": + while str[i] not in " \t\n": i = i+1 - return len(code[self.stmt_start:i].expandtabs(\ + return len(str[self.stmt_start:i].expandtabs(\ self.tabwidth)) + 1 + # Return the leading whitespace on the initial line of the last + # interesting stmt. + def get_base_indent_string(self): - """Return the leading whitespace on the initial line of the last - interesting stmt. - """ self._study2() i, n = self.stmt_start, self.stmt_end j = i - code = self.code - while j < n and code[j] in " \t": + str = self.str + while j < n and str[j] in " \t": j = j + 1 - return code[i:j] + return str[i:j] + + # Did the last interesting stmt open a block? def is_block_opener(self): - "Return True if the last interesting statement opens a block." self._study2() return self.lastch == ':' + # Did the last interesting stmt close a block? + def is_block_closer(self): - "Return True if the last interesting statement closes a block." self._study2() - return _closere(self.code, self.stmt_start) is not None + return _closere(self.str, self.stmt_start) is not None - def get_last_stmt_bracketing(self): - """Return bracketing structure of the last interesting statement. + # index of last open bracket ({[, or None if none + lastopenbracketpos = None - The returned tuple is in the format defined in _study2(). - """ + def get_last_open_bracket_pos(self): self._study2() - return self.stmt_bracketing + return self.lastopenbracketpos + # the structure of the bracketing of the last interesting statement, + # in the format defined in _study2, or None if the text didn't contain + # anything + stmt_bracketing = None -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_pyparse', verbosity=2) + def get_last_stmt_bracketing(self): + self._study2() + return self.stmt_bracketing diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/PyShell.py index 065122d..2ea7e6b 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/PyShell.py @@ -1,70 +1,69 @@ -#! /usr/bin/env python3 +#! /usr/bin/env python +from __future__ import print_function -import sys -if __name__ == "__main__": - sys.modules['idlelib.pyshell'] = sys.modules['__main__'] - -try: - from tkinter import * -except ImportError: - print("** IDLE can't import Tkinter.\n" - "Your Python may not be configured for Tk. **", file=sys.__stderr__) - raise SystemExit(1) - -# Valid arguments for the ...Awareness call below are defined in the following. -# https://msdn.microsoft.com/en-us/library/windows/desktop/dn280512(v=vs.85).aspx -if sys.platform == 'win32': - try: - import ctypes - PROCESS_SYSTEM_DPI_AWARE = 1 - ctypes.OleDLL('shcore').SetProcessDpiAwareness(PROCESS_SYSTEM_DPI_AWARE) - except (ImportError, AttributeError, OSError): - pass - -import tkinter.messagebox as tkMessageBox -if TkVersion < 8.5: - root = Tk() # otherwise create root in main - root.withdraw() - from idlelib.run import fix_scaling - fix_scaling(root) - tkMessageBox.showerror("Idle Cannot Start", - "Idle requires tcl/tk 8.5+, not %s." % TkVersion, - parent=root) - raise SystemExit(1) - -from code import InteractiveInterpreter -import linecache import os import os.path -from platform import python_version +import sys +import string +import getopt import re import socket -import subprocess -from textwrap import TextWrapper -import threading import time -import tokenize -import warnings +import threading +import io + +import linecache +from code import InteractiveInterpreter +from platform import python_version, system -from idlelib.colorizer import ColorDelegator -from idlelib.config import idleConf -from idlelib import debugger -from idlelib import debugger_r -from idlelib.editor import EditorWindow, fixwordbreaks -from idlelib.filelist import FileList -from idlelib.outwin import OutputWindow +try: + from Tkinter import * +except ImportError: + print("** IDLE can't import Tkinter.\n" + "Your Python may not be configured for Tk. **", file=sys.__stderr__) + sys.exit(1) +import tkMessageBox + +from idlelib.EditorWindow import EditorWindow, fixwordbreaks +from idlelib.FileList import FileList +from idlelib.ColorDelegator import ColorDelegator +from idlelib.UndoDelegator import UndoDelegator +from idlelib.OutputWindow import OutputWindow +from idlelib.configHandler import idleConf from idlelib import rpc -from idlelib.run import idle_formatwarning, StdInputFile, StdOutputFile -from idlelib.undo import UndoDelegator +from idlelib import Debugger +from idlelib import RemoteDebugger +from idlelib import macosxSupport +from idlelib import IOBinding +IDENTCHARS = string.ascii_letters + string.digits + "_" HOST = '127.0.0.1' # python execution server on localhost loopback PORT = 0 # someday pass in host, port for remote debug capability +try: + from signal import SIGTERM +except ImportError: + SIGTERM = 15 + # Override warnings module to write to warning_stream. Initialize to send IDLE # internal warnings to the console. ScriptBinding.check_syntax() will # temporarily redirect the stream to the shell window to display warnings when # checking user's code. warning_stream = sys.__stderr__ # None, at least on Windows, if no console. +import warnings + +def idle_formatwarning(message, category, filename, lineno, line=None): + """Format warnings the IDLE way.""" + + s = "\nWarning (from warnings module):\n" + s += ' File \"%s\", line %s\n' % (filename, lineno) + if line is None: + line = linecache.getline(filename, lineno) + line = line.strip() + if line: + s += " %s\n" % line + s += "%s: %s\n" % (category.__name__, message) + return s def idle_showwarning( message, category, filename, lineno, file=None, line=None): @@ -80,7 +79,7 @@ def idle_showwarning( file.write(idle_formatwarning( message, category, filename, lineno, line=line)) file.write(">>> ") - except (AttributeError, OSError): + except (AttributeError, IOError): pass # if file (probably __stderr__) is invalid, skip warning. _warnings_showwarning = None @@ -133,9 +132,8 @@ class PyShellEditorWindow(EditorWindow): self.text.bind("<<clear-breakpoint-here>>", self.clear_breakpoint_here) self.text.bind("<<open-python-shell>>", self.flist.open_shell) - #TODO: don't read/write this from/to .idlerc when testing - self.breakpointPath = os.path.join( - idleConf.userdir, 'breakpoints.lst') + self.breakpointPath = os.path.join(idleConf.GetUserCfgDir(), + 'breakpoints.lst') # whenever a file is changed, restore breakpoints def filename_changed_hook(old_hook=self.io.filename_change_hook, self=self): @@ -150,7 +148,6 @@ class PyShellEditorWindow(EditorWindow): ("Cut", "<<cut>>", "rmenu_check_cut"), ("Copy", "<<copy>>", "rmenu_check_copy"), ("Paste", "<<paste>>", "rmenu_check_paste"), - (None, None, None), ("Set Breakpoint", "<<set-breakpoint-here>>", None), ("Clear Breakpoint", "<<clear-breakpoint-here>>", None) ] @@ -245,12 +242,12 @@ class PyShellEditorWindow(EditorWindow): breaks = self.breakpoints filename = self.io.filename try: - with open(self.breakpointPath, "r") as fp: - lines = fp.readlines() - except OSError: + with open(self.breakpointPath,"r") as old_file: + lines = old_file.readlines() + except IOError: lines = [] try: - with open(self.breakpointPath, "w") as new_file: + with open(self.breakpointPath,"w") as new_file: for line in lines: if not line.startswith(filename + '='): new_file.write(line) @@ -258,7 +255,7 @@ class PyShellEditorWindow(EditorWindow): breaks = self.breakpoints if breaks: new_file.write(filename + '=' + str(breaks) + '\n') - except OSError as err: + except IOError as err: if not getattr(self.root, "breakpoint_error_displayed", False): self.root.breakpoint_error_displayed = True tkMessageBox.showerror(title='IDLE Error', @@ -275,8 +272,7 @@ class PyShellEditorWindow(EditorWindow): if filename is None: return if os.path.isfile(self.breakpointPath): - with open(self.breakpointPath, "r") as fp: - lines = fp.readlines() + lines = open(self.breakpointPath,"r").readlines() for line in lines: if line.startswith(filename + '='): breakpoint_linenumbers = eval(line[len(filename)+1:]) @@ -388,19 +384,6 @@ class MyRPCClient(rpc.RPCClient): "Override the base class - just re-raise EOFError" raise EOFError -def restart_line(width, filename): # See bpo-38141. - """Return width long restart line formatted with filename. - - Fill line with balanced '='s, with any extras and at least one at - the beginning. Do not end with a trailing space. - """ - tag = f"= RESTART: {filename or 'Shell'} =" - if width >= len(tag): - div, mod = divmod((width -len(tag)), 2) - return f"{(div+mod)*'='}{tag}{div*'='}" - else: - return tag[:-2] # Remove ' ='. - class ModifiedInterpreter(InteractiveInterpreter): @@ -408,6 +391,7 @@ class ModifiedInterpreter(InteractiveInterpreter): self.tkconsole = tkconsole locals = sys.modules['__main__'].__dict__ InteractiveInterpreter.__init__(self, locals=locals) + self.save_warnings_filters = None self.restarting = False self.subprocess_arglist = None self.port = PORT @@ -415,24 +399,35 @@ class ModifiedInterpreter(InteractiveInterpreter): _afterid = None rpcclt = None - rpcsubproc = None + rpcpid = None def spawn_subprocess(self): if self.subprocess_arglist is None: self.subprocess_arglist = self.build_subprocess_arglist() - self.rpcsubproc = subprocess.Popen(self.subprocess_arglist) + args = self.subprocess_arglist + self.rpcpid = os.spawnv(os.P_NOWAIT, sys.executable, args) def build_subprocess_arglist(self): assert (self.port!=0), ( "Socket should have been assigned a port number.") w = ['-W' + s for s in sys.warnoptions] + if 1/2 > 0: # account for new division + w.append('-Qnew') # Maybe IDLE is installed and is being accessed via sys.path, # or maybe it's not installed and the idle.py script is being # run from the IDLE source directory. del_exitf = idleConf.GetOption('main', 'General', 'delete-exitfunc', default=False, type='bool') - command = "__import__('idlelib.run').run.main(%r)" % (del_exitf,) - return [sys.executable] + w + ["-c", command, str(self.port)] + if __name__ == 'idlelib.PyShell': + command = "__import__('idlelib.run').run.main(%r)" % (del_exitf,) + else: + command = "__import__('run').main(%r)" % (del_exitf,) + if sys.platform[:3] == 'win' and ' ' in sys.executable: + # handle embedded space in path by quoting the argument + decorated_exec = '"%s"' % sys.executable + else: + decorated_exec = sys.executable + return [decorated_exec] + w + ["-c", command, str(self.port)] def start_subprocess(self): addr = (HOST, self.port) @@ -442,7 +437,7 @@ class ModifiedInterpreter(InteractiveInterpreter): try: self.rpcclt = MyRPCClient(addr) break - except OSError: + except socket.error: pass else: self.display_port_binding_error() @@ -486,12 +481,12 @@ class ModifiedInterpreter(InteractiveInterpreter): if debug: try: # Only close subprocess debugger, don't unregister gui_adap! - debugger_r.close_subprocess_debugger(self.rpcclt) + RemoteDebugger.close_subprocess_debugger(self.rpcclt) except: pass # Kill subprocess, spawn a new one, accept connection. self.rpcclt.close() - self.terminate_subprocess() + self.unix_terminate() console = self.tkconsole was_executing = console.executing console.executing = False @@ -505,8 +500,9 @@ class ModifiedInterpreter(InteractiveInterpreter): console.stop_readline() # annotate restart in shell window and mark it console.text.delete("iomark", "end-1c") - console.write('\n') - console.write(restart_line(console.width, filename)) + 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") if not filename: @@ -514,7 +510,7 @@ class ModifiedInterpreter(InteractiveInterpreter): # restart subprocess debugger if debug: # Restarted debugger connects to current instance of debug GUI - debugger_r.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 @@ -531,29 +527,26 @@ class ModifiedInterpreter(InteractiveInterpreter): if self._afterid is not None: self.tkconsole.text.after_cancel(self._afterid) try: - self.rpcclt.listening_sock.close() - except AttributeError: # no socket - pass - try: self.rpcclt.close() except AttributeError: # no socket pass - self.terminate_subprocess() + self.unix_terminate() self.tkconsole.executing = False self.rpcclt = None - def terminate_subprocess(self): - "Make sure subprocess is terminated" - try: - self.rpcsubproc.kill() - except OSError: - # process already terminated - return - else: + def unix_terminate(self): + "UNIX: make sure subprocess is terminated and collect status" + if hasattr(os, 'kill'): try: - self.rpcsubproc.wait() + os.kill(self.rpcpid, SIGTERM) except OSError: + # process already terminated: return + else: + try: + os.waitpid(self.rpcpid, 0) + except OSError: + return def transfer_path(self, with_cwd=False): if with_cwd: # Issue 13506 @@ -576,7 +569,7 @@ class ModifiedInterpreter(InteractiveInterpreter): return try: response = clt.pollresponse(self.active_seq, wait=0.05) - except (EOFError, OSError, KeyboardInterrupt): + except (EOFError, IOError, KeyboardInterrupt): # lost connection or subprocess terminated itself, restart # [the KBI is from rpc.SocketIO.handle_EOF()] if self.tkconsole.closing: @@ -595,7 +588,7 @@ class ModifiedInterpreter(InteractiveInterpreter): if self.tkconsole.getvar("<<toggle-jit-stack-viewer>>"): self.remote_stack_viewer() elif how == "ERROR": - errmsg = "pyshell.ModifiedInterpreter: Subprocess ERROR:\n" + errmsg = "PyShell.ModifiedInterpreter: Subprocess ERROR:\n" print(errmsg, what, file=sys.__stderr__) print(errmsg, what, file=console) # we received a response to the currently active seq number: @@ -630,13 +623,13 @@ class ModifiedInterpreter(InteractiveInterpreter): return def remote_stack_viewer(self): - from idlelib import debugobj_r + from idlelib import RemoteObjectBrowser oid = self.rpcclt.remotequeue("exec", "stackviewer", ("flist",), {}) if oid is None: self.tkconsole.root.bell() return - item = debugobj_r.StubObjectTreeItem(self.rpcclt, oid) - from idlelib.tree import ScrolledCanvas, TreeNode + item = RemoteObjectBrowser.StubObjectTreeItem(self.rpcclt, oid) + from idlelib.TreeWidget import ScrolledCanvas, TreeNode top = Toplevel(self.tkconsole.root) theme = idleConf.CurrentTheme() background = idleConf.GetHighlight(theme, 'normal')['background'] @@ -656,17 +649,13 @@ class ModifiedInterpreter(InteractiveInterpreter): def execfile(self, filename, source=None): "Execute an existing file" if source is None: - with tokenize.open(filename) as fp: - source = fp.read() - if use_subprocess: - source = (f"__file__ = r'''{os.path.abspath(filename)}'''\n" - + source + "\ndel __file__") + source = open(filename, "r").read() try: - code = compile(source, filename, "exec") + code = compile(source, filename, "exec", dont_inherit=True) except (OverflowError, SyntaxError): self.tkconsole.resetoutput() print('*** Error in script or command!\n' - 'Traceback (most recent call last):', + 'Traceback (most recent call last):', file=self.tkconsole.stderr) InteractiveInterpreter.showsyntaxerror(self, filename) self.tkconsole.showprompt() @@ -677,11 +666,25 @@ class ModifiedInterpreter(InteractiveInterpreter): "Extend base class method: Stuff the source in the line cache first" filename = self.stuffsource(source) self.more = 0 - # at the moment, InteractiveInterpreter expects str - assert isinstance(source, str) - # InteractiveInterpreter.runsource() calls its runcode() method, - # which is overridden (see below) - return InteractiveInterpreter.runsource(self, source, filename) + self.save_warnings_filters = warnings.filters[:] + warnings.filterwarnings(action="error", category=SyntaxWarning) + if isinstance(source, unicode) and IOBinding.encoding != 'utf-8': + try: + source = '# -*- coding: %s -*-\n%s' % ( + IOBinding.encoding, + source.encode(IOBinding.encoding)) + except UnicodeError: + self.tkconsole.resetoutput() + self.write("Unsupported characters in input\n") + return + try: + # InteractiveInterpreter.runsource() calls its runcode() method, + # which is overridden (see below) + return InteractiveInterpreter.runsource(self, source, filename) + finally: + if self.save_warnings_filters is not None: + warnings.filters[:] = self.save_warnings_filters + self.save_warnings_filters = None def stuffsource(self, source): "Stuff source in the filename cache" @@ -704,30 +707,47 @@ class ModifiedInterpreter(InteractiveInterpreter): \n""" % (filename,)) def showsyntaxerror(self, filename=None): - """Override Interactive Interpreter method: Use Colorizing + """Extend base class method: Add Colorizing Color the offending position instead of printing it and pointing at it with a caret. """ - tkconsole = self.tkconsole - text = tkconsole.text - text.tag_remove("ERROR", "1.0", "end") + text = self.tkconsole.text + stuff = self.unpackerror() + if stuff: + msg, lineno, offset, line = stuff + if lineno == 1: + pos = "iomark + %d chars" % (offset-1) + else: + pos = "iomark linestart + %d lines + %d chars" % \ + (lineno-1, offset-1) + text.tag_add("ERROR", pos) + text.see(pos) + char = text.get(pos) + if char and char in IDENTCHARS: + text.tag_add("ERROR", pos + " wordstart", pos) + self.tkconsole.resetoutput() + self.write("SyntaxError: %s\n" % str(msg)) + else: + self.tkconsole.resetoutput() + InteractiveInterpreter.showsyntaxerror(self, filename) + self.tkconsole.showprompt() + + def unpackerror(self): type, value, tb = sys.exc_info() - msg = getattr(value, 'msg', '') or value or "<no detail available>" - lineno = getattr(value, 'lineno', '') or 1 - offset = getattr(value, 'offset', '') or 0 - if offset == 0: - lineno += 1 #mark end of offending line - if lineno == 1: - pos = "iomark + %d chars" % (offset-1) + ok = type is SyntaxError + if ok: + try: + msg, (dummy_filename, lineno, offset, line) = value + if not offset: + offset = 0 + except: + ok = 0 + if ok: + return msg, lineno, offset, line else: - pos = "iomark linestart + %d lines + %d chars" % \ - (lineno-1, offset-1) - tkconsole.colorize_syntax_error(text, pos) - tkconsole.resetoutput() - self.write("SyntaxError: %s\n" % msg) - tkconsole.showprompt() + return None def showtraceback(self): "Extend base class method to reset output properly" @@ -739,7 +759,7 @@ class ModifiedInterpreter(InteractiveInterpreter): def checklinecache(self): c = linecache.cache - for key in list(c.keys()): + for key in c.keys(): if key[:1] + key[-1:] != "<>": del c[key] @@ -752,7 +772,7 @@ class ModifiedInterpreter(InteractiveInterpreter): if self.rpcclt: self.rpcclt.remotequeue("exec", "runcode", (code,), {}) else: - exec(code, self.locals) + exec code in self.locals return 1 def runcode(self, code): @@ -760,6 +780,9 @@ class ModifiedInterpreter(InteractiveInterpreter): if self.tkconsole.executing: self.interp.restart_subprocess() self.checklinecache() + if self.save_warnings_filters is not None: + warnings.filters[:] = self.save_warnings_filters + self.save_warnings_filters = None debugger = self.debugger try: self.tkconsole.beginexecuting() @@ -769,7 +792,7 @@ class ModifiedInterpreter(InteractiveInterpreter): elif debugger: debugger.run(code, self.locals) else: - exec(code, self.locals) + exec code in self.locals except SystemExit: if not self.tkconsole.closing: if tkMessageBox.askyesno( @@ -803,7 +826,7 @@ class ModifiedInterpreter(InteractiveInterpreter): def write(self, s): "Override base class method" - return self.tkconsole.stderr.write(s) + self.tkconsole.stderr.write(s) def display_port_binding_error(self): tkMessageBox.showerror( @@ -818,10 +841,10 @@ class ModifiedInterpreter(InteractiveInterpreter): def display_no_subprocess_error(self): tkMessageBox.showerror( - "Subprocess Connection Error", - "IDLE's subprocess didn't make connection.\n" - "See the 'Startup failure' section of the IDLE doc, online at\n" - "https://docs.python.org/3/library/idle.html#startup-failure", + "Subprocess Startup Error", + "IDLE's subprocess didn't make connection. Either IDLE can't " + "start a subprocess or personal firewall software is blocking " + "the connection.", parent=self.tkconsole.text) def display_executing_dialog(self): @@ -846,19 +869,13 @@ class PyShell(OutputWindow): ("edit", "_Edit"), ("debug", "_Debug"), ("options", "_Options"), - ("window", "_Window"), + ("windows", "_Window"), ("help", "_Help"), ] - # Extend right-click context menu - rmenu_specs = OutputWindow.rmenu_specs + [ - ("Squeeze", "<<squeeze-current-text>>"), - ] - - allow_line_numbers = False # New classes - from idlelib.history import History + from idlelib.IdleHistory import History def __init__(self, flist=None): if use_subprocess: @@ -871,17 +888,15 @@ class PyShell(OutputWindow): fixwordbreaks(root) root.withdraw() flist = PyShellFileList(root) - + # OutputWindow.__init__(self, flist, None, None) - + # +## self.config(usetabs=1, indentwidth=8, context_use_ps1=1) self.usetabs = True # indentwidth must be 8 when using tabs. See note in EditorWindow: self.indentwidth = 8 - - self.sys_ps1 = sys.ps1 if hasattr(sys, 'ps1') else '>>> ' - self.prompt_last_line = self.sys_ps1.split('\n')[-1] - self.prompt = self.sys_ps1 # Changes when debug active - + self.context_use_ps1 = True + # text = self.text text.configure(wrap="char") text.bind("<<newline-and-indent>>", self.enter_callback) @@ -894,34 +909,19 @@ class PyShell(OutputWindow): if use_subprocess: text.bind("<<view-restart>>", self.view_restart_mark) text.bind("<<restart-shell>>", self.restart_shell) - squeezer = self.Squeezer(self) - text.bind("<<squeeze-current-text>>", - squeezer.squeeze_current_text_event) - + # self.save_stdout = sys.stdout self.save_stderr = sys.stderr self.save_stdin = sys.stdin - from idlelib import iomenu - self.stdin = StdInputFile(self, "stdin", - iomenu.encoding, iomenu.errors) - self.stdout = StdOutputFile(self, "stdout", - iomenu.encoding, iomenu.errors) - self.stderr = StdOutputFile(self, "stderr", - iomenu.encoding, "backslashreplace") - self.console = StdOutputFile(self, "console", - iomenu.encoding, iomenu.errors) + from idlelib import IOBinding + self.stdin = PseudoInputFile(self, "stdin", IOBinding.encoding) + self.stdout = PseudoOutputFile(self, "stdout", IOBinding.encoding) + self.stderr = PseudoOutputFile(self, "stderr", IOBinding.encoding) + self.console = PseudoOutputFile(self, "console", IOBinding.encoding) if not use_subprocess: sys.stdout = self.stdout sys.stderr = self.stderr sys.stdin = self.stdin - try: - # page help() text to shell. - import pydoc # import must be done here to capture i/o rebinding. - # XXX KBK 27Dec07 use text viewer someday, but must work w/o subproc - pydoc.pager = pydoc.plainpager - except: - sys.stderr = sys.__stderr__ - raise # self.history = self.History(self.text) # @@ -971,22 +971,22 @@ class PyShell(OutputWindow): self.interp.setdebugger(None) db.close() if self.interp.rpcclt: - debugger_r.close_remote_debugger(self.interp.rpcclt) + RemoteDebugger.close_remote_debugger(self.interp.rpcclt) self.resetoutput() self.console.write("[DEBUG OFF]\n") - self.prompt = self.sys_ps1 + sys.ps1 = ">>> " self.showprompt() self.set_debugger_indicator() def open_debugger(self): if self.interp.rpcclt: - dbg_gui = debugger_r.start_remote_debugger(self.interp.rpcclt, + dbg_gui = RemoteDebugger.start_remote_debugger(self.interp.rpcclt, self) else: - dbg_gui = debugger.Debugger(self) + dbg_gui = Debugger.Debugger(self) self.interp.setdebugger(dbg_gui) dbg_gui.load_breakpoints() - self.prompt = "[DEBUG ON]\n" + self.sys_ps1 + sys.ps1 = "[DEBUG ON]\n>>> " self.showprompt() self.set_debugger_indicator() @@ -1043,7 +1043,6 @@ class PyShell(OutputWindow): 'Type "help", "copyright", "credits" or "license()" for more information.' def begin(self): - self.text.mark_set("iomark", "insert") self.resetoutput() if use_subprocess: nosub = '' @@ -1052,18 +1051,13 @@ class PyShell(OutputWindow): self.close() return False else: - 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 - + nosub = "==== No Subprocess ====" 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? + import Tkinter + Tkinter._default_root = None # 03Jan04 KBK What's this? return True def stop_readline(self): @@ -1085,6 +1079,12 @@ class PyShell(OutputWindow): line = self.text.get("iomark", "end-1c") if len(line) == 0: # may be EOF if we quit our mainloop with Ctrl-C line = "\n" + if isinstance(line, unicode): + from idlelib import IOBinding + try: + line = line.encode(IOBinding.encoding) + except UnicodeError: + pass self.resetoutput() if self.canceled: self.canceled = 0 @@ -1202,7 +1202,7 @@ class PyShell(OutputWindow): self.text.tag_add("stdin", "iomark", "end-1c") self.text.update_idletasks() if self.reading: - self.top.quit() # Break out of recursive mainloop() + self.top.quit() # Break out of recursive mainloop() in raw_input() else: self.runit() return "break" @@ -1258,7 +1258,7 @@ class PyShell(OutputWindow): "(sys.last_traceback is not defined)", parent=self.text) return - from idlelib.stackviewer import StackBrowser + from idlelib.StackViewer import StackBrowser StackBrowser(self.root, self.flist) def view_restart_mark(self, event=None): @@ -1271,19 +1271,15 @@ class PyShell(OutputWindow): def showprompt(self): self.resetoutput() - self.console.write(self.prompt) + try: + s = str(sys.ps1) + except: + s = "" + self.console.write(s) self.text.mark_set("insert", "end-1c") self.set_line_and_column() self.io.reset_undo() - def show_warning(self, msg): - width = self.interp.tkconsole.width - wrapper = TextWrapper(width=width, tabsize=8, expand_tabs=True) - wrapped_msg = '\n'.join(wrapper.wrap(msg)) - if not wrapped_msg.endswith('\n'): - wrapped_msg += '\n' - self.per.bottom.insert("iomark linestart", wrapped_msg, "stderr") - def resetoutput(self): source = self.text.get("iomark", "end-1c") if self.history: @@ -1292,20 +1288,19 @@ class PyShell(OutputWindow): self.text.insert("end-1c", "\n") self.text.mark_set("iomark", "end-1c") self.set_line_and_column() + sys.stdout.softspace = 0 def write(self, s, tags=()): try: self.text.mark_gravity("iomark", "right") - count = OutputWindow.write(self, s, tags, "iomark") + OutputWindow.write(self, s, tags, "iomark") self.text.mark_gravity("iomark", "left") except: - raise ###pass # ### 11Aug07 KBK if we are expecting exceptions - # let's find out what they are and be specific. + pass if self.canceled: self.canceled = 0 if not use_subprocess: raise KeyboardInterrupt - return count def rmenu_check_cut(self): try: @@ -1313,12 +1308,104 @@ class PyShell(OutputWindow): return 'disabled' except TclError: # no selection, so the index 'sel.first' doesn't exist return 'disabled' - return super().rmenu_check_cut() + return super(PyShell, self).rmenu_check_cut() def rmenu_check_paste(self): - if self.text.compare('insert','<','iomark'): + if self.text.compare('insert', '<', 'iomark'): return 'disabled' - return super().rmenu_check_paste() + return super(PyShell, self).rmenu_check_paste() + +class PseudoFile(io.TextIOBase): + + def __init__(self, shell, tags, encoding=None): + self.shell = shell + self.tags = tags + self.softspace = 0 + self._encoding = encoding + + @property + def encoding(self): + return self._encoding + + @property + def name(self): + return '<%s>' % self.tags + + def isatty(self): + return True + + +class PseudoOutputFile(PseudoFile): + + def writable(self): + return True + + def write(self, s): + if self.closed: + raise ValueError("write to closed file") + if type(s) not in (unicode, str, bytearray): + # See issue #19481 + if isinstance(s, unicode): + s = unicode.__getitem__(s, slice(None)) + elif isinstance(s, str): + s = str.__str__(s) + elif isinstance(s, bytearray): + s = bytearray.__str__(s) + else: + raise TypeError('must be string, not ' + type(s).__name__) + return self.shell.write(s, self.tags) + + +class PseudoInputFile(PseudoFile): + + def __init__(self, shell, tags, encoding=None): + PseudoFile.__init__(self, shell, tags, encoding) + self._line_buffer = '' + + def readable(self): + return True + + def read(self, size=-1): + if self.closed: + raise ValueError("read from closed file") + if size is None: + size = -1 + elif not isinstance(size, (int, long)): + raise TypeError('must be int, not ' + type(size).__name__) + result = self._line_buffer + self._line_buffer = '' + if size < 0: + while True: + line = self.shell.readline() + if not line: break + result += line + else: + while len(result) < size: + line = self.shell.readline() + if not line: break + result += line + self._line_buffer = result[size:] + result = result[:size] + return result + + def readline(self, size=-1): + if self.closed: + raise ValueError("read from closed file") + if size is None: + size = -1 + elif not isinstance(size, (int, long)): + raise TypeError('must be int, not ' + type(size).__name__) + line = self._line_buffer or self.shell.readline() + if size < 0: + size = len(line) + eol = line.find('\n', 0, size) + if eol >= 0: + size = eol + 1 + self._line_buffer = line[size:] + return line[:size] + + def close(self): + self.shell.close() def fix_x11_paste(root): @@ -1339,8 +1426,7 @@ USAGE: idle [-deins] [-t title] [file]* idle [-dns] [-t title] - [arg]* -h print this help message and exit - -n run IDLE without a subprocess (DEPRECATED, - see Help/IDLE Help for details) + -n run IDLE without a subprocess (see Help/IDLE Help for details) The following options will override the IDLE 'settings' configuration: @@ -1372,7 +1458,7 @@ idle -est "Baz" foo.py Run $IDLESTARTUP or $PYTHONSTARTUP, edit foo.py, and open a shell window with the title "Baz". -idle -c "import sys; print(sys.argv)" "foo" +idle -c "import sys; print sys.argv" "foo" Open a shell window and run the command, passing "-c" in sys.argv[0] and "foo" in sys.argv[1]. @@ -1381,17 +1467,12 @@ idle -d -s -r foo.py "Hello World" run foo.py, passing "foo.py" in sys.argv[0] and "Hello World" in sys.argv[1]. -echo "import sys; print(sys.argv)" | idle - "foobar" +echo "import sys; print sys.argv" | idle - "foobar" Open a shell window, run the script piped in, passing '' in sys.argv[0] and "foobar" in sys.argv[1]. """ def main(): - import getopt - from platform import system - from idlelib import testing # bool value - from idlelib import macosx - global flist, root, use_subprocess capture_warnings(True) @@ -1422,15 +1503,13 @@ 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 if os.path.isfile(script): pass else: - print("No script file: ", script) + print("No script file: ", script, file=sys.stderr) sys.exit() enable_shell = True if o == '-s': @@ -1458,11 +1537,11 @@ def main(): pathx.append(os.path.dirname(filename)) for dir in pathx: dir = os.path.abspath(dir) - if not dir in sys.path: + if dir not in sys.path: sys.path.insert(0, dir) else: dir = os.getcwd() - if dir not in sys.path: + if not dir in sys.path: sys.path.insert(0, dir) # check the IDLE settings configuration (but command line overrides) edit_start = idleConf.GetOption('main', 'General', @@ -1470,10 +1549,7 @@ def main(): enable_edit = enable_edit or edit_start enable_shell = enable_shell or not enable_edit - # Setup root. Don't break user code run in IDLE process. - # Don't change environment when testing. - if use_subprocess and not testing: - NoDefaultRoot() + # start editor and/or shell windows: root = Tk(className="Idle") root.withdraw() from idlelib.run import fix_scaling @@ -1484,19 +1560,25 @@ def main(): if system() == 'Windows': iconfile = os.path.join(icondir, 'idle.ico') root.wm_iconbitmap(default=iconfile) - elif not macosx.isAquaTk(): + elif TkVersion >= 8.5 and sys.platform != 'darwin': ext = '.png' if TkVersion >= 8.6 else '.gif' iconfiles = [os.path.join(icondir, 'idle_%d%s' % (size, ext)) for size in (16, 32, 48)] - icons = [PhotoImage(master=root, file=iconfile) - for iconfile in iconfiles] - root.wm_iconphoto(True, *icons) + icons = [PhotoImage(file=iconfile) for iconfile in iconfiles] + root.tk.call('wm', 'iconphoto', str(root), "-default", *icons) - # start editor and/or shell windows: fixwordbreaks(root) fix_x11_paste(root) flist = PyShellFileList(root) - macosx.setupApp(root, flist) + 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): @@ -1511,7 +1593,7 @@ def main(): shell = flist.open_shell() if not shell: return # couldn't open shell - if macosx.isAquaTk() 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 @@ -1542,20 +1624,12 @@ def main(): shell.interp.execfile(script) elif shell: # If there is a shell window and no cmd or script in progress, - # check for problematic issues and print warning message(s) in - # the IDLE shell window; this is less intrusive than always - # opening a separate window. - - # Warn if using a problematic OS X Tk version. - tkversionwarning = macosx.tkVersionWarning(root) + # check for problematic OS X Tk versions and print a warning + # message in the IDLE shell window; this is less intrusive + # than always opening a separate window. + tkversionwarning = macosxSupport.tkVersionWarning(root) if tkversionwarning: - shell.show_warning(tkversionwarning) - - # Warn if the "Prefer tabs when opening documents" system - # preference is set to "Always". - prefer_tabs_preference_warning = macosx.preferTabsPreferenceWarning() - if prefer_tabs_preference_warning: - shell.show_warning(prefer_tabs_preference_warning) + shell.interp.runcommand("print('%s')" % tkversionwarning) while flist.inversedict: # keep IDLE running while files are open. root.mainloop() @@ -1563,6 +1637,7 @@ def main(): capture_warnings(False) if __name__ == "__main__": + sys.modules['PyShell'] = sys.modules['__main__'] main() capture_warnings(False) # Make sure turned off; see issue 18081 diff --git a/Lib/idlelib/README.txt b/Lib/idlelib/README.txt index 48a1f4a..bc169c8 100644 --- a/Lib/idlelib/README.txt +++ b/Lib/idlelib/README.txt @@ -16,7 +16,7 @@ and omissions and lag behind changes in idlelib. IDLELIB FILES -Implementation files not in IDLE MENU are marked (nim). +Implemetation files not in IDLE MENU are marked (nim). Deprecated files and objects are listed separately as the end. Startup @@ -29,61 +29,61 @@ idle.pyw Implementation -------------- -autocomplete.py # Complete attribute names or filenames. -autocomplete_w.py # Display completions. -autoexpand.py # Expand word with previous word in file. -browser.py # Create module browser window. -calltip_w.py # Display calltip. -calltips.py # Create calltip text. -codecontext.py # Show compound statement headers otherwise not visible. -colorizer.py # Colorize text (nim) -config.py # Load, fetch, and save configuration (nim). -configdialog.py # Display user configuration dialogs. -config_help.py # Specify help source in configdialog. -config_key.py # Change keybindings. -dynoption.py # Define mutable OptionMenu widget (nim). -debugobj.py # Define class used in stackviewer. -debugobj_r.py # Communicate objects between processes with rpc (nim). -debugger.py # Debug code run from shell or editor; show window. -debugger_r.py # Debug code run in remote process. -delegator.py # Define base class for delegators (nim). -editor.py # Define most of editor and utility functions. -filelist.py # Open files and manage list of open windows (nim). -grep.py # Find all occurrences of pattern in multiple files. +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. -help_about.py # Display About IDLE dialog. -history.py # Get previous or next user input in shell (nim) -hyperparser.py # Parse code around a given index. -iomenu.py # Open, read, and write files -macosx.py # Help IDLE run on Macs (nim). -mainmenu.py # Define most of IDLE menu. -multicall.py # Wrap tk widget to allow multiple calls per event (nim). -outwin.py # Create window for grep output. -paragraph.py # Re-wrap multiline strings and comments. -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 -query.py # Query user for information -redirector.py # Intercept widget subcommands (for percolator) (nim). -replace.py # Search and replace pattern in text. -rpc.py # Communicate between idle and user processes (nim). -rstrip.py # Strip trailing whitespace. +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. -runscript.py # Check and run user code. -scrolledlist.py # Define scrolledlist widget for IDLE (nim). -search.py # Search for pattern in text. -searchbase.py # Define base for search, replace, and grep dialogs. -searchengine.py # Define engine for all 3 search dialogs. -stackviewer.py # View stack after exception. -statusbar.py # Define status bar for windows (nim). tabbedpages.py # Define tabbed pages widget (nim). -textview.py # Define read-only text widget (nim). -tree.py # Define tree widget, used in browsers (nim). -undo.py # Manage undo stack. -windows.py # Manage window list and define listed top level. -zoomheight.py # Zoom window to full height of screen. +textView.py # Define read-only text widget (nim). Configuration ------------- @@ -97,155 +97,134 @@ 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, 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 +Icons # small image files +idle_test # files for human test and automated unit tests Unused and Deprecated files and objects (nim) --------------------------------------------- -tooltip.py # unused - +EditorWindow.py: Helpdialog and helpDialog +ToolTip.py: unused. +help.txt +idlever.py IDLE MENUS -Top level items and most submenu items are defined in mainmenu. -Extensions add submenu items when active. The names given are +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. -'eEW' = editor.EditorWindow -File - New File # eEW.new_callback - Open... # iomenu.open - Open Module # eEw.open_module +File # IOBindig except as noted + New File + Open... # IOBinding.open + Open Module Recent Files - Class Browser # eEW.open_class_browser, browser.ClassBrowser - Path Browser # eEW.open_path_browser, pathbrowser + Class Browser # Class Browser + Path Browser # Path Browser --- - Save # iomenu.save - Save As... # iomenu.save_as - Save Copy As... # iomenu.save_a_copy + Save # IDBinding.save + Save As... # IOBinding.save_as + Save Copy As... # IOBindling.save_a_copy --- - Print Window # iomenu.print_window + Print Window # IOBinding.print_window --- - Close # eEW.close_event - Exit # flist.close_all_callback (bound in eEW) + Close + Exit Edit - Undo # undodelegator - Redo # undodelegator - --- # eEW.right_menu_event - Cut # eEW.cut - Copy # eEW.copy - Paste # eEW.past - Select All # eEW.select_all (+ see eEW.remove_selection) - --- # Next 5 items use searchengine; dialogs use searchbase - Find # eEW.find_event, search.SearchDialog.find - Find Again # eEW.find_again_event, sSD.find_again - Find Selection # eEW.find_selection_event, sSD.find_selection - Find in Files... # eEW.find_in_files_event, grep - Replace... # eEW.replace_event, replace.ReplaceDialog.replace - Go to Line # eEW.goto_line_event - 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.PyShell.view_restart_mark - Restart Shell # pyshell.PyShell.restart_shell + 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.PyShell.view_restart_mark + Restart Shell # PyShell.PyShell.restart_shell Interrupt Execution # pyshell.PyShell.cancel_callback Debug (Shell only) Go to File/Line - debugger # debugger, debugger_r, PyShell.toggle_debugger - Stack Viewer # stackviewer, PyShell.open_stack_viewer - Auto-open Stack Viewer # stackviewer + Debugger # Debugger, RemoteDebugger, PyShell.toggle_debuger + Stack Viewer # StackViewer, PyShell.open_stack_viewer + Auto-open Stack Viewer # StackViewer Format (Editor only) - Indent Region # eEW.indent_region_event - Dedent Region # eEW.dedent_region_event - Comment Out Reg. # eEW.comment_region_event - Uncomment Region # eEW.uncomment_region_event - Tabify Region # eEW.tabify_region_event - Untabify Region # eEW.untabify_region_event - Toggle Tabs # eEW.toggle_tabs_event - New Indent Width # eEW.change_indentwidth_event - Format Paragraph # paragraph extension + 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 # rstrip extension + Strip tailing whitespace # RstripExtension extension Run (Editor only) - Python Shell # pyshell + Python Shell # PyShell --- - Check Module # runscript - Run Module # runscript + Check Module # ScriptBinding + Run Module # ScriptBinding Options - Configure IDLE # eEW.config_dialog, configdialog + Configure IDLE # configDialog (tabs in the dialog) - Font tab # config-main.def - Highlight tab # query, config-highlight.def - Keys tab # query, config_key, config_keys.def - General tab # config_help, config-main.def - Extensions tab # config-extensions.def, corresponding .py + 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 (ed)# codecontext extension + Code Context (editor only) # CodeContext extension Window - Zoomheight # zoomheight extension + Zoomheight # ZoomHeight extension --- - <open windows> # windows + <open windows> # WindowList Help - About IDLE # eEW.about_dialog, help_about.AboutDialog + About IDLE # aboutDialog --- - IDLE Help # eEW.help_dialog, helpshow_idlehelp - Python Doc # eEW.python_docs - Turtle Demo # eEW.open_turtle_demo + IDLE Help # help + Python Doc + Turtle Demo --- <other help sources> <Context Menu> (right click) - Defined in editor, PyShelpyshellut - 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 - -<No menu> -Center Insert # eEW.center_insert_event - - -CODE STYLE -- Generally PEP 8. - -import ------- -Put import at the top, unless there is a good reason otherwise. -PEP 8 says to group stdlib, 3rd-party dependencies, and package imports. -For idlelib, the groups are general stdlib, tkinter, and idlelib. -Sort modules within each group, except that tkinter.ttk follows tkinter. -Sort 'from idlelib import mod1' and 'from idlelib.mod2 import object' -together by module, ignoring within module objects. -Put 'import __main__' after other idlelib imports. - -Imports only needed for testing are put not at the top but in an -htest function def or "if __name__ == '__main__'" clause. - -Within module imports like "from idlelib.mod import class" may cause -circular imports to deadlock. Even without this, circular imports may -require at least one of the imports to be delayed until a function call. +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/debugger_r.py b/Lib/idlelib/RemoteDebugger.py index 9dcfc56..8c71a21 100644 --- a/Lib/idlelib/debugger_r.py +++ b/Lib/idlelib/RemoteDebugger.py @@ -21,7 +21,7 @@ barrier, in particular frame and traceback objects. """ import types -from idlelib import debugger +from idlelib import Debugger debugging = 0 @@ -92,13 +92,16 @@ class IdbAdapter: self.idb.set_return(frame) def get_stack(self, fid, tbid): + ##print >>sys.__stderr__, "get_stack(%r, %r)" % (fid, tbid) frame = frametable[fid] if tbid is None: tb = None else: tb = tracebacktable[tbid] stack, i = self.idb.get_stack(frame, tb) + ##print >>sys.__stderr__, "get_stack() ->", stack stack = [(wrap_frame(frame2), k) for frame2, k in stack] + ##print >>sys.__stderr__, "get_stack() ->", stack return stack, i def run(self, cmd): @@ -157,20 +160,13 @@ class IdbAdapter: #----------called by a DictProxy---------- def dict_keys(self, did): - raise NotImplementedError("dict_keys not public or pickleable") -## dict = dicttable[did] -## return dict.keys() - - ### Needed until dict_keys is type is finished and pickealable. - ### Will probably need to extend rpc.py:SocketIO._proxify at that time. - def dict_keys_list(self, did): dict = dicttable[did] - return list(dict.keys()) + return dict.keys() def dict_item(self, did, key): dict = dicttable[did] value = dict[key] - value = repr(value) ### can't pickle module 'builtins' + value = repr(value) return value #----------end class IdbAdapter---------- @@ -187,7 +183,7 @@ def start_debugger(rpchandler, gui_adap_oid): """ gui_proxy = GUIProxy(rpchandler, gui_adap_oid) - idb = debugger.Idb(gui_proxy) + idb = Debugger.Idb(gui_proxy) idb_adap = IdbAdapter(idb) rpchandler.register(idb_adap_oid, idb_adap) return idb_adap_oid @@ -208,7 +204,7 @@ class FrameProxy: def __getattr__(self, name): if name[:1] == "_": - raise AttributeError(name) + raise AttributeError, name if name == "f_code": return self._get_f_code() if name == "f_globals": @@ -263,21 +259,16 @@ class DictProxy: self._oid = oid self._did = did -## def keys(self): -## return self._conn.remotecall(self._oid, "dict_keys", (self._did,), {}) - - # 'temporary' until dict_keys is a pickleable built-in type def keys(self): - return self._conn.remotecall(self._oid, - "dict_keys_list", (self._did,), {}) + return self._conn.remotecall(self._oid, "dict_keys", (self._did,), {}) def __getitem__(self, key): return self._conn.remotecall(self._oid, "dict_item", (self._did, key), {}) def __getattr__(self, name): - ##print("*** Failed DictProxy.__getattr__:", name) - raise AttributeError(name) + ##print >>sys.__stderr__, "failed DictProxy.__getattr__:", name + raise AttributeError, name class GUIAdapter: @@ -287,7 +278,7 @@ class GUIAdapter: self.gui = gui def interaction(self, message, fid, modified_info): - ##print("*** Interaction: (%s, %s, %s)" % (message, fid, modified_info)) + ##print "interaction: (%s, %s, %s)" % (message, fid, modified_info) frame = FrameProxy(self.conn, fid) self.gui.interaction(message, frame, modified_info) @@ -299,10 +290,10 @@ class IdbProxy: self.conn = conn self.shell = shell - def call(self, methodname, /, *args, **kwargs): - ##print("*** IdbProxy.call %s %s %s" % (methodname, args, kwargs)) + def call(self, methodname, *args, **kwargs): + ##print "**IdbProxy.call %s %s %s" % (methodname, args, kwargs) value = self.conn.remotecall(self.oid, methodname, args, kwargs) - ##print("*** IdbProxy.call %s returns %r" % (methodname, value)) + ##print "**IdbProxy.call %s returns %r" % (methodname, value) return value def run(self, cmd, locals): @@ -362,7 +353,7 @@ def start_remote_debugger(rpcclt, pyshell): idb_adap_oid = rpcclt.remotecall("exec", "start_the_debugger",\ (gui_adap_oid,), {}) idb_proxy = IdbProxy(rpcclt, pyshell, idb_adap_oid) - gui = debugger.Debugger(pyshell, idb_proxy) + gui = Debugger.Debugger(pyshell, idb_proxy) gui_adap = GUIAdapter(rpcclt, gui) rpcclt.register(gui_adap_oid, gui_adap) return gui @@ -373,7 +364,7 @@ def close_remote_debugger(rpcclt): Request that the RPCServer shut down the subprocess debugger and link. Unregister the GUIAdapter, which will cause a GC on the Idle process debugger and RPC link objects. (The second reference to the debugger GUI - is deleted in pyshell.close_remote_debugger().) + is deleted in PyShell.close_remote_debugger().) """ close_subprocess_debugger(rpcclt) @@ -386,8 +377,3 @@ def restart_subprocess_debugger(rpcclt): idb_adap_oid_ret = rpcclt.remotecall("exec", "start_the_debugger",\ (gui_adap_oid,), {}) assert idb_adap_oid_ret == idb_adap_oid, 'Idb restarted with different oid' - - -if __name__ == "__main__": - from unittest import main - main('idlelib.idle_test.test_debugger', verbosity=2, exit=False) diff --git a/Lib/idlelib/debugobj_r.py b/Lib/idlelib/RemoteObjectBrowser.py index 75e75eb..43e2c68 100644 --- a/Lib/idlelib/debugobj_r.py +++ b/Lib/idlelib/RemoteObjectBrowser.py @@ -17,8 +17,8 @@ class WrappedObjectTreeItem: return value def _GetSubList(self): - sub_list = self.__item._GetSubList() - return list(map(remote_object_tree_item, sub_list)) + list = self.__item._GetSubList() + return map(remote_object_tree_item, list) class StubObjectTreeItem: # Lives in IDLE process @@ -32,10 +32,5 @@ class StubObjectTreeItem: return value def _GetSubList(self): - sub_list = self.sockio.remotecall(self.oid, "_GetSubList", (), {}) - return [StubObjectTreeItem(self.sockio, oid) for oid in sub_list] - - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_debugobj_r', verbosity=2) + list = self.sockio.remotecall(self.oid, "_GetSubList", (), {}) + return [StubObjectTreeItem(self.sockio, oid) for oid in list] diff --git a/Lib/idlelib/replace.py b/Lib/idlelib/ReplaceDialog.py index 6be034a..66a871a 100644 --- a/Lib/idlelib/replace.py +++ b/Lib/idlelib/ReplaceDialog.py @@ -1,27 +1,13 @@ -"""Replace dialog for IDLE. Inherits SearchDialogBase for GUI. -Uses idlelib.searchengine.SearchEngine for search capability. -Defines various replace related functions like replace, replace all, -and replace+find. -""" -import re - -from tkinter import StringVar, TclError +from Tkinter import * -from idlelib.searchbase import SearchDialogBase -from idlelib import searchengine +from idlelib import SearchEngine +from idlelib.SearchDialogBase import SearchDialogBase +import re def replace(text): - """Create or reuse a singleton ReplaceDialog instance. - - The singleton dialog saves user entries and preferences - across instances. - - Args: - text: Text widget containing the text to be searched. - """ root = text._root() - engine = searchengine.get(root) + engine = SearchEngine.get(root) if not hasattr(engine, "_replacedialog"): engine._replacedialog = ReplaceDialog(root, engine) dialog = engine._replacedialog @@ -29,36 +15,15 @@ def replace(text): class ReplaceDialog(SearchDialogBase): - "Dialog for finding and replacing a pattern in text." title = "Replace Dialog" icon = "Replace" def __init__(self, root, engine): - """Create search dialog for finding and replacing text. - - Uses SearchDialogBase as the basis for the GUI and a - searchengine instance to prepare the search. - - Attributes: - replvar: StringVar containing 'Replace with:' value. - replent: Entry widget for replvar. Created in - create_entries(). - ok: Boolean used in searchengine.search_text to indicate - whether the search includes the selection. - """ - super().__init__(root, engine) + SearchDialogBase.__init__(self, root, engine) self.replvar = StringVar(root) def open(self, text): - """Make dialog visible on top of others and ready to use. - - Also, highlight the currently selected text and set the - search to include the current selection (self.ok). - - Args: - text: Text widget being searched. - """ SearchDialogBase.open(self, text) try: first = text.index("sel.first") @@ -71,50 +36,35 @@ class ReplaceDialog(SearchDialogBase): first = first or text.index("insert") last = last or first self.show_hit(first, last) - self.ok = True + self.ok = 1 def create_entries(self): - "Create base and additional label and text entry widgets." SearchDialogBase.create_entries(self) self.replent = self.make_entry("Replace with:", self.replvar)[0] def create_command_buttons(self): - """Create base and additional command buttons. - - The additional buttons are for Find, Replace, - Replace+Find, and Replace All. - """ SearchDialogBase.create_command_buttons(self) self.make_button("Find", self.find_it) self.make_button("Replace", self.replace_it) - self.make_button("Replace+Find", self.default_command, isdef=True) + self.make_button("Replace+Find", self.default_command, 1) self.make_button("Replace All", self.replace_all) def find_it(self, event=None): - "Handle the Find button." - self.do_find(False) + self.do_find(0) def replace_it(self, event=None): - """Handle the Replace button. - - If the find is successful, then perform replace. - """ if self.do_find(self.ok): self.do_replace() def default_command(self, event=None): - """Handle the Replace+Find button as the default command. - - First performs a replace and then, if the replace was - successful, a find next. - """ if self.do_find(self.ok): - if self.do_replace(): # Only find next match if replace succeeded. - # A bad re can cause it to fail. - self.do_find(False) + if self.do_replace(): # Only find next match if replace succeeded. + # A bad re can cause it to fail. + self.do_find(0) def _replace_expand(self, m, repl): - "Expand replacement text if regular expression." + """ Helper function for expanding a regular expression + in the replace field, if needed. """ if self.engine.isre(): try: new = m.expand(repl) @@ -123,19 +73,9 @@ class ReplaceDialog(SearchDialogBase): new = None else: new = repl - return new def replace_all(self, event=None): - """Handle the Replace All button. - - Search text for occurrences of the Find value and replace - each of them. The 'wrap around' value controls the start - point for searching. If wrap isn't set, then the searching - starts at the first occurrence after the current selection; - if wrap is set, the replacement starts at the first line. - The replacement is always done top-to-bottom in the text. - """ prog = self.engine.getprog() if not prog: return @@ -143,7 +83,7 @@ class ReplaceDialog(SearchDialogBase): text = self.text res = self.engine.search_text(text, prog) if not res: - self.bell() + text.bell() return text.tag_remove("sel", "1.0", "end") text.tag_remove("hit", "1.0", "end") @@ -152,13 +92,12 @@ class ReplaceDialog(SearchDialogBase): if self.engine.iswrap(): line = 1 col = 0 - ok = True + ok = 1 first = last = None # XXX ought to replace circular instead of top-to-bottom when wrapping text.undo_block_start() - while True: - res = self.engine.search_forward(text, prog, line, col, - wrap=False, ok=ok) + while 1: + res = self.engine.search_forward(text, prog, line, col, 0, ok) if not res: break line, m = res @@ -179,34 +118,29 @@ class ReplaceDialog(SearchDialogBase): if new: text.insert(first, new) col = i + len(new) - ok = False + ok = 0 text.undo_block_stop() if first and last: self.show_hit(first, last) self.close() - def do_find(self, ok=False): - """Search for and highlight next occurrence of pattern in text. - - No text replacement is done with this option. - """ + def do_find(self, ok=0): if not self.engine.getprog(): return False text = self.text res = self.engine.search_text(text, None, ok) if not res: - self.bell() + text.bell() return False line, m = res i, j = m.span() first = "%d.%d" % (line, i) last = "%d.%d" % (line, j) self.show_hit(first, last) - self.ok = True + self.ok = 1 return True def do_replace(self): - "Replace search pattern in text with replacement value." prog = self.engine.getprog() if not prog: return False @@ -218,7 +152,7 @@ class ReplaceDialog(SearchDialogBase): pos = None if not pos: first = last = pos = text.index("insert") - line, col = searchengine.get_line_col(pos) + line, col = SearchEngine.get_line_col(pos) chars = text.get("%d.0" % line, "%d.0" % (line+1)) m = prog.match(chars, col) if not prog: @@ -234,20 +168,10 @@ class ReplaceDialog(SearchDialogBase): text.insert(first, new) text.undo_block_stop() self.show_hit(first, text.index("insert")) - self.ok = False + self.ok = 0 return True def show_hit(self, first, last): - """Highlight text between first and last indices. - - Text is highlighted via the 'hit' tag and the marked - section is brought into view. - - The colors from the 'hit' tag aren't currently shown - when the text is displayed. This is due to the 'sel' - tag being added first, so the colors in the 'sel' - config are seen instead of the colors for 'hit'. - """ text = self.text text.mark_set("insert", first) text.tag_remove("sel", "1.0", "end") @@ -261,19 +185,14 @@ class ReplaceDialog(SearchDialogBase): text.update_idletasks() def close(self, event=None): - "Close the dialog and remove hit tags." SearchDialogBase.close(self, event) self.text.tag_remove("hit", "1.0", "end") - -def _replace_dialog(parent): # htest # - from tkinter import Toplevel, Text, END, SEL - from tkinter.ttk import Frame, Button - - top = Toplevel(parent) - top.title("Test ReplaceDialog") - x, y = map(int, parent.geometry().split('+')[1:]) - top.geometry("+%d+%d" % (x, y + 175)) +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(): @@ -282,26 +201,20 @@ def _replace_dialog(parent): # htest # def undo_block_stop(): pass - frame = Frame(top) - frame.pack() - text = Text(frame, inactiveselectbackground='gray') + 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\nPlus MORE.") - text.focus_set() + 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(frame, text="Replace", command=show_replace) + button = Button(root, text="Replace", command=show_replace) button.pack() if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_replace', verbosity=2, exit=False) - from idlelib.idle_test.htest import run run(_replace_dialog) diff --git a/Lib/idlelib/RstripExtension.py b/Lib/idlelib/RstripExtension.py new file mode 100644 index 0000000..2ce3c7e --- /dev/null +++ b/Lib/idlelib/RstripExtension.py @@ -0,0 +1,33 @@ +'Provides "Strip trailing whitespace" under the "Format" menu.' + +class RstripExtension: + + menudefs = [ + ('format', [None, ('Strip trailing whitespace', '<<do-rstrip>>'), ] ), ] + + def __init__(self, editwin): + self.editwin = editwin + self.editwin.text.bind("<<do-rstrip>>", self.do_rstrip) + + def do_rstrip(self, event=None): + + text = self.editwin.text + undo = self.editwin.undo + + undo.undo_block_start() + + end_line = int(float(text.index('end'))) + for cur in range(1, end_line): + txt = text.get('%i.0' % cur, '%i.end' % cur) + raw = len(txt) + cut = len(txt.rstrip()) + # Since text.delete() marks file as changed, even if not, + # only call it when needed to actually delete something. + if cut < raw: + text.delete('%i.%i' % (cur, cut), '%i.end' % cur) + + undo.undo_block_stop() + +if __name__ == "__main__": + import unittest + unittest.main('idlelib.idle_test.test_rstrip', verbosity=2, exit=False) diff --git a/Lib/idlelib/ScriptBinding.py b/Lib/idlelib/ScriptBinding.py new file mode 100644 index 0000000..0309a8a --- /dev/null +++ b/Lib/idlelib/ScriptBinding.py @@ -0,0 +1,222 @@ +"""Extension to execute code outside the Python shell window. + +This adds the following commands: + +- Check module does a full syntax check of the current module. + It also runs the tabnanny to catch any inconsistent tabs. + +- Run module executes the module's code in the __main__ namespace. The window + must have been saved previously. The module is added to sys.modules, and is + also added to the __main__ namespace. + +XXX GvR Redesign this interface (yet again) as follows: + +- Present a dialog box for ``Run Module'' + +- Allow specify command line arguments in the dialog box + +""" + +import os +import re +import string +import tabnanny +import tokenize +import tkMessageBox +from idlelib import PyShell + +from idlelib.configHandler import idleConf +from idlelib import macosxSupport + +IDENTCHARS = string.ascii_letters + string.digits + "_" + +indent_message = """Error: Inconsistent indentation detected! + +1) Your indentation is outright incorrect (easy to fix), OR + +2) Your indentation mixes tabs and spaces. + +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 = [ + ('run', [None, + ('Check Module', '<<check-module>>'), + ('Run Module', '<<run-module>>'), ]), ] + + def __init__(self, editwin): + self.editwin = editwin + # Provide instance variables referenced by Debugger + # XXX This should be done differently + self.flist = self.editwin.flist + self.root = self.editwin.root + + if macosxSupport.isCocoaTk(): + self.editwin.text_frame.bind('<<run-module-event-2>>', self._run_module_event) + + def check_module_event(self, event): + filename = self.getfilename() + if not filename: + return 'break' + if not self.checksyntax(filename): + return 'break' + if not self.tabnanny(filename): + return 'break' + + def tabnanny(self, filename): + f = open(filename, 'r') + try: + tabnanny.process_tokens(tokenize.generate_tokens(f.readline)) + except tokenize.TokenError as msg: + msgtxt, (lineno, start) = msg.args + self.editwin.gotoline(lineno) + self.errorbox("Tabnanny Tokenizing Error", + "Token Error: %s" % msgtxt) + return False + except tabnanny.NannyNag as nag: + # The error messages from tabnanny are too confusing... + self.editwin.gotoline(nag.get_lineno()) + self.errorbox("Tab/space error", indent_message) + return False + return True + + def checksyntax(self, filename): + self.shell = shell = self.flist.open_shell() + saved_stream = shell.get_warning_stream() + shell.set_warning_stream(shell.stderr) + with open(filename, 'r') as f: + source = f.read() + if '\r' in source: + source = re.sub(r"\r\n", "\n", source) + source = re.sub(r"\r", "\n", source) + if source and source[-1] != '\n': + source = source + '\n' + text = self.editwin.text + text.tag_remove("ERROR", "1.0", "end") + try: + try: + # If successful, return the compiled code + return compile(source, filename, "exec") + except (SyntaxError, OverflowError, ValueError) as err: + try: + msg, (errorfilename, lineno, offset, line) = err + if not errorfilename: + err.args = msg, (filename, lineno, offset, line) + err.filename = filename + self.colorize_syntax_error(msg, lineno, offset) + except: + msg = "*** " + str(err) + self.errorbox("Syntax error", + "There's an error in your program:\n" + msg) + return False + finally: + shell.set_warning_stream(saved_stream) + + def colorize_syntax_error(self, msg, lineno, offset): + text = self.editwin.text + pos = "0.0 + %d lines + %d chars" % (lineno-1, offset-1) + text.tag_add("ERROR", pos) + char = text.get(pos) + if char and char in IDENTCHARS: + text.tag_add("ERROR", pos + " wordstart", pos) + if '\n' == text.get(pos): # error at line end + text.mark_set("insert", pos) + else: + text.mark_set("insert", pos + "+1c") + text.see(pos) + + def run_module_event(self, event): + """Run the module after setting up the environment. + + First check the syntax. If OK, make sure the shell is active and + then transfer the arguments, set the run environment's working + directory to the directory of the module being executed and also + add that directory to its sys.path if not already included. + + """ + filename = self.getfilename() + if not filename: + return 'break' + code = self.checksyntax(filename) + if not code: + return 'break' + if not self.tabnanny(filename): + return 'break' + interp = self.shell.interp + if PyShell.use_subprocess: + interp.restart_subprocess(with_cwd=False, filename=code.co_filename) + dirname = os.path.dirname(filename) + # XXX Too often this discards arguments the user just set... + interp.runcommand("""if 1: + __file__ = {filename!r} + import sys as _sys + from os.path import basename as _basename + if (not _sys.argv or + _basename(_sys.argv[0]) != _basename(__file__)): + _sys.argv = [__file__] + import os as _os + _os.chdir({dirname!r}) + del _sys, _basename, _os + \n""".format(filename=filename, dirname=dirname)) + interp.prepend_syspath(filename) + # XXX KBK 03Jul04 When run w/o subprocess, runtime warnings still + # go to __stderr__. With subprocess, they go to the shell. + # Need to change streams in PyShell.ModifiedInterpreter. + interp.runcode(code) + return 'break' + + 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 + # tries to run a module using the keyboard shortcut + # (the menu item works fine). + _run_module_event = run_module_event + + def run_module_event(self, event): + self.editwin.text_frame.after(200, + lambda: self.editwin.text_frame.event_generate('<<run-module-event-2>>')) + return 'break' + + def getfilename(self): + """Get source filename. If not saved, offer to save (or create) file + + The debugger requires a source file. Make sure there is one, and that + the current version of the source buffer has been saved. If the user + declines to save or cancels the Save As dialog, return None. + + If the user has configured IDLE for Autosave, the file will be + silently saved if it already exists and is dirty. + + """ + filename = self.editwin.io.filename + if not self.editwin.get_saved(): + autosave = idleConf.GetOption('main', 'General', + 'autosave', type='bool') + if autosave and filename: + self.editwin.io.save(None) + else: + confirm = self.ask_save_dialog() + self.editwin.text.focus_set() + if confirm: + self.editwin.io.save(None) + filename = self.editwin.io.filename + else: + filename = None + return filename + + def ask_save_dialog(self): + msg = "Source Must Be Saved\n" + 5*' ' + "OK to Save?" + confirm = tkMessageBox.askokcancel(title="Save Before Run or Check", + message=msg, + default=tkMessageBox.OK, + parent=self.editwin.text) + return confirm + + def errorbox(self, title, message): + # XXX This should really be a function of EditorWindow... + 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 71fd18a..fd9f0ff 100644 --- a/Lib/idlelib/scrolledlist.py +++ b/Lib/idlelib/ScrolledList.py @@ -1,8 +1,5 @@ -from tkinter import * -from tkinter.ttk import Frame, Scrollbar - -from idlelib import macosx - +from Tkinter import * +from idlelib import macosxSupport class ScrolledList: @@ -26,7 +23,7 @@ class ScrolledList: # Bind events to the list box listbox.bind("<ButtonRelease-1>", self.click_event) listbox.bind("<Double-ButtonRelease-1>", self.double_click_event) - if macosx.isAquaTk(): + if macosxSupport.isAquaTk(): listbox.bind("<ButtonPress-2>", self.popup_event) listbox.bind("<Control-Button-1>", self.popup_event) else: @@ -76,7 +73,6 @@ class ScrolledList: index = self.listbox.index("active") self.select(index) menu.tk_popup(event.x_root, event.y_root) - return "break" def make_menu(self): menu = Menu(self.listbox, tearoff=0) @@ -128,22 +124,22 @@ class ScrolledList: pass -def _scrolled_list(parent): # htest # - top = Toplevel(parent) - x, y = map(int, parent.geometry().split('+')[1:]) - top.geometry("+%d+%d" % (x+200, y + 175)) +def _scrolled_list(parent): + root = Tk() + 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="right click") - def on_select(self, index): print("select", self.get(index)) - def on_double(self, index): print("double", self.get(index)) + def on_select(self, index): print "select", self.get(index) + def on_double(self, index): print "double", self.get(index) - scrolled_list = MyScrolledList(top) + scrolled_list = MyScrolledList(root) for i in range(30): scrolled_list.append("Item %02d" % i) -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_scrolledlist', verbosity=2,) + root.mainloop() +if __name__ == '__main__': from idlelib.idle_test.htest import run run(_scrolled_list) diff --git a/Lib/idlelib/SearchDialog.py b/Lib/idlelib/SearchDialog.py new file mode 100644 index 0000000..043168a --- /dev/null +++ b/Lib/idlelib/SearchDialog.py @@ -0,0 +1,89 @@ +from Tkinter import * + +from idlelib import SearchEngine +from idlelib.SearchDialogBase import SearchDialogBase + +def _setup(text): + root = text._root() + engine = SearchEngine.get(root) + if not hasattr(engine, "_searchdialog"): + engine._searchdialog = SearchDialog(root, engine) + return engine._searchdialog + +def find(text): + pat = text.get("sel.first", "sel.last") + return _setup(text).open(text,pat) + +def find_again(text): + return _setup(text).find_again(text) + +def find_selection(text): + return _setup(text).find_selection(text) + +class SearchDialog(SearchDialogBase): + + def create_widgets(self): + SearchDialogBase.create_widgets(self) + self.make_button("Find Next", self.default_command, 1) + + def default_command(self, event=None): + if not self.engine.getprog(): + return + self.find_again(self.text) + + def find_again(self, text): + if not self.engine.getpat(): + self.open(text) + return False + if not self.engine.getprog(): + return False + res = self.engine.search_text(text) + if res: + line, m = res + i, j = m.span() + first = "%d.%d" % (line, i) + last = "%d.%d" % (line, j) + try: + selfirst = text.index("sel.first") + sellast = text.index("sel.last") + if selfirst == first and sellast == last: + text.bell() + return False + except TclError: + pass + text.tag_remove("sel", "1.0", "end") + text.tag_add("sel", first, last) + text.mark_set("insert", self.engine.isback() and first or last) + text.see("insert") + return True + else: + text.bell() + return False + + def find_selection(self, text): + pat = text.get("sel.first", "sel.last") + 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/searchbase.py b/Lib/idlelib/SearchDialogBase.py index 6fba0b8..9eb8b22 100644 --- a/Lib/idlelib/searchbase.py +++ b/Lib/idlelib/SearchDialogBase.py @@ -1,8 +1,7 @@ '''Define SearchDialogBase used by Search, Replace, and Grep dialogs.''' -from tkinter import Toplevel -from tkinter.ttk import Frame, Entry, Label, Button, Checkbutton, Radiobutton - +from Tkinter import (Toplevel, Frame, Entry, Label, Button, + Checkbutton, Radiobutton) class SearchDialogBase: '''Create most of a 3 or 4 row, 3 column search dialog. @@ -36,13 +35,12 @@ class SearchDialogBase: 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_widgets(). + 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.bell = root.bell self.engine = engine self.top = None @@ -129,7 +127,7 @@ class SearchDialogBase: def create_option_buttons(self): '''Return (filled frame, options) for testing. - Options is a list of searchengine booleanvar, label pairs. + 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. ''' @@ -141,8 +139,10 @@ class SearchDialogBase: if self.needwrapbutton: options.append((engine.wrapvar, "Wrap around")) for var, label in options: - btn = Checkbutton(frame, variable=var, text=label) + 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): @@ -155,8 +155,11 @@ class SearchDialogBase: var = self.engine.backvar others = [(1, 'Up'), (0, 'Down')] for val, label in others: - btn = Radiobutton(frame, variable=var, value=val, text=label) + 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): @@ -174,30 +177,10 @@ class SearchDialogBase: 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 = self.make_button("close", self.close) b.lower() - -class _searchbase(SearchDialogBase): # htest # - "Create auto-opening dialog with no text connection." - - def __init__(self, parent): - import re - from idlelib import searchengine - - self.root = parent - self.engine = searchengine.get(parent) - self.create_widgets() - print(parent.geometry()) - width,height, x,y = list(map(int, re.split('[x+]', parent.geometry()))) - self.top.geometry("+%d+%d" % (x + 40, y + 175)) - - def default_command(self, dummy): pass - - if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_searchbase', verbosity=2, exit=False) - - from idlelib.idle_test.htest import run - run(_searchbase) + import unittest + unittest.main( + 'idlelib.idle_test.test_searchdialogbase', verbosity=2) diff --git a/Lib/idlelib/searchengine.py b/Lib/idlelib/SearchEngine.py index 911e7d4..ad43130 100644 --- a/Lib/idlelib/searchengine.py +++ b/Lib/idlelib/SearchEngine.py @@ -1,8 +1,7 @@ '''Define SearchEngine for search dialogs.''' import re - -from tkinter import StringVar, BooleanVar, TclError -import tkinter.messagebox as tkMessageBox +from Tkinter import StringVar, BooleanVar, TclError +import tkMessageBox def get(root): '''Return the singleton SearchEngine instance for the process. @@ -15,7 +14,6 @@ def get(root): # This creates a cycle that persists until root is deleted. return root._searchengine - class SearchEngine: """Handles searching a text widget for Find, Replace, and Grep.""" @@ -59,7 +57,7 @@ class SearchEngine: def setcookedpat(self, pat): "Set pattern after escaping if re." - # called only in search.py: 66 + # called only in SearchDialog.py: 66 if self.isre(): pat = re.escape(pat) self.setpat(pat) @@ -188,7 +186,6 @@ class SearchEngine: col = len(chars) - 1 return None - def search_reverse(prog, chars, col): '''Search backwards and return an re match object or None. @@ -231,7 +228,6 @@ def get_line_col(index): line, col = map(int, index.split(".")) # Fails on invalid index return line, col - if __name__ == "__main__": - from unittest import main - main('idlelib.idle_test.test_searchengine', verbosity=2) + 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 94ffb4e..555a08c 100644 --- a/Lib/idlelib/stackviewer.py +++ b/Lib/idlelib/StackViewer.py @@ -1,14 +1,14 @@ -import linecache import os import sys +import linecache +import re +import Tkinter as tk -import tkinter as tk - -from idlelib.debugobj import ObjectTreeItem, make_objecttreeitem -from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas +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): - global sc, item, node # For testing. if top is None: top = tk.Toplevel(root) sc = ScrolledCanvas(top, bg="white", highlightthickness=0) @@ -17,7 +17,6 @@ def StackBrowser(root, flist=None, tb=None, top=None): node = TreeNode(sc.canvas, None, item) node.expand() - class StackTreeItem(TreeItem): def __init__(self, flist=None, tb=None): @@ -56,7 +55,6 @@ class StackTreeItem(TreeItem): sublist.append(item) return sublist - class FrameTreeItem(TreeItem): def __init__(self, info, flist): @@ -98,7 +96,6 @@ class FrameTreeItem(TreeItem): if os.path.isfile(filename): self.flist.gotofileline(filename, lineno) - class VariablesTreeItem(ObjectTreeItem): def GetText(self): @@ -123,24 +120,26 @@ class VariablesTreeItem(ObjectTreeItem): sublist.append(item) return sublist + def keys(self): # unused, left for possible 3rd party use + return self.object.keys() def _stack_viewer(parent): # htest # - from idlelib.pyshell import PyShellFileList - top = tk.Toplevel(parent) - top.title("Test StackViewer") - x, y = map(int, parent.geometry().split('+')[1:]) - top.geometry("+%d+%d" % (x + 50, y + 175)) - flist = PyShellFileList(top) + 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(top, flist=flist, top=top, tb=exc_tb) + StackBrowser(root, flist=flist, top=root, tb=exc_tb) # restore sys to original state del sys.last_type @@ -148,8 +147,5 @@ def _stack_viewer(parent): # htest # del sys.last_traceback if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_stackviewer', verbosity=2, exit=False) - from idlelib.idle_test.htest import run run(_stack_viewer) diff --git a/Lib/idlelib/ToolTip.py b/Lib/idlelib/ToolTip.py new file mode 100644 index 0000000..11136c4 --- /dev/null +++ b/Lib/idlelib/ToolTip.py @@ -0,0 +1,97 @@ +# general purpose 'tooltip' routines - currently unused in idlefork +# (although the 'calltips' extension is partly based on this code) +# may be useful for some purposes in (or almost in ;) the current project scope +# Ideas gleaned from PySol + +from Tkinter import * + +class ToolTipBase: + + def __init__(self, button): + self.button = button + self.tipwindow = None + self.id = None + self.x = self.y = 0 + self._id1 = self.button.bind("<Enter>", self.enter) + self._id2 = self.button.bind("<Leave>", self.leave) + self._id3 = self.button.bind("<ButtonPress>", self.leave) + + def enter(self, event=None): + self.schedule() + + def leave(self, event=None): + self.unschedule() + self.hidetip() + + def schedule(self): + self.unschedule() + self.id = self.button.after(1500, self.showtip) + + def unschedule(self): + id = self.id + self.id = None + if id: + self.button.after_cancel(id) + + def showtip(self): + if self.tipwindow: + return + # The tip window must be completely outside the button; + # otherwise when the mouse enters the tip window we get + # a leave event and it disappears, and then we get an enter + # event and it reappears, and so on forever :-( + x = self.button.winfo_rootx() + 20 + y = self.button.winfo_rooty() + self.button.winfo_height() + 1 + self.tipwindow = tw = Toplevel(self.button) + tw.wm_overrideredirect(1) + tw.wm_geometry("+%d+%d" % (x, y)) + self.showcontents() + + def showcontents(self, text="Your text here"): + # Override this in derived class + label = Label(self.tipwindow, text=text, justify=LEFT, + background="#ffffe0", relief=SOLID, borderwidth=1) + label.pack() + + def hidetip(self): + tw = self.tipwindow + self.tipwindow = None + if tw: + tw.destroy() + +class ToolTip(ToolTipBase): + def __init__(self, button, text): + ToolTipBase.__init__(self, button) + self.text = text + def showcontents(self): + ToolTipBase.showcontents(self, self.text) + +class ListboxToolTip(ToolTipBase): + def __init__(self, button, items): + ToolTipBase.__init__(self, button) + self.items = items + def showcontents(self): + listbox = Listbox(self.tipwindow, background="#ffffe0") + listbox.pack() + for item in self.items: + listbox.insert(END, item) + +def _tooltip(parent): + root = Tk() + 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__': + from idlelib.idle_test.htest import run + run(_tooltip) diff --git a/Lib/idlelib/tree.py b/Lib/idlelib/TreeWidget.py index 6229be4..9d9d4d9 100644 --- a/Lib/idlelib/tree.py +++ b/Lib/idlelib/TreeWidget.py @@ -15,12 +15,11 @@ # - optimize tree redraw after expand of subnode import os +from Tkinter import * +import imp -from tkinter import * -from tkinter.ttk import Frame, Scrollbar - -from idlelib.config import idleConf -from idlelib import zoomheight +from idlelib import ZoomHeight +from idlelib.configHandler import idleConf ICONDIR = "Icons" @@ -32,7 +31,7 @@ except NameError: if os.path.isdir(_icondir): ICONDIR = _icondir elif not os.path.isdir(ICONDIR): - raise RuntimeError("can't find icon directory (%r)" % (ICONDIR,)) + raise RuntimeError, "can't find icon directory (%r)" % (ICONDIR,) def listicons(icondir=ICONDIR): """Utility to display the available icons.""" @@ -56,30 +55,6 @@ def listicons(icondir=ICONDIR): column = 0 root.images = images -def wheel_event(event, widget=None): - """Handle scrollwheel event. - - For wheel up, event.delta = 120*n on Windows, -1*n on darwin, - where n can be > 1 if one scrolls fast. Flicking the wheel - generates up to maybe 20 events with n up to 10 or more 1. - Macs use wheel down (delta = 1*n) to scroll up, so positive - delta means to scroll up on both systems. - - X-11 sends Control-Button-4,5 events instead. - - The widget parameter is needed so browser label bindings can pass - the underlying canvas. - - This function depends on widget.yview to not be overridden by - a subclass. - """ - up = {EventType.MouseWheel: event.delta > 0, - EventType.ButtonPress: event.num == 4} - lines = -5 if up[event.type] else 5 - widget = event.widget if widget is None else widget - widget.yview(SCROLL, lines, 'units') - return 'break' - class TreeNode: @@ -284,9 +259,6 @@ class TreeNode: anchor="nw", window=self.label) self.label.bind("<1>", self.select_or_edit) self.label.bind("<Double-1>", self.flip) - self.label.bind("<MouseWheel>", lambda e: wheel_event(e, self.canvas)) - self.label.bind("<Button-4>", lambda e: wheel_event(e, self.canvas)) - self.label.bind("<Button-5>", lambda e: wheel_event(e, self.canvas)) self.text_id = id def select_or_edit(self, event=None): @@ -411,7 +383,7 @@ class FileTreeItem(TreeItem): try: os.rename(self.path, newpath) self.path = newpath - except OSError: + except os.error: pass def GetIconName(self): @@ -424,7 +396,7 @@ class FileTreeItem(TreeItem): def GetSubList(self): try: names = os.listdir(self.path) - except OSError: + except os.error: return [] names.sort(key = os.path.normcase) sublist = [] @@ -437,7 +409,6 @@ class FileTreeItem(TreeItem): # A canvas widget with scroll bars and some useful bindings class ScrolledCanvas: - def __init__(self, master, **opts): if 'yscrollincrement' not in opts: opts['yscrollincrement'] = 17 @@ -459,9 +430,6 @@ class ScrolledCanvas: self.canvas.bind("<Key-Next>", self.page_down) self.canvas.bind("<Key-Up>", self.unit_up) self.canvas.bind("<Key-Down>", self.unit_down) - self.canvas.bind("<MouseWheel>", wheel_event) - self.canvas.bind("<Button-4>", wheel_event) - self.canvas.bind("<Button-5>", wheel_event) #if isinstance(master, Toplevel) or isinstance(master, Tk): self.canvas.bind("<Alt-Key-2>", self.zoom_height) self.canvas.focus_set() @@ -478,23 +446,22 @@ class ScrolledCanvas: self.canvas.yview_scroll(1, "unit") return "break" def zoom_height(self, event): - zoomheight.zoom_height(self.master) + ZoomHeight.zoom_height(self.master) return "break" -def _tree_widget(parent): # htest # - top = Toplevel(parent) - x, y = map(int, parent.geometry().split('+')[1:]) - top.geometry("+%d+%d" % (x+50, y+175)) - sc = ScrolledCanvas(top, bg="white", highlightthickness=0, takefocus=1) +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", side=LEFT) - item = FileTreeItem(ICONDIR) + item = FileTreeItem(os.getcwd()) node = TreeNode(sc.canvas, None, item) node.expand() + root.mainloop() if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_tree', verbosity=2, exit=False) - from idlelib.idle_test.htest import run run(_tree_widget) diff --git a/Lib/idlelib/undo.py b/Lib/idlelib/UndoDelegator.py index 85ecffe..cdeacea 100644 --- a/Lib/idlelib/undo.py +++ b/Lib/idlelib/UndoDelegator.py @@ -1,9 +1,7 @@ import string +from Tkinter import * -from idlelib.delegator import Delegator - -# tkinter import not needed because module does not create widgets, -# although many methods operate on text widget arguments. +from idlelib.Delegator import Delegator #$ event <<redo>> #$ win <Control-y> @@ -40,10 +38,10 @@ class UndoDelegator(Delegator): def dump_event(self, event): from pprint import pprint pprint(self.undolist[:self.pointer]) - print("pointer:", self.pointer, end=' ') - print("saved:", self.saved, end=' ') - print("can_merge:", self.can_merge, end=' ') - print("get_saved():", self.get_saved()) + print "pointer:", self.pointer, + print "saved:", self.saved, + print "can_merge:", self.can_merge, + print "get_saved():", self.get_saved() pprint(self.undolist[self.pointer:]) return "break" @@ -160,6 +158,7 @@ class UndoDelegator(Delegator): class Command: + # Base class for Undoable commands tags = None @@ -205,6 +204,7 @@ class Command: class InsertCommand(Command): + # Undoable insert command def __init__(self, index1, chars, tags=None): @@ -262,6 +262,7 @@ class InsertCommand(Command): class DeleteCommand(Command): + # Undoable delete command def __init__(self, index1, index2=None): @@ -296,8 +297,8 @@ class DeleteCommand(Command): text.see('insert') ##sys.__stderr__.write("undo: %s\n" % self) - class CommandSequence(Command): + # Wrapper for a sequence of undoable cmds to be undone/redone # as a unit @@ -335,32 +336,30 @@ class CommandSequence(Command): self.depth = self.depth + incr return self.depth +def _undo_delegator(parent): + from idlelib.Percolator import Percolator + root = Tk() + root.title("Test UndoDelegator") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) -def _undo_delegator(parent): # htest # - from tkinter import Toplevel, Text, Button - from idlelib.percolator import Percolator - undowin = Toplevel(parent) - undowin.title("Test UndoDelegator") - x, y = map(int, parent.geometry().split('+')[1:]) - undowin.geometry("+%d+%d" % (x, y + 175)) - - text = Text(undowin, height=10) + text = Text(root) + text.config(height=10) text.pack() text.focus_set() p = Percolator(text) d = UndoDelegator() p.insertfilter(d) - undo = Button(undowin, text="Undo", command=lambda:d.undo_event(None)) + undo = Button(root, text="Undo", command=lambda:d.undo_event(None)) undo.pack(side='left') - redo = Button(undowin, text="Redo", command=lambda:d.redo_event(None)) + redo = Button(root, text="Redo", command=lambda:d.redo_event(None)) redo.pack(side='left') - dump = Button(undowin, text="Dump", command=lambda:d.dump_event(None)) + dump = Button(root, text="Dump", command=lambda:d.dump_event(None)) dump.pack(side='left') -if __name__ == "__main__": - from unittest import main - main('idlelib.idle_test.test_undo', verbosity=2, exit=False) + root.mainloop() +if __name__ == "__main__": from idlelib.idle_test.htest import run run(_undo_delegator) diff --git a/Lib/idlelib/redirector.py b/Lib/idlelib/WidgetRedirector.py index 9ab34c5..54431f7 100644 --- a/Lib/idlelib/redirector.py +++ b/Lib/idlelib/WidgetRedirector.py @@ -1,4 +1,5 @@ -from tkinter import TclError +from __future__ import print_function +from Tkinter import TclError class WidgetRedirector: """Support for redirecting arbitrary widget subcommands. @@ -16,7 +17,7 @@ class WidgetRedirector: 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. We will also intercept method calls on the tkinter class + 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 @@ -47,9 +48,8 @@ class WidgetRedirector: tk.createcommand(w, self.dispatch) def __repr__(self): - return "%s(%s<%s>)" % (self.__class__.__name__, - self.widget.__class__.__name__, - self.widget._w) + return "WidgetRedirector(%s<%s>)" % (self.widget.__class__.__name__, + self.widget._w) def close(self): "Unregister operations and revert redirection created by .__init__." @@ -68,7 +68,7 @@ class WidgetRedirector: '''Return OriginalCommand(operation) after registering function. Registration adds an operation: function pair to ._operations. - It also adds a widget function attribute that masks the tkinter + It also adds a widget function attribute that masks the Tkinter class instance method. Method masking operates independently from command dispatch. @@ -104,7 +104,7 @@ class WidgetRedirector: Note that if a registered function is called, the operation is not passed through to Tk. Apply the function returned by self.register() - to *args to accomplish that. For an example, see colorizer.py. + to *args to accomplish that. For an example, see ColorDelegator.py. ''' m = self._operations.get(operation) @@ -143,21 +143,21 @@ class OriginalCommand: self.orig_and_operation = (redir.orig, operation) def __repr__(self): - return "%s(%r, %r)" % (self.__class__.__name__, - self.redir, self.operation) + return "OriginalCommand(%r, %r)" % (self.redir, self.operation) def __call__(self, *args): return self.tk_call(self.orig_and_operation + args) def _widget_redirector(parent): # htest # - from tkinter import Toplevel, Text - - top = Toplevel(parent) - top.title("Test WidgetRedirector") - x, y = map(int, parent.geometry().split('+')[1:]) - top.geometry("+%d+%d" % (x, y + 175)) - text = Text(top) + from Tkinter import Tk, Text + import re + + root = Tk() + 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) @@ -165,10 +165,11 @@ def _widget_redirector(parent): # htest # print("insert", args) original_insert(*args) original_insert = redir.register("insert", my_insert) + root.mainloop() if __name__ == "__main__": - from unittest import main - main('idlelib.idle_test.test_redirector', verbosity=2, exit=False) - + 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/window.py b/Lib/idlelib/WindowList.py index 460d5b6..658502b 100644 --- a/Lib/idlelib/window.py +++ b/Lib/idlelib/WindowList.py @@ -1,6 +1,4 @@ -from tkinter import Toplevel, TclError -import sys - +from Tkinter import * class WindowList: @@ -22,15 +20,15 @@ class WindowList: def add_windows_to_menu(self, menu): list = [] - for key in self.dict: + for key in self.dict.keys(): window = self.dict[key] try: title = window.get_title() except TclError: continue - list.append((title, key, window)) + list.append((title, window)) list.sort() - for title, key, window in list: + for title, window in list: menu.add_command(label=title, command=window.wakeup) def register_callback(self, callback): @@ -47,9 +45,8 @@ class WindowList: try: callback() except: - t, v, tb = sys.exc_info() - print("warning: callback failed in WindowList", t, ":", v) - + print "warning: callback failed in WindowList", \ + sys.exc_type, ":", sys.exc_value registry = WindowList() @@ -88,11 +85,6 @@ class ListedToplevel(Toplevel): self.tkraise() self.focused_widget.focus_set() except TclError: - # This can happen when the Window menu was torn off. + # This can happen when the window menu was torn off. # Simply ignore it. pass - - -if __name__ == "__main__": - from unittest import main - main('idlelib.idle_test.test_window', verbosity=2) diff --git a/Lib/idlelib/ZoomHeight.py b/Lib/idlelib/ZoomHeight.py new file mode 100644 index 0000000..a5d679e --- /dev/null +++ b/Lib/idlelib/ZoomHeight.py @@ -0,0 +1,51 @@ +# Sample extension: zoom a window to maximum height + +import re +import sys + +from idlelib import macosxSupport + +class ZoomHeight: + + menudefs = [ + ('windows', [ + ('_Zoom Height', '<<zoom-height>>'), + ]) + ] + + def __init__(self, editwin): + self.editwin = editwin + + def zoom_height_event(self, event): + top = self.editwin.top + zoom_height(top) + +def zoom_height(top): + geom = top.wm_geometry() + m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom) + if not m: + top.bell() + return + width, height, x, y = map(int, m.groups()) + newheight = top.winfo_screenheight() + if sys.platform == 'win32': + newy = 0 + newheight = newheight - 72 + + 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. + newy = 22 + newheight = newheight - newy - 88 + + else: + #newy = 24 + newy = 0 + #newheight = newheight - 96 + newheight = newheight - 88 + if height >= newheight: + newgeom = "" + else: + newgeom = "%dx%d+%d+%d" % (width, newheight, x, newy) + top.wm_geometry(newgeom) diff --git a/Lib/idlelib/__init__.py b/Lib/idlelib/__init__.py index 791ddea..32b7eac 100644 --- a/Lib/idlelib/__init__.py +++ b/Lib/idlelib/__init__.py @@ -1,10 +1,8 @@ """The idlelib package implements the Idle application. Idle includes an interactive shell and editor. -Starting with Python 3.6, IDLE requires tcl/tk 8.5 or later. 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. +The other files are private implementations. Their details are subject +to change. See PEP 434 for more. Import them at your own risk. """ -testing = False # Set True by test.test_idle. diff --git a/Lib/idlelib/__main__.py b/Lib/idlelib/__main__.py deleted file mode 100644 index 6349ec7..0000000 --- a/Lib/idlelib/__main__.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -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 new file mode 100644 index 0000000..87d6c3c --- /dev/null +++ b/Lib/idlelib/aboutDialog.py @@ -0,0 +1,151 @@ +"""About Dialog for IDLE + +""" +import os +from sys import version +from Tkinter import * +from idlelib import textView + +class AboutDialog(Toplevel): + """Modal about dialog for idle + + """ + def __init__(self, parent, title, _htest=False): + """ + _htest - bool, change box location when running htest + """ + Toplevel.__init__(self, parent) + self.configure(borderwidth=5) + # 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() + self.resizable(height=FALSE, width=FALSE) + self.title(title) + self.transient(parent) + self.grab_set() + self.protocol("WM_DELETE_WINDOW", self.Ok) + self.parent = parent + self.buttonOk.focus_set() + self.bind('<Return>',self.Ok) #dismiss dialog + self.bind('<Escape>',self.Ok) #dismiss dialog + 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) + frameMain.pack(side=TOP, expand=TRUE, fill=BOTH) + self.buttonOk = Button(frameButtons, text='Close', + command=self.Ok) + self.buttonOk.pack(padx=5, pady=5) + #self.picture = Image('photo', data=self.pictureData) + frameBg = Frame(frameMain, bg=self.bg) + frameBg.pack(expand=TRUE, fill=BOTH) + labelTitle = Label(frameBg, text='IDLE', fg=self.fg, bg=self.bg, + font=('courier', 24, 'bold')) + labelTitle.grid(row=0, column=0, sticky=W, padx=10, pady=10) + #labelPicture = Label(frameBg, text='[picture]') + #image=self.picture, bg=self.bg) + #labelPicture.grid(row=1, column=1, sticky=W, rowspan=2, + # padx=0, pady=3) + byline = "Python's Integrated DeveLopment Environment" + 5*'\n' + labelDesc = Label(frameBg, text=byline, justify=LEFT, + fg=self.fg, bg=self.bg) + labelDesc.grid(row=2, column=0, sticky=W, columnspan=3, padx=10, pady=5) + labelEmail = Label(frameBg, text='email: idle-dev@python.org', + 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='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: ' + + 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: '+ + tkVer, fg=self.fg, bg=self.bg) + labelTkVer.grid(row=9, column=1, sticky=W, padx=2, pady=0) + py_button_f = Frame(frameBg, bg=self.bg) + py_button_f.grid(row=10, column=0, columnspan=2, sticky=NSEW) + buttonLicense = Button(py_button_f, text='License', width=8, + highlightbackground=self.bg, + command=self.ShowLicense) + buttonLicense.pack(side=LEFT, padx=10, pady=10) + buttonCopyright = Button(py_button_f, text='Copyright', width=8, + highlightbackground=self.bg, + command=self.ShowCopyright) + buttonCopyright.pack(side=LEFT, padx=10, pady=10) + buttonCredits = Button(py_button_f, text='Credits', width=8, + highlightbackground=self.bg, + command=self.ShowPythonCredits) + buttonCredits.pack(side=LEFT, padx=10, pady=10) + 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: ' + 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) + idle_button_f.grid(row=13, column=0, columnspan=3, sticky=NSEW) + idle_about_b = Button(idle_button_f, text='README', width=8, + highlightbackground=self.bg, + command=self.ShowIDLEAbout) + idle_about_b.pack(side=LEFT, padx=10, pady=10) + idle_news_b = Button(idle_button_f, text='NEWS', width=8, + highlightbackground=self.bg, + command=self.ShowIDLENEWS) + idle_news_b.pack(side=LEFT, padx=10, pady=10) + idle_credits_b = Button(idle_button_f, text='Credits', width=8, + highlightbackground=self.bg, + command=self.ShowIDLECredits) + idle_credits_b.pack(side=LEFT, padx=10, pady=10) + + # License, et all, are of type _sitebuiltins._Printer + def ShowLicense(self): + self.display_printer_text('About - License', license) + + def ShowCopyright(self): + self.display_printer_text('About - Copyright', copyright) + + def ShowPythonCredits(self): + self.display_printer_text('About - Python Credits', credits) + + # Encode CREDITS.txt to utf-8 for proper version of Loewis. + # Specify others as ascii until need utf-8, so catch errors. + def ShowIDLECredits(self): + self.display_file_text('About - Credits', 'CREDITS.txt', 'utf-8') + + def ShowIDLEAbout(self): + self.display_file_text('About - Readme', 'README.txt', 'ascii') + + def ShowIDLENEWS(self): + self.display_file_text('About - NEWS', 'NEWS.txt', 'utf-8') + + def display_printer_text(self, title, printer): + printer._Printer__setup() + text = '\n'.join(printer._Printer__lines) + textView.view_text(self, title, text) + + def display_file_text(self, title, filename, encoding=None): + fn = os.path.join(os.path.abspath(os.path.dirname(__file__)), filename) + textView.view_file(self, title, fn, encoding) + + def Ok(self, event=None): + self.grab_release() + self.destroy() + +if __name__ == '__main__': + import unittest + unittest.main('idlelib.idle_test.test_helpabout', verbosity=2, exit=False) + from idlelib.idle_test.htest import run + run(AboutDialog) diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py deleted file mode 100644 index 3c3a53a..0000000 --- a/Lib/idlelib/browser.py +++ /dev/null @@ -1,249 +0,0 @@ -"""Module browser. - -XXX TO DO: - -- reparse when source changed (maybe just a button would be OK?) - (or recheck on window popup) -- add popup menu with more options (e.g. doc strings, base classes, imports) -- add base classes to class browser tree -- finish removing limitation to x.py files (ModuleBrowserTreeItem) -""" - -import os -import pyclbr -import sys - -from idlelib.config import idleConf -from idlelib import pyshell -from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas -from idlelib.window import ListedToplevel - - -file_open = None # Method...Item and Class...Item use this. -# Normally pyshell.flist.open, but there is no pyshell.flist for htest. - - -def transform_children(child_dict, modname=None): - """Transform a child dictionary to an ordered sequence of objects. - - The dictionary maps names to pyclbr information objects. - Filter out imported objects. - Augment class names with bases. - The insertion order of the dictionary is assumed to have been in line - number order, so sorting is not necessary. - - The current tree only calls this once per child_dict as it saves - TreeItems once created. A future tree and tests might violate this, - so a check prevents multiple in-place augmentations. - """ - obs = [] # Use list since values should already be sorted. - for key, obj in child_dict.items(): - if modname is None or obj.module == modname: - if hasattr(obj, 'super') and obj.super and obj.name == key: - # If obj.name != key, it has already been suffixed. - supers = [] - for sup in obj.super: - if type(sup) is type(''): - sname = sup - else: - sname = sup.name - if sup.module != obj.module: - sname = f'{sup.module}.{sname}' - supers.append(sname) - obj.name += '({})'.format(', '.join(supers)) - obs.append(obj) - return obs - - -class ModuleBrowser: - """Browse module classes and functions in IDLE. - """ - # This class is also the base class for pathbrowser.PathBrowser. - # Init and close are inherited, other methods are overridden. - # PathBrowser.__init__ does not call __init__ below. - - def __init__(self, master, path, *, _htest=False, _utest=False): - """Create a window for browsing a module's structure. - - Args: - master: parent for widgets. - path: full path of file to browse. - _htest - bool; change box location when running htest. - -utest - bool; suppress contents when running unittest. - - Global variables: - file_open: Function used for opening a file. - - Instance variables: - name: Module name. - file: Full path and module with .py extension. Used in - creating ModuleBrowserTreeItem as the rootnode for - the tree and subsequently in the children. - """ - self.master = master - self.path = path - self._htest = _htest - self._utest = _utest - self.init() - - def close(self, event=None): - "Dismiss the window and the tree nodes." - self.top.destroy() - self.node.destroy() - - def init(self): - "Create browser tkinter widgets, including the tree." - global file_open - root = self.master - flist = (pyshell.flist if not (self._htest or self._utest) - else pyshell.PyShellFileList(root)) - file_open = flist.open - pyclbr._modules.clear() - - # create top - self.top = top = ListedToplevel(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" % - (root.winfo_rootx(), root.winfo_rooty() + 200)) - self.settitle() - top.focus_set() - - # create scrolled canvas - 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") - item = self.rootnode() - self.node = node = TreeNode(sc.canvas, None, item) - if not self._utest: - node.update() - node.expand() - - def settitle(self): - "Set the window title." - self.top.wm_title("Module Browser - " + os.path.basename(self.path)) - self.top.wm_iconname("Module Browser") - - def rootnode(self): - "Return a ModuleBrowserTreeItem as the root of the tree." - return ModuleBrowserTreeItem(self.path) - - -class ModuleBrowserTreeItem(TreeItem): - """Browser tree for Python module. - - Uses TreeItem as the basis for the structure of the tree. - Used by both browsers. - """ - - def __init__(self, file): - """Create a TreeItem for the file. - - Args: - file: Full path and module name. - """ - self.file = file - - def GetText(self): - "Return the module name as the text string to display." - return os.path.basename(self.file) - - def GetIconName(self): - "Return the name of the icon to display." - return "python" - - def GetSubList(self): - "Return ChildBrowserTreeItems for children." - return [ChildBrowserTreeItem(obj) for obj in self.listchildren()] - - def OnDoubleClick(self): - "Open a module in an editor window when double clicked." - if os.path.normcase(self.file[-3:]) != ".py": - return - if not os.path.exists(self.file): - return - file_open(self.file) - - def IsExpandable(self): - "Return True if Python (.py) file." - return os.path.normcase(self.file[-3:]) == ".py" - - def listchildren(self): - "Return sequenced classes and functions in the module." - dir, base = os.path.split(self.file) - name, ext = os.path.splitext(base) - if os.path.normcase(ext) != ".py": - return [] - try: - tree = pyclbr.readmodule_ex(name, [dir] + sys.path) - except ImportError: - return [] - return transform_children(tree, name) - - -class ChildBrowserTreeItem(TreeItem): - """Browser tree for child nodes within the module. - - Uses TreeItem as the basis for the structure of the tree. - """ - - def __init__(self, obj): - "Create a TreeItem for a pyclbr class/function object." - self.obj = obj - self.name = obj.name - self.isfunction = isinstance(obj, pyclbr.Function) - - def GetText(self): - "Return the name of the function/class to display." - name = self.name - if self.isfunction: - return "def " + name + "(...)" - else: - return "class " + name - - def GetIconName(self): - "Return the name of the icon to display." - if self.isfunction: - return "python" - else: - return "folder" - - def IsExpandable(self): - "Return True if self.obj has nested objects." - return self.obj.children != {} - - def GetSubList(self): - "Return ChildBrowserTreeItems for children." - return [ChildBrowserTreeItem(obj) - for obj in transform_children(self.obj.children)] - - def OnDoubleClick(self): - "Open module with file_open and position to lineno." - try: - edit = file_open(self.obj.file) - edit.gotoline(self.obj.lineno) - except (OSError, AttributeError): - pass - - -def _module_browser(parent): # htest # - if len(sys.argv) > 1: # If pass file on command line. - file = sys.argv[1] - else: - file = __file__ - # Add nested objects for htest. - class Nested_in_func(TreeNode): - def nested_in_class(): pass - def closure(): - class Nested_in_closure: pass - ModuleBrowser(parent, file, _htest=True) - -if __name__ == "__main__": - if len(sys.argv) == 1: # If pass file on command line, unittest fails. - from unittest import main - main('idlelib.idle_test.test_browser', verbosity=2, exit=False) - from idlelib.idle_test.htest import run - run(_module_browser) diff --git a/Lib/idlelib/calltip.py b/Lib/idlelib/calltip.py deleted file mode 100644 index a3dda26..0000000 --- a/Lib/idlelib/calltip.py +++ /dev/null @@ -1,177 +0,0 @@ -"""Pop up a reminder of how to call a function. - -Call Tips are floating windows which display function, class, and method -parameter and docstring information when you type an opening parenthesis, and -which disappear when you type a closing parenthesis. -""" -import __main__ -import inspect -import re -import sys -import textwrap -import types - -from idlelib import calltip_w -from idlelib.hyperparser import HyperParser - - -class Calltip: - - def __init__(self, editwin=None): - if editwin is None: # subprocess and test - self.editwin = None - else: - self.editwin = editwin - self.text = editwin.text - self.active_calltip = None - self._calltip_window = self._make_tk_calltip_window - - def close(self): - self._calltip_window = None - - def _make_tk_calltip_window(self): - # See __init__ for usage - return calltip_w.CalltipWindow(self.text) - - def _remove_calltip_window(self, event=None): - if self.active_calltip: - self.active_calltip.hidetip() - self.active_calltip = None - - def force_open_calltip_event(self, event): - "The user selected the menu entry or hotkey, open the tip." - self.open_calltip(True) - return "break" - - def try_open_calltip_event(self, event): - """Happens when it would be nice to open a calltip, but not really - necessary, for example after an opening bracket, so function calls - won't be made. - """ - self.open_calltip(False) - - def refresh_calltip_event(self, event): - if self.active_calltip and self.active_calltip.tipwindow: - self.open_calltip(False) - - def open_calltip(self, evalfuncs): - self._remove_calltip_window() - - hp = HyperParser(self.editwin, "insert") - sur_paren = hp.get_surrounding_brackets('(') - if not sur_paren: - return - hp.set_index(sur_paren[0]) - expression = hp.get_expression() - if not expression: - return - if not evalfuncs and (expression.find('(') != -1): - return - argspec = self.fetch_tip(expression) - if not argspec: - return - self.active_calltip = self._calltip_window() - self.active_calltip.showtip(argspec, sur_paren[0], sur_paren[1]) - - def fetch_tip(self, expression): - """Return the argument list and docstring of a function or class. - - If there is a Python subprocess, get the calltip there. Otherwise, - either this fetch_tip() is running in the subprocess or it was - called in an IDLE running without the subprocess. - - The subprocess environment is that of the most recently run script. If - two unrelated modules are being edited some calltips in the current - module may be inoperative if the module was not the last to run. - - To find methods, fetch_tip must be fed a fully qualified name. - - """ - try: - rpcclt = self.editwin.flist.pyshell.interp.rpcclt - except AttributeError: - rpcclt = None - if rpcclt: - return rpcclt.remotecall("exec", "get_the_calltip", - (expression,), {}) - else: - return get_argspec(get_entity(expression)) - - -def get_entity(expression): - """Return the object corresponding to expression evaluated - in a namespace spanning sys.modules and __main.dict__. - """ - if expression: - namespace = {**sys.modules, **__main__.__dict__} - try: - return eval(expression, namespace) # Only protect user code. - except BaseException: - # An uncaught exception closes idle, and eval can raise any - # exception, especially if user classes are involved. - return None - -# The following are used in get_argspec and some in tests -_MAX_COLS = 85 -_MAX_LINES = 5 # enough for bytes -_INDENT = ' '*4 # for wrapped signatures -_first_param = re.compile(r'(?<=\()\w*\,?\s*') -_default_callable_argspec = "See source or doc" -_invalid_method = "invalid method signature" -_argument_positional = " # '/' marks preceding args as positional-only." - -def get_argspec(ob): - '''Return a string describing the signature of a callable object, or ''. - - For Python-coded functions and methods, the first line is introspected. - Delete 'self' parameter for classes (.__init__) and bound methods. - The next lines are the first lines of the doc string up to the first - empty line or _MAX_LINES. For builtins, this typically includes - the arguments in addition to the return value. - ''' - argspec = default = "" - try: - ob_call = ob.__call__ - except BaseException: - return default - - fob = ob_call if isinstance(ob_call, types.MethodType) else ob - - try: - argspec = str(inspect.signature(fob)) - except ValueError as err: - msg = str(err) - if msg.startswith(_invalid_method): - return _invalid_method - - if '/' in argspec and len(argspec) < _MAX_COLS - len(_argument_positional): - # Add explanation TODO remove after 3.7, before 3.9. - argspec += _argument_positional - if isinstance(fob, type) and argspec == '()': - # If fob has no argument, use default callable argspec. - argspec = _default_callable_argspec - - lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT) - if len(argspec) > _MAX_COLS else [argspec] if argspec else []) - - if isinstance(ob_call, types.MethodType): - doc = ob_call.__doc__ - else: - doc = getattr(ob, "__doc__", "") - if doc: - for line in doc.split('\n', _MAX_LINES)[:_MAX_LINES]: - line = line.strip() - if not line: - break - if len(line) > _MAX_COLS: - line = line[: _MAX_COLS - 3] + '...' - lines.append(line) - argspec = '\n'.join(lines) - if not argspec: - argspec = _default_callable_argspec - return argspec - - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_calltip', verbosity=2) diff --git a/Lib/idlelib/calltip_w.py b/Lib/idlelib/calltip_w.py deleted file mode 100644 index 1e0404a..0000000 --- a/Lib/idlelib/calltip_w.py +++ /dev/null @@ -1,201 +0,0 @@ -"""A call-tip window class for Tkinter/IDLE. - -After tooltip.py, which uses ideas gleaned from PySol. -Used by calltip.py. -""" -from tkinter import Label, LEFT, SOLID, TclError - -from idlelib.tooltip import TooltipBase - -HIDE_EVENT = "<<calltipwindow-hide>>" -HIDE_SEQUENCES = ("<Key-Escape>", "<FocusOut>") -CHECKHIDE_EVENT = "<<calltipwindow-checkhide>>" -CHECKHIDE_SEQUENCES = ("<KeyRelease>", "<ButtonRelease>") -CHECKHIDE_TIME = 100 # milliseconds - -MARK_RIGHT = "calltipwindowregion_right" - - -class CalltipWindow(TooltipBase): - """A call-tip widget for tkinter text widgets.""" - - def __init__(self, text_widget): - """Create a call-tip; shown by showtip(). - - text_widget: a Text widget with code for which call-tips are desired - """ - # Note: The Text widget will be accessible as self.anchor_widget - super(CalltipWindow, self).__init__(text_widget) - - self.label = self.text = None - self.parenline = self.parencol = self.lastline = None - self.hideid = self.checkhideid = None - self.checkhide_after_id = None - - def get_position(self): - """Choose the position of the call-tip.""" - curline = int(self.anchor_widget.index("insert").split('.')[0]) - if curline == self.parenline: - anchor_index = (self.parenline, self.parencol) - else: - anchor_index = (curline, 0) - box = self.anchor_widget.bbox("%d.%d" % anchor_index) - if not box: - box = list(self.anchor_widget.bbox("insert")) - # align to left of window - box[0] = 0 - box[2] = 0 - return box[0] + 2, box[1] + box[3] - - def position_window(self): - "Reposition the window if needed." - curline = int(self.anchor_widget.index("insert").split('.')[0]) - if curline == self.lastline: - return - self.lastline = curline - self.anchor_widget.see("insert") - super(CalltipWindow, self).position_window() - - def showtip(self, text, parenleft, parenright): - """Show the call-tip, bind events which will close it and reposition it. - - text: the text to display in the call-tip - parenleft: index of the opening parenthesis in the text widget - parenright: index of the closing parenthesis in the text widget, - or the end of the line if there is no closing parenthesis - """ - # Only called in calltip.Calltip, where lines are truncated - self.text = text - if self.tipwindow or not self.text: - return - - self.anchor_widget.mark_set(MARK_RIGHT, parenright) - self.parenline, self.parencol = map( - int, self.anchor_widget.index(parenleft).split(".")) - - super(CalltipWindow, self).showtip() - - self._bind_events() - - def showcontents(self): - """Create the call-tip widget.""" - self.label = Label(self.tipwindow, text=self.text, justify=LEFT, - background="#ffffd0", foreground="black", - relief=SOLID, borderwidth=1, - font=self.anchor_widget['font']) - self.label.pack() - - def checkhide_event(self, event=None): - """Handle CHECK_HIDE_EVENT: call hidetip or reschedule.""" - if not self.tipwindow: - # If the event was triggered by the same event that unbound - # this function, the function will be called nevertheless, - # so do nothing in this case. - return None - - # Hide the call-tip if the insertion cursor moves outside of the - # parenthesis. - curline, curcol = map(int, self.anchor_widget.index("insert").split('.')) - if curline < self.parenline or \ - (curline == self.parenline and curcol <= self.parencol) or \ - self.anchor_widget.compare("insert", ">", MARK_RIGHT): - self.hidetip() - return "break" - - # Not hiding the call-tip. - - self.position_window() - # Re-schedule this function to be called again in a short while. - if self.checkhide_after_id is not None: - self.anchor_widget.after_cancel(self.checkhide_after_id) - self.checkhide_after_id = \ - self.anchor_widget.after(CHECKHIDE_TIME, self.checkhide_event) - return None - - def hide_event(self, event): - """Handle HIDE_EVENT by calling hidetip.""" - if not self.tipwindow: - # See the explanation in checkhide_event. - return None - self.hidetip() - return "break" - - def hidetip(self): - """Hide the call-tip.""" - if not self.tipwindow: - return - - try: - self.label.destroy() - except TclError: - pass - self.label = None - - self.parenline = self.parencol = self.lastline = None - try: - self.anchor_widget.mark_unset(MARK_RIGHT) - except TclError: - pass - - try: - self._unbind_events() - except (TclError, ValueError): - # ValueError may be raised by MultiCall - pass - - super(CalltipWindow, self).hidetip() - - def _bind_events(self): - """Bind event handlers.""" - self.checkhideid = self.anchor_widget.bind(CHECKHIDE_EVENT, - self.checkhide_event) - for seq in CHECKHIDE_SEQUENCES: - self.anchor_widget.event_add(CHECKHIDE_EVENT, seq) - self.anchor_widget.after(CHECKHIDE_TIME, self.checkhide_event) - self.hideid = self.anchor_widget.bind(HIDE_EVENT, - self.hide_event) - for seq in HIDE_SEQUENCES: - self.anchor_widget.event_add(HIDE_EVENT, seq) - - def _unbind_events(self): - """Unbind event handlers.""" - for seq in CHECKHIDE_SEQUENCES: - self.anchor_widget.event_delete(CHECKHIDE_EVENT, seq) - self.anchor_widget.unbind(CHECKHIDE_EVENT, self.checkhideid) - self.checkhideid = None - for seq in HIDE_SEQUENCES: - self.anchor_widget.event_delete(HIDE_EVENT, seq) - self.anchor_widget.unbind(HIDE_EVENT, self.hideid) - self.hideid = None - - -def _calltip_window(parent): # htest # - from tkinter import Toplevel, Text, LEFT, BOTH - - top = Toplevel(parent) - top.title("Test call-tips") - x, y = map(int, parent.geometry().split('+')[1:]) - top.geometry("250x100+%d+%d" % (x + 175, y + 150)) - text = Text(top) - text.pack(side=LEFT, fill=BOTH, expand=1) - text.insert("insert", "string.split") - top.update() - - calltip = CalltipWindow(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__': - from unittest import main - main('idlelib.idle_test.test_calltip_w', verbosity=2, exit=False) - - from idlelib.idle_test.htest import run - run(_calltip_window) diff --git a/Lib/idlelib/codecontext.py b/Lib/idlelib/codecontext.py deleted file mode 100644 index 4ce9813..0000000 --- a/Lib/idlelib/codecontext.py +++ /dev/null @@ -1,261 +0,0 @@ -"""codecontext - display the block context above the edit window - -Once code has scrolled off the top of a window, it can be difficult to -determine which block you are in. This extension implements a pane at the top -of each IDLE edit window which provides block structure hints. These hints are -the lines which contain the block opening keywords, e.g. 'if', for the -enclosing block. The number of hint lines is determined by the maxlines -variable in the codecontext section of config-extensions.def. Lines which do -not open blocks are not shown in the context hints pane. - -""" -import re -from sys import maxsize as INFINITY - -import tkinter -from tkinter.constants import NSEW, SUNKEN - -from idlelib.config import idleConf - -BLOCKOPENERS = {"class", "def", "elif", "else", "except", "finally", "for", - "if", "try", "while", "with", "async"} - - -def get_spaces_firstword(codeline, c=re.compile(r"^(\s*)(\w*)")): - "Extract the beginning whitespace and first word from codeline." - return c.match(codeline).groups() - - -def get_line_info(codeline): - """Return tuple of (line indent value, codeline, block start keyword). - - The indentation of empty lines (or comment lines) is INFINITY. - If the line does not start a block, the keyword value is False. - """ - spaces, firstword = get_spaces_firstword(codeline) - indent = len(spaces) - if len(codeline) == indent or codeline[indent] == '#': - indent = INFINITY - opener = firstword in BLOCKOPENERS and firstword - return indent, codeline, opener - - -class CodeContext: - "Display block context above the edit window." - UPDATEINTERVAL = 100 # millisec - - def __init__(self, editwin): - """Initialize settings for context block. - - editwin is the Editor window for the context block. - self.text is the editor window text widget. - - self.context displays the code context text above the editor text. - Initially None, it is toggled via <<toggle-code-context>>. - self.topvisible is the number of the top text line displayed. - self.info is a list of (line number, indent level, line text, - block keyword) tuples for the block structure above topvisible. - self.info[0] is initialized with a 'dummy' line which - starts the toplevel 'block' of the module. - - self.t1 and self.t2 are two timer events on the editor text widget to - monitor for changes to the context text or editor font. - """ - self.editwin = editwin - self.text = editwin.text - self._reset() - - def _reset(self): - self.context = None - self.cell00 = None - self.t1 = None - self.topvisible = 1 - self.info = [(0, -1, "", False)] - - @classmethod - def reload(cls): - "Load class variables from config." - cls.context_depth = idleConf.GetOption("extensions", "CodeContext", - "maxlines", type="int", - default=15) - - def __del__(self): - "Cancel scheduled events." - if self.t1 is not None: - try: - self.text.after_cancel(self.t1) - except tkinter.TclError: - pass - self.t1 = None - - def toggle_code_context_event(self, event=None): - """Toggle code context display. - - If self.context doesn't exist, create it to match the size of the editor - window text (toggle on). If it does exist, destroy it (toggle off). - Return 'break' to complete the processing of the binding. - """ - if self.context is None: - # Calculate the border width and horizontal padding required to - # align the context with the text in the main Text widget. - # - # All values are passed through getint(), since some - # values may be pixel objects, which can't simply be added to ints. - widgets = self.editwin.text, self.editwin.text_frame - # Calculate the required horizontal padding and border width. - padx = 0 - border = 0 - for widget in widgets: - info = (widget.grid_info() - if widget is self.editwin.text - else widget.pack_info()) - padx += widget.tk.getint(info['padx']) - padx += widget.tk.getint(widget.cget('padx')) - border += widget.tk.getint(widget.cget('border')) - self.context = tkinter.Text( - self.editwin.text_frame, - height=1, - width=1, # Don't request more than we get. - highlightthickness=0, - padx=padx, border=border, relief=SUNKEN, state='disabled') - self.update_font() - self.update_highlight_colors() - self.context.bind('<ButtonRelease-1>', self.jumptoline) - # Get the current context and initiate the recurring update event. - self.timer_event() - # Grid the context widget above the text widget. - self.context.grid(row=0, column=1, sticky=NSEW) - - line_number_colors = idleConf.GetHighlight(idleConf.CurrentTheme(), - 'linenumber') - self.cell00 = tkinter.Frame(self.editwin.text_frame, - bg=line_number_colors['background']) - self.cell00.grid(row=0, column=0, sticky=NSEW) - menu_status = 'Hide' - else: - self.context.destroy() - self.context = None - self.cell00.destroy() - self.cell00 = None - self.text.after_cancel(self.t1) - self._reset() - menu_status = 'Show' - self.editwin.update_menu_label(menu='options', index='* Code Context', - label=f'{menu_status} Code Context') - return "break" - - def get_context(self, new_topvisible, stopline=1, stopindent=0): - """Return a list of block line tuples and the 'last' indent. - - The tuple fields are (linenum, indent, text, opener). - The list represents header lines from new_topvisible back to - stopline with successively shorter indents > stopindent. - The list is returned ordered by line number. - Last indent returned is the smallest indent observed. - """ - assert stopline > 0 - lines = [] - # The indentation level we are currently in. - lastindent = INFINITY - # For a line to be interesting, it must begin with a block opening - # keyword, and have less indentation than lastindent. - for linenum in range(new_topvisible, stopline-1, -1): - codeline = self.text.get(f'{linenum}.0', f'{linenum}.end') - indent, text, opener = get_line_info(codeline) - if indent < lastindent: - lastindent = indent - if opener in ("else", "elif"): - # Also show the if statement. - lastindent += 1 - if opener and linenum < new_topvisible and indent >= stopindent: - lines.append((linenum, indent, text, opener)) - if lastindent <= stopindent: - break - lines.reverse() - return lines, lastindent - - def update_code_context(self): - """Update context information and lines visible in the context pane. - - No update is done if the text hasn't been scrolled. If the text - was scrolled, the lines that should be shown in the context will - be retrieved and the context area will be updated with the code, - up to the number of maxlines. - """ - new_topvisible = self.editwin.getlineno("@0,0") - if self.topvisible == new_topvisible: # Haven't scrolled. - return - if self.topvisible < new_topvisible: # Scroll down. - lines, lastindent = self.get_context(new_topvisible, - self.topvisible) - # Retain only context info applicable to the region - # between topvisible and new_topvisible. - while self.info[-1][1] >= lastindent: - del self.info[-1] - else: # self.topvisible > new_topvisible: # Scroll up. - stopindent = self.info[-1][1] + 1 - # Retain only context info associated - # with lines above new_topvisible. - while self.info[-1][0] >= new_topvisible: - stopindent = self.info[-1][1] - del self.info[-1] - lines, lastindent = self.get_context(new_topvisible, - self.info[-1][0]+1, - stopindent) - self.info.extend(lines) - self.topvisible = new_topvisible - # Last context_depth context lines. - context_strings = [x[2] for x in self.info[-self.context_depth:]] - showfirst = 0 if context_strings[0] else 1 - # Update widget. - self.context['height'] = len(context_strings) - showfirst - self.context['state'] = 'normal' - self.context.delete('1.0', 'end') - self.context.insert('end', '\n'.join(context_strings[showfirst:])) - self.context['state'] = 'disabled' - - def jumptoline(self, event=None): - "Show clicked context line at top of editor." - lines = len(self.info) - if lines == 1: # No context lines are showing. - newtop = 1 - else: - # Line number clicked. - contextline = int(float(self.context.index('insert'))) - # Lines not displayed due to maxlines. - offset = max(1, lines - self.context_depth) - 1 - newtop = self.info[offset + contextline][0] - self.text.yview(f'{newtop}.0') - self.update_code_context() - - def timer_event(self): - "Event on editor text widget triggered every UPDATEINTERVAL ms." - if self.context is not None: - self.update_code_context() - self.t1 = self.text.after(self.UPDATEINTERVAL, self.timer_event) - - def update_font(self): - if self.context is not None: - font = idleConf.GetFont(self.text, 'main', 'EditorWindow') - self.context['font'] = font - - def update_highlight_colors(self): - if self.context is not None: - colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'context') - self.context['background'] = colors['background'] - self.context['foreground'] = colors['foreground'] - - if self.cell00 is not None: - line_number_colors = idleConf.GetHighlight(idleConf.CurrentTheme(), - 'linenumber') - self.cell00.config(bg=line_number_colors['background']) - - -CodeContext.reload() - - -if __name__ == "__main__": - from unittest import main - main('idlelib.idle_test.test_codecontext', verbosity=2, exit=False) - - # Add htest. diff --git a/Lib/idlelib/config-extensions.def b/Lib/idlelib/config-extensions.def index 7e23fb0..a24b8c9 100644 --- a/Lib/idlelib/config-extensions.def +++ b/Lib/idlelib/config-extensions.def @@ -1,22 +1,5 @@ # config-extensions.def # -# The following sections are for features that are no longer extensions. -# Their options values are left here for back-compatibility. - -[AutoComplete] -popupwait= 2000 - -[CodeContext] -maxlines= 15 - -[FormatParagraph] -max-width= 72 - -[ParenMatch] -style= expression -flash-delay= 500 -bell= True - # IDLE reads several config files to determine user preferences. This # file is the default configuration file for IDLE extensions settings. # @@ -36,7 +19,7 @@ bell= True # extension that may be sensibly re-configured. # # If there are no keybindings for a menus' virtual events, include lines -# like <<toggle-code-context>>=. +# 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 @@ -49,14 +32,68 @@ bell= True # See config-keys.def for notes on specifying keys and extend.txt for # information on creating IDLE extensions. -# A fake extension for testing and example purposes. When enabled and -# invoked, inserts or deletes z-text at beginning of every line. -[ZzDummy] -enable= False -enable_shell = False -enable_editor = True -z-text= Z -[ZzDummy_cfgBindings] -z-in= <Control-Shift-KeyRelease-Insert> -[ZzDummy_bindings] -z-out= <Control-Shift-KeyRelease-Delete> +[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=True +[AutoExpand_cfgBindings] +expand-word=<Alt-Key-slash> + +[CallTips] +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=True +style= expression +flash-delay= 500 +bell=True +[ParenMatch_cfgBindings] +flash-paren=<Control-Key-0> +[ParenMatch_bindings] +paren-closed=<KeyRelease-parenright> <KeyRelease-bracketright> <KeyRelease-braceright> + +[RstripExtension] +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 a7b0433..4146e28 100644 --- a/Lib/idlelib/config-highlight.def +++ b/Lib/idlelib/config-highlight.def @@ -22,10 +22,6 @@ hit-foreground= #ffffff hit-background= #000000 error-foreground= #000000 error-background= #ff7777 -context-foreground= #000000 -context-background= lightgray -linenumber-foreground= gray -linenumber-background= #ffffff #cursor (only foreground can be set, restart IDLE) cursor-foreground= black #shell window @@ -57,10 +53,6 @@ hit-foreground= #ffffff hit-background= #000000 error-foreground= #000000 error-background= #ff7777 -context-foreground= #000000 -context-background= lightgray -linenumber-foreground= gray -linenumber-background= #ffffff #cursor (only foreground can be set, restart IDLE) cursor-foreground= black #shell window @@ -99,7 +91,3 @@ stdout-background = #002240 hit-foreground = #002240 comment-background = #002240 break-foreground = #FFFFFF -context-foreground= #ffffff -context-background= #454545 -linenumber-foreground= gray -linenumber-background= #002240 diff --git a/Lib/idlelib/config-keys.def b/Lib/idlelib/config-keys.def index f71269b..3bfcb69 100644 --- a/Lib/idlelib/config-keys.def +++ b/Lib/idlelib/config-keys.def @@ -57,15 +57,6 @@ 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> -force-open-completions= <Control-Key-space> -expand-word= <Alt-Key-slash> -force-open-calltip= <Control-Key-backslash> -format-paragraph= <Alt-Key-q> -flash-paren= <Control-Key-0> -run-module= <Key-F5> -run-custom= <Shift-Key-F5> -check-module= <Alt-Key-x> -zoom-height= <Alt-Key-2> [IDLE Classic Unix] copy=<Alt-Key-w> <Meta-Key-w> @@ -117,75 +108,6 @@ toggle-tabs=<Alt-Key-t> change-indentwidth=<Alt-Key-u> del-word-left=<Alt-Key-BackSpace> del-word-right=<Alt-Key-d> -force-open-completions= <Control-Key-space> -expand-word= <Alt-Key-slash> -force-open-calltip= <Control-Key-backslash> -format-paragraph= <Alt-Key-q> -flash-paren= <Control-Key-0> -run-module= <Key-F5> -run-custom= <Shift-Key-F5> -check-module= <Alt-Key-x> -zoom-height= <Alt-Key-2> - -[IDLE Modern Unix] -copy = <Control-Shift-Key-C> <Control-Key-Insert> -cut = <Control-Key-x> <Shift-Key-Delete> -paste = <Control-Key-v> <Shift-Key-Insert> -beginning-of-line = <Key-Home> -center-insert = <Control-Key-l> -close-all-windows = <Control-Key-q> -close-window = <Control-Key-w> <Control-Shift-Key-W> -do-nothing = <Control-Key-F12> -end-of-file = <Control-Key-d> -history-next = <Alt-Key-n> <Meta-Key-n> -history-previous = <Alt-Key-p> <Meta-Key-p> -interrupt-execution = <Control-Key-c> -view-restart = <Key-F6> -restart-shell = <Control-Key-F6> -open-class-browser = <Control-Key-b> -open-module = <Control-Key-m> -open-new-window = <Control-Key-n> -open-window-from-file = <Control-Key-o> -plain-newline-and-indent = <Control-Key-j> -print-window = <Control-Key-p> -python-context-help = <Shift-Key-F1> -python-docs = <Key-F1> -redo = <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> -toggle-auto-coloring = <Control-Key-slash> -undo = <Control-Key-z> -find = <Control-Key-f> -find-again = <Key-F3> -find-in-files = <Control-Shift-Key-f> -find-selection = <Control-Key-h> -replace = <Control-Key-r> -goto-line = <Control-Key-g> -smart-backspace = <Key-BackSpace> -newline-and-indent = <Key-Return> <Key-KP_Enter> -smart-indent = <Key-Tab> -indent-region = <Control-Key-bracketright> -dedent-region = <Control-Key-bracketleft> -comment-region = <Control-Key-d> -uncomment-region = <Control-Shift-Key-D> -tabify-region = <Alt-Key-5> -untabify-region = <Alt-Key-6> -toggle-tabs = <Control-Key-T> -change-indentwidth = <Alt-Key-u> -del-word-left = <Control-Key-BackSpace> -del-word-right = <Control-Key-Delete> -force-open-completions= <Control-Key-space> -expand-word= <Alt-Key-slash> -force-open-calltip= <Control-Key-backslash> -format-paragraph= <Alt-Key-q> -flash-paren= <Control-Key-0> -run-module= <Key-F5> -run-custom= <Shift-Key-F5> -check-module= <Alt-Key-x> -zoom-height= <Alt-Key-2> [IDLE Classic Mac] copy=<Command-Key-c> @@ -237,15 +159,6 @@ toggle-tabs=<Control-Key-t> change-indentwidth=<Control-Key-u> del-word-left=<Control-Key-BackSpace> del-word-right=<Control-Key-Delete> -force-open-completions= <Control-Key-space> -expand-word= <Option-Key-slash> -force-open-calltip= <Control-Key-backslash> -format-paragraph= <Option-Key-q> -flash-paren= <Control-Key-0> -run-module= <Key-F5> -run-custom= <Shift-Key-F5> -check-module= <Option-Key-x> -zoom-height= <Option-Key-0> [IDLE Classic OSX] toggle-tabs = <Control-Key-t> @@ -298,12 +211,4 @@ python-context-help = <Shift-Key-F1> save-copy-of-window-as-file = <Option-Command-Key-s> open-window-from-file = <Command-Key-o> python-docs = <Key-F1> -force-open-completions= <Control-Key-space> -expand-word= <Option-Key-slash> -force-open-calltip= <Control-Key-backslash> -format-paragraph= <Option-Key-q> -flash-paren= <Control-Key-0> -run-module= <Key-F5> -run-custom= <Shift-Key-F5> -check-module= <Option-Key-x> -zoom-height= <Option-Key-0> + diff --git a/Lib/idlelib/config-main.def b/Lib/idlelib/config-main.def index 28ae941..f241199 100644 --- a/Lib/idlelib/config-main.def +++ b/Lib/idlelib/config-main.def @@ -4,72 +4,59 @@ # When IDLE starts, it will look in # the following two sets of files, in order: # -# default configuration files in idlelib -# -------------------------------------- -# config-main.def default general config file -# config-extensions.def default extension config file -# config-highlight.def default highlighting config file -# config-keys.def default keybinding config file +# default configuration +# --------------------- +# config-main.def the default general config file +# config-extensions.def the default extension config file +# config-highlight.def the default highlighting config file +# config-keys.def the default keybinding config file # -# user configuration files in ~/.idlerc -# ------------------------------------- -# config-main.cfg user general config file -# config-extensions.cfg user extension config file -# config-highlight.cfg user highlighting config file -# config-keys.cfg user keybinding config file +# user configuration +# ------------------- +# ~/.idlerc/config-main.cfg the user general config file +# ~/.idlerc/config-extensions.cfg the user extension config file +# ~/.idlerc/config-highlight.cfg the user highlighting config file +# ~/.idlerc/config-keys.cfg the user keybinding config file # -# On Windows, the default location of the home directory ('~' above) -# depends on the version. For Windows 10, it is C:\Users\<username>. +# On Windows2000 and Windows XP the .idlerc directory is at +# Documents and Settings\<username>\.idlerc # -# Any options the user saves through the config dialog will be saved to -# the relevant user config file. Reverting any general or extension -# setting to the default causes that entry to be wiped from the user -# file and re-read from the default file. This rule applies to each -# item, except that the three editor font items are saved as a group. -# -# User highlighting themes and keybinding sets must have (section) names -# distinct from the default names. All items are added and saved as a -# group. They are retained unless specifically deleted within the config -# dialog. Choosing one of the default themes or keysets just applies the -# relevant settings from the default file. +# On Windows98 it is at c:\.idlerc # -# Additional help sources are listed in the [HelpFiles] section below -# and should be viewable by a web browser (or the Windows Help viewer in -# the case of .chm files). These sources will be listed on the Help -# menu. The pattern, and two examples, are: +# Any options the user saves through the config dialog will be saved to +# the relevant user config file. Reverting any general setting to the +# default causes that entry to be wiped from the user file and re-read +# from the default file. User highlighting themes or keybinding sets are +# retained unless specifically deleted within the config dialog. Choosing +# one of the default themes or keysets just applies the relevant settings +# from the default file. # +# Additional help sources are listed in the [HelpFiles] section and must be +# viewable by a web browser (or the Windows Help viewer in the case of .chm +# files). These sources will be listed on the Help menu. The pattern is # <sequence_number = menu item;/path/to/help/source> -# 1 = IDLE;C:/Programs/Python36/Lib/idlelib/help.html -# 2 = Pillow;https://pillow.readthedocs.io/en/latest/ +# You can't use a semi-colon in a menu item or path. The path will be platform +# specific because of path separators, drive specs etc. # -# You can't use a semi-colon in a menu item or path. The path will be -# platform specific because of path separators, drive specs etc. -# -# The default files should not be edited except to add new sections to -# config-extensions.def for added extensions. The user files should be -# modified through the Settings dialog. +# It is best to use the Configuration GUI to set up additional help sources! +# Example: +#1 = My Extra Help Source;/usr/share/doc/foo/index.html +#2 = Another Help Source;/path/to/another.pdf [General] editor-on-startup= 0 autosave= 0 -print-command-posix=lpr %%s -print-command-win=start /min notepad /p %%s +print-command-posix=lpr %s +print-command-win=start /min notepad /p %s delete-exitfunc= 1 [EditorWindow] width= 80 height= 40 -cursor-blink= 1 font= TkFixedFont -# For TkFixedFont, the actual size and boldness are obtained from tk -# and override 10 and 0. See idlelib.config.IdleConf.GetFont font-size= 10 font-bold= 0 encoding= none -line-numbers-default= 0 - -[PyShell] -auto-squeeze-min-lines= 50 [Indent] use-spaces= 1 @@ -83,9 +70,7 @@ name2= [Keys] default= 1 -name= -name2= -# name2 set in user config-main.cfg for keys added after 2016 July 1 +name= IDLE Classic Windows [History] cyclic=1 diff --git a/Lib/idlelib/configDialog.py b/Lib/idlelib/configDialog.py new file mode 100644 index 0000000..3c29af1 --- /dev/null +++ b/Lib/idlelib/configDialog.py @@ -0,0 +1,1457 @@ +"""IDLE Configuration Dialog: support user customization of IDLE by GUI + +Customize font faces, sizes, and colorization attributes. Set indentation +defaults. Customize keybindings. Colorization and keybindings can be +saved as user defined sets. Select startup options including shell/editor +and default window size. Define additional help sources. + +Note that tab width in IDLE is currently fixed at eight due to Tk issues. +Refer to comments in EditorWindow autoindent code for details. + +""" +from Tkinter import * +import tkMessageBox, tkColorChooser, tkFont + +from idlelib.configHandler import idleConf +from idlelib.dynOptionMenuWidget import DynOptionMenu +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='', _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(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'), + '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.transient(parent) + self.grab_set() + self.protocol("WM_DELETE_WINDOW", self.Cancel) + 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.LoadConfigs() + self.AttachVarCallbacks() #avoid callbacks during LoadConfigs + + if not _utest: + self.wm_deiconify() + self.wait_window() + + def CreateWidgets(self): + self.tabPages = TabbedPageSet(self, + 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.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): + 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 + #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 + 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, 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) + + #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 + 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) + return frame + + def CreatePageHighlight(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 + #body section frames + 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) + 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):\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', 'builtin'), (')\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]) + for element in self.themeElements: + 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.fgHilite.set(1) + 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', + 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 + 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) + self.new_custom_theme.pack(side=TOP, fill=X, pady=5) + return frame + + def CreatePageKeys(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 + #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 + 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) + #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', + command=self.DeleteCustomKeys) + 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 + 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) + #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) + 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): + 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 + #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) + frameEncoding = 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') + #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') + #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) + #frameEncoding + labelEncodingTitle = Label( + frameEncoding, text="Default Source Encoding") + radioEncLocale = Radiobutton( + frameEncoding, variable=self.encoding, + value="locale", text="Locale-defined") + radioEncUTF8 = Radiobutton( + frameEncoding, variable=self.encoding, + value="utf-8", text="UTF-8") + radioEncNone = Radiobutton( + frameEncoding, variable=self.encoding, + value="none", text="None") + #frameHelp + 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) + + #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) + frameEncoding.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) + #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) + #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) + #frameEncoding + labelEncodingTitle.pack(side=LEFT, anchor=W, padx=5, pady=5) + radioEncNone.pack(side=RIGHT, anchor=E, pady=5) + radioEncUTF8.pack(side=RIGHT, anchor=E, pady=5) + radioEncLocale.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) + return frame + + def AttachVarCallbacks(self): + 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 remove_var_callbacks(self): + for var in ( + self.fontSize, self.fontName, self.fontBold, + self.spaceNum, self.colour, self.builtinTheme, + self.customTheme, self.themeIsBuiltin, self.highlightTarget, + self.keyBinding, self.builtinKeys, self.customKeys, + self.keysAreBuiltin, self.winWidth, self.winHeight, + self.startupEdit, self.autoSave, self.encoding,): + var.trace_vdelete('w', var.trace_vinfo()[0][1]) + + 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() + 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() + if value != '- no custom themes -': + self.AddChangedItem('main', 'Theme', 'name', value) + self.PaintThemeSample() + + 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): + self.SetHighlightTarget() + + 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) + else: #this is an extension key binding + 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) + self.LoadKeysList(value) + + def VarChanged_customKeys(self, *params): + value = self.customKeys.get() + if value != '- no custom keys -': + self.AddChangedItem('main', 'Keys', 'name', value) + self.LoadKeysList(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_winHeight(self, *params): + value = self.winHeight.get() + self.AddChangedItem('main', 'EditorWindow', 'height', 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_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 + #should be made in the relevant section (config type) of this + #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':{}} + + 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':{}} + for configType in dItems: + sections = idleConf.GetSectionList('default', configType) + for section in sections: + dItems[configType][section] = {} + options = idleConf.defaultCfg[configType].GetOptionList(section) + for option in options: + dItems[configType][section][option] = ( + idleConf.defaultCfg[configType].Get(section, option)) + return dItems + + def SetThemeType(self): + if self.themeIsBuiltin.get(): + self.optMenuThemeBuiltin.config(state=NORMAL) + self.optMenuThemeCustom.config(state=DISABLED) + self.buttonDeleteCustomTheme.config(state=DISABLED) + else: + self.optMenuThemeBuiltin.config(state=DISABLED) + self.radioThemeCustom.config(state=NORMAL) + self.optMenuThemeCustom.config(state=NORMAL) + self.buttonDeleteCustomTheme.config(state=NORMAL) + + def SetKeysType(self): + if self.keysAreBuiltin.get(): + self.optMenuKeysBuiltin.config(state=NORMAL) + self.optMenuKeysCustom.config(state=DISABLED) + self.buttonDeleteCustomKeys.config(state=DISABLED) + else: + self.optMenuKeysBuiltin.config(state=DISABLED) + self.radioKeysCustom.config(state=NORMAL) + self.optMenuKeysCustom.config(state=NORMAL) + 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 + if self.keysAreBuiltin.get(): + currentKeySetName = self.builtinKeys.get() + else: + currentKeySetName = self.customKeys.get() + currentBindings = idleConf.GetCurrentKeySet() + if currentKeySetName in self.changedItems['keys']: #unsaved changes + keySetChanges = self.changedItems['keys'][currentKeySetName] + for event in keySetChanges: + currentBindings[event] = keySetChanges[event].split() + currentKeySequences = currentBindings.values() + 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) + if not newKeySet: #user cancelled custom key set creation + self.listBindings.select_set(listIndex) + self.listBindings.select_anchor(listIndex) + return + 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.select_set(listIndex) + self.listBindings.select_anchor(listIndex) + self.keyBinding.set(newKeys) + else: + 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 + return newKeySet + + def SaveAsNewKeySet(self): + newKeysName = self.GetNewKeysName('New Key Set Name:') + if newKeysName: + self.CreateNewKeySet(newKeysName) + + def KeyBindingSelected(self, event): + self.buttonNewKeys.config(state=NORMAL) + + 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() + else: + 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 + #handle any unsaved changes to prev key set + if prevKeySetName in self.changedItems['keys']: + keySetChanges = self.changedItems['keys'][prevKeySetName] + for event in keySetChanges: + newKeys[event] = keySetChanges[event] + #save the new theme + self.SaveNewKeySet(newKeySetName, newKeys) + #change gui over to the new key set + customKeyList = idleConf.GetSectionList('user', 'keys') + customKeyList.sort() + self.optMenuKeysCustom.SetMenu(customKeyList, newKeySetName) + self.keysAreBuiltin.set(0) + self.SetKeysType() + + def LoadKeysList(self, keySetName): + reselect = 0 + newKeySet = 0 + if self.listBindings.curselection(): + reselect = 1 + listIndex = self.listBindings.index(ANCHOR) + keySet = idleConf.GetKeySet(keySetName) + bindNames = keySet.keys() + bindNames.sort() + 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 + 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] + self.listBindings.insert(END, bindName+' - '+key) + if reselect: + self.listBindings.see(listIndex) + self.listBindings.select_set(listIndex) + self.listBindings.select_anchor(listIndex) + + def DeleteCustomKeys(self): + keySetName=self.customKeys.get() + delmsg = 'Are you sure you wish to delete the key set %r ?' + if not tkMessageBox.askyesno( + 'Delete Key Set', delmsg % keySetName, parent=self): + return + self.DeactivateCurrentConfig() + #remove key set from config + idleConf.userCfg['keys'].remove_section(keySetName) + if keySetName in self.changedItems['keys']: + del(self.changedItems['keys'][keySetName]) + #write changes + idleConf.userCfg['keys'].Save() + #reload user key set list + itemList = idleConf.GetSectionList('user', 'keys') + itemList.sort() + if not itemList: + self.radioKeysCustom.config(state=DISABLED) + self.optMenuKeysCustom.SetMenu(itemList, '- no custom keys -') + else: + 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')) + #user can't back out of these changes, they must be applied now + self.SaveAllChangedConfigs() + self.ActivateConfigChanges() + self.SetKeysType() + + def DeleteCustomTheme(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 + self.DeactivateCurrentConfig() + #remove theme from config + idleConf.userCfg['highlight'].remove_section(themeName) + if themeName in self.changedItems['highlight']: + del(self.changedItems['highlight'][themeName]) + #write changes + idleConf.userCfg['highlight'].Save() + #reload user theme list + itemList = idleConf.GetSectionList('user', 'highlight') + itemList.sort() + if not itemList: + self.radioThemeCustom.config(state=DISABLED) + self.optMenuThemeCustom.SetMenu(itemList, '- no custom themes -') + else: + 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')) + #user can't back out of these changes, they must be applied now + self.SaveAllChangedConfigs() + self.ActivateConfigChanges() + 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): + #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 + return + else: #create new custom theme based on previously active theme + self.CreateNewTheme(newTheme) + self.colour.set(colourString) + else: #current theme is user defined + self.colour.set(colourString) + + def OnNewColourSet(self): + newColour=self.colour.get() + 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 + return newTheme + + def SaveAsNewTheme(self): + newThemeName = self.GetNewThemeName('New Theme Name:') + if newThemeName: + self.CreateNewTheme(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() + else: + 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] + for element in themeChanges: + newTheme[element] = themeChanges[element] + #save the new theme + self.SaveNewTheme(newThemeName, newTheme) + #change gui over to the new theme + customThemeList = idleConf.GetSectionList('user', 'highlight') + customThemeList.sort() + self.optMenuThemeCustom.SetMenu(customThemeList, newThemeName) + self.themeIsBuiltin.set(0) + self.SetThemeType() + + 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() + 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 + self.radioFg.config(state=DISABLED) + self.radioBg.config(state=DISABLED) + self.fgHilite.set(1) + 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): + self.SetColourSample() + + def SetColourSample(self): + #set the colour smaple area + 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() + 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') + #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'] + self.textHighlightSample.tag_config(element, **colours) + self.SetColourSample() + + def HelpSourceSelected(self, event): + self.SetHelpListButtonStates() + + def SetHelpListButtonStates(self): + 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 + self.buttonHelpListEdit.config(state=NORMAL) + self.buttonHelpListRemove.config(state=NORMAL) + else: #there currently is not a selection + self.buttonHelpListEdit.config(state=DISABLED) + self.buttonHelpListRemove.config(state=DISABLED) + + def HelpListItemAdd(self): + helpSource = GetHelpSourceDialog(self, 'New Help Source').result + if helpSource: + 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): + return #no changes + self.userHelpList[itemIndex] = newHelpSource + self.listHelp.delete(itemIndex) + self.listHelp.insert(itemIndex, newHelpSource[0]) + self.UpdateUserHelpChangedItems() + self.SetHelpListButtonStates() + + def HelpListItemRemove(self): + itemIndex = self.listHelp.index(ANCHOR) + del(self.userHelpList[itemIndex]) + self.listHelp.delete(itemIndex) + self.UpdateUserHelpChangedItems() + self.SetHelpListButtonStates() + + 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), + ';'.join(self.userHelpList[num-1][:2])) + + def LoadFontCfg(self): + ##base editor font selection list + fonts = list(tkFont.families(self)) + fonts.sort() + for font in fonts: + 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] + 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 + self.optMenuFontSize.SetMenu(('7', '8', '9', '10', '11', '12', '13', + '14', '16', '18', '20', '22', + '25', '29', '34', '40'), fontSize ) + ##fontWeight + self.fontBold.set(fontBold) + ##font sample + self.SetFontSample() + + def LoadTabCfg(self): + ##indent sizes + 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)) + ##currently set theme + currentOption = idleConf.CurrentTheme() + ##load available theme option menus + if self.themeIsBuiltin.get(): #default theme selected + itemList = idleConf.GetSectionList('default', 'highlight') + itemList.sort() + 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]) + else: #user theme selected + itemList = idleConf.GetSectionList('user', 'highlight') + itemList.sort() + self.optMenuThemeCustom.SetMenu(itemList, currentOption) + itemList = idleConf.GetSectionList('default', 'highlight') + itemList.sort() + self.optMenuThemeBuiltin.SetMenu(itemList, itemList[0]) + self.SetThemeType() + ##load theme element option menu + themeNames = self.themeElements.keys() + themeNames.sort(key=lambda x: self.themeElements[x][1]) + 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)) + ##currently set keys + currentOption = idleConf.CurrentKeys() + ##load available keyset option menus + if self.keysAreBuiltin.get(): #default theme selected + itemList = idleConf.GetSectionList('default', 'keys') + itemList.sort() + 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]) + else: #user key set selected + itemList = idleConf.GetSectionList('user', 'keys') + itemList.sort() + self.optMenuKeysCustom.SetMenu(itemList, currentOption) + itemList = idleConf.GetSectionList('default', 'keys') + itemList.sort() + self.optMenuKeysBuiltin.SetMenu(itemList, itemList[0]) + self.SetKeysType() + ##load keyset element list + 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')) + #autosave state + 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')) + # default source encoding + 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.SetHelpListButtonStates() + + def LoadConfigs(self): + """ + load configuration from default and user config files and populate + the widgets on the config dialog pages. + """ + ### fonts / tabs page + self.LoadFontCfg() + self.LoadTabCfg() + ### highlighting page + self.LoadThemeCfg() + ### keys page + self.LoadKeyCfg() + ### general page + self.LoadGeneralCfg() + # note: extension page handled separately + + def SaveNewKeySet(self, keySetName, keySet): + """ + save a newly created core key set. + keySetName - string, the name of the new key set + keySet - dictionary containing the new key set + """ + 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) + + def SaveNewTheme(self, themeName, theme): + """ + save a newly created theme. + themeName - string, the name of the new theme + theme - dictionary containing the new theme + """ + 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) + + 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) + #if we got here set the option + return idleConf.userCfg[configType].SetOption(section, item, value) + + def SaveAllChangedConfigs(self): + "Save configuration changes to the user config file." + idleConf.userCfg['main'].Save() + for configType in self.changedItems: + cfgTypeHasChanges = False + for section in self.changedItems[configType]: + if section == 'HelpFiles': + #this section gets completely replaced + idleConf.userCfg['main'].remove_section('HelpFiles') + cfgTypeHasChanges = True + for item in self.changedItems[configType][section]: + value = self.changedItems[configType][section][item] + if self.SetUserValue(configType, section, item, value): + cfgTypeHasChanges = True + if cfgTypeHasChanges: + idleConf.userCfg[configType].Save() + for configType in ['keys', 'highlight']: + # 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 + #config must be done - remove the previous keybindings + winInstances = self.parent.instance_dict + for instance in winInstances: + instance.RemoveKeybindings() + + def ActivateConfigChanges(self): + "Dynamically apply configuration changes" + winInstances = self.parent.instance_dict.keys() + for instance in winInstances: + instance.ResetColorizer() + instance.ResetFont() + instance.set_notabs_indentwidth() + instance.ApplyKeybindings() + instance.reset_help_menu_entries() + + def Cancel(self): + self.grab_release() + self.destroy() + + def Ok(self): + self.Apply() + self.grab_release() + self.destroy() + + def Apply(self): + self.DeactivateCurrentConfig() + self.SaveAllChangedConfigs() + self.ActivateConfigChanges() + + def Help(self): + 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 a 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__': + 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/config.py b/Lib/idlelib/configHandler.py index 04444a3..ca885ed 100644 --- a/Lib/idlelib/config.py +++ b/Lib/idlelib/configHandler.py @@ -1,20 +1,13 @@ -"""idlelib.config -- Manage IDLE configuration information. - -The comments at the beginning of config-main.def describe the -configuration files and the design implemented to update user -configuration information. In particular, user configuration choices -which duplicate the defaults will be removed from the user's -configuration files, and if a user file becomes empty, it will be -deleted. - -The configuration database maps options to values. Conceptually, the -database keys are tuples (config-type, section, item). As implemented, -there are separate dicts for default and user values. Each has -config-type keys 'main', 'extensions', 'highlight', and 'keys'. The -value for each key is a ConfigParser instance that maps section and item -to values. For 'main' and 'extensions', user values override -default values. For 'highlight' and 'keys', user sections augment the -default sections (and must, therefore, have distinct names). +"""Provides access to stored IDLE configuration information. + +Refer to the comments at the beginning of config-main.def for a description of +the available configuration files and the design implemented to update user +configuration information. In particular, user configuration choices which +duplicate the defaults will be removed from the user's configuration files, +and if a file becomes empty, it will be deleted. + +The contents of the user files may be altered using the Options/Configure IDLE +menu to access the configuration GUI (configDialog.py), or manually. Throughout this module there is an emphasis on returning useable defaults when a problem occurs in returning a requested configuration value back to @@ -25,15 +18,17 @@ configuration problem notification and resolution. """ # TODOs added Oct 2014, tjr -from configparser import ConfigParser +from __future__ import print_function import os import sys -from tkinter.font import Font -import idlelib +from ConfigParser import ConfigParser +from Tkinter import TkVersion +from tkFont import Font, nametofont class InvalidConfigType(Exception): pass class InvalidConfigSet(Exception): pass +class InvalidFgBg(Exception): pass class InvalidTheme(Exception): pass class IdleConfParser(ConfigParser): @@ -44,8 +39,8 @@ class IdleConfParser(ConfigParser): """ cfgFile - string, fully specified configuration file name """ - self.file = cfgFile # This is currently '' when testing. - ConfigParser.__init__(self, defaults=cfgDefaults, strict=False) + self.file = cfgFile + ConfigParser.__init__(self, defaults=cfgDefaults) def Get(self, section, option, type=None, default=None, raw=False): """ @@ -73,14 +68,38 @@ class IdleConfParser(ConfigParser): def Load(self): "Load the configuration file from disk." - if self.file: - self.read(self.file) + self.read(self.file) class IdleUserConfParser(IdleConfParser): """ IdleConfigParser specialised for user configuration handling. """ + 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." + for section in self.sections(): + if not self.GetOptionList(section): + self.remove_section(section) + + def IsEmpty(self): + "Return True if no sections after removing empty sections." + self.RemoveEmptySections() + return not self.sections() + + 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 False + def SetOption(self, section, option, value): """Return True if option is added or changed to value, else False. @@ -98,49 +117,29 @@ class IdleUserConfParser(IdleConfParser): self.set(section, option, value) return True - 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 False - - 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." - for section in self.sections(): - if not self.GetOptionList(section): - self.remove_section(section) - - def IsEmpty(self): - "Return True if no sections after removing empty sections." - self.RemoveEmptySections() - return not self.sections() + def RemoveFile(self): + "Remove user config file self.file from disk if it exists." + if os.path.exists(self.file): + os.remove(self.file) def Save(self): """Update user configuration file. - If self not empty after removing empty sections, write the file - to disk. Otherwise, remove the file from disk if it exists. + Remove empty sections. If resulting config isn't empty, write the file + to disk. If config is empty, remove the file from disk if it exists. + """ - fname = self.file - if fname and fname[0] != '#': - if not self.IsEmpty(): - try: - cfgFile = open(fname, 'w') - except OSError: - os.unlink(fname) - cfgFile = open(fname, 'w') - with cfgFile: - self.write(cfgFile) - elif os.path.exists(self.file): - os.remove(self.file) + if not self.IsEmpty(): + fname = self.file + try: + cfgFile = open(fname, 'w') + except IOError: + os.unlink(fname) + cfgFile = open(fname, 'w') + with cfgFile: + self.write(cfgFile) + else: + self.RemoveFile() class IdleConf: """Hold config parsers for all idle config files in singleton instance. @@ -153,27 +152,35 @@ class IdleConf: for config_type in self.config_types: (user home dir)/.idlerc/config-{config-type}.cfg """ - def __init__(self, _utest=False): - self.config_types = ('main', 'highlight', 'keys', 'extensions') + def __init__(self): + self.config_types = ('main', 'extensions', 'highlight', 'keys') self.defaultCfg = {} self.userCfg = {} self.cfg = {} # TODO use to select userCfg vs defaultCfg - # self.blink_off_time = <first editor text>['insertofftime'] - # See https:/bugs.python.org/issue4630, msg356516. + self.CreateConfigHandlers() + self.LoadCfgFiles() - if not _utest: - self.CreateConfigHandlers() - self.LoadCfgFiles() def CreateConfigHandlers(self): "Populate default and user config parser dictionaries." - idledir = os.path.dirname(__file__) - self.userdir = userdir = '' if idlelib.testing else self.GetUserCfgDir() - for cfg_type in self.config_types: - self.defaultCfg[cfg_type] = IdleConfParser( - os.path.join(idledir, f'config-{cfg_type}.def')) - self.userCfg[cfg_type] = IdleUserConfParser( - os.path.join(userdir or '#', f'config-{cfg_type}.cfg')) + #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() + + 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): """Return a filesystem directory for storing user config files. @@ -184,13 +191,12 @@ class IdleConf: userDir = os.path.expanduser('~') if userDir != '~': # expanduser() found user home dir if not os.path.exists(userDir): - if not idlelib.testing: - warn = ('\n Warning: os.path.expanduser("~") points to\n ' + - userDir + ',\n but the path does not exist.') - try: - print(warn, file=sys.stderr) - except OSError: - pass + warn = ('\n Warning: os.path.expanduser("~") points to\n ' + + userDir + ',\n but the path does not exist.') + try: + print(warn, file=sys.stderr) + except IOError: + pass userDir = '~' if userDir == "~": # still no path to home! # traditionally IDLE has defaulted to os.getcwd(), is this adequate? @@ -199,14 +205,10 @@ class IdleConf: if not os.path.exists(userDir): try: os.mkdir(userDir) - except OSError: - if not idlelib.testing: - warn = ('\n Warning: unable to create user config directory\n' + - userDir + '\n Check path and permissions.\n Exiting!\n') - try: - print(warn, file=sys.stderr) - except OSError: - pass + except (OSError, IOError): + 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 @@ -229,12 +231,15 @@ class IdleConf: return self.userCfg[configType].Get(section, option, type=type, raw=raw) except ValueError: - warning = ('\n Warning: config.py - IdleConf.GetOption -\n' + warning = ('\n Warning: configHandler.py - IdleConf.GetOption -\n' ' invalid %r value for configuration option %r\n' ' from section %r: %r' % (type, option, section, self.userCfg[configType].Get(section, option, raw=raw))) - _warn(warning, configType, section, option) + try: + print(warning, file=sys.stderr) + except IOError: + pass try: if self.defaultCfg[configType].has_option(section,option): return self.defaultCfg[configType].Get( @@ -243,12 +248,15 @@ class IdleConf: pass #returning default, print warning if warn_on_default: - warning = ('\n Warning: config.py - IdleConf.GetOption -\n' + warning = ('\n Warning: configHandler.py - IdleConf.GetOption -\n' ' problem retrieving configuration option %r\n' ' from section %r.\n' ' returning default value: %r' % (option, section, default)) - _warn(warning, configType, section, option) + try: + print(warning, file=sys.stderr) + except IOError: + pass return default def SetOption(self, configType, section, option, value): @@ -271,20 +279,34 @@ class IdleConf: raise InvalidConfigSet('Invalid configSet specified') return cfgParser.sections() - def GetHighlight(self, theme, element): - """Return dict of theme element highlight colors. + def GetHighlight(self, theme, element, fgBg=None): + """Return individual theme element highlight color(s). - The keys are 'foreground' and 'background'. The values are - tkinter color strings for configuring backgrounds and tags. + 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). """ - cfg = ('default' if self.defaultCfg['highlight'].has_section(theme) - else 'user') - theme_dict = self.GetThemeDict(cfg, theme) - fore = theme_dict[element + '-foreground'] - if element == 'cursor': - element = 'normal' - back = theme_dict[element + '-background'] - return {"foreground": fore, "background": back} + if self.defaultCfg['highlight'].has_section(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'] + else: + back = themeDict[element + '-background'] + highlight = {"foreground": fore, "background": back} + if not fgBg: # Return dict of both colors + return highlight + else: # Return specified color only + if fgBg == 'fg': + return highlight["foreground"] + if fgBg == 'bg': + return highlight["background"] + else: + raise InvalidFgBg('Invalid fgBg specified') def GetThemeDict(self, type, themeName): """Return {option:value} dict for elements in themeName. @@ -325,10 +347,6 @@ class IdleConf: 'hit-background':'#000000', 'error-foreground':'#ffffff', 'error-background':'#000000', - 'context-foreground':'#000000', - 'context-background':'#ffffff', - 'linenumber-foreground':'#000000', - 'linenumber-background':'#ffffff', #cursor (only foreground can be set) 'cursor-foreground':'#000000', #shell window @@ -337,80 +355,56 @@ class IdleConf: 'stderr-foreground':'#000000', 'stderr-background':'#ffffff', 'console-foreground':'#000000', - 'console-background':'#ffffff', - } + 'console-background':'#ffffff' } for element in theme: - if not (cfgParser.has_option(themeName, element) or - # Skip warning for new elements. - element.startswith(('context-', 'linenumber-'))): + if not cfgParser.has_option(themeName, element): # Print warning that will return a default color - warning = ('\n Warning: config.IdleConf.GetThemeDict' + warning = ('\n Warning: configHandler.IdleConf.GetThemeDict' ' -\n problem retrieving theme element %r' '\n from theme %r.\n' ' returning default color: %r' % (element, themeName, theme[element])) - _warn(warning, 'highlight', themeName, element) + try: + print(warning, file=sys.stderr) + except IOError: + pass theme[element] = cfgParser.Get( themeName, element, default=theme[element]) return theme def CurrentTheme(self): - "Return the name of the currently active text color theme." - return self.current_colors_and_keys('Theme') - - def CurrentKeys(self): - """Return the name of the currently active key set.""" - return self.current_colors_and_keys('Keys') - - def current_colors_and_keys(self, section): - """Return the currently active name for Theme or Keys section. - - idlelib.config-main.def ('default') includes these sections + """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 - [Keys] - default= 1 - name= - name2= - - Item 'name2', is used for built-in ('default') themes and keys - added after 2015 Oct 1 and 2016 July 1. This kludge is needed - because setting 'name' to a builtin not defined in older IDLEs - to display multiple error messages or quit. + 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. When default = False, - 'name2' may still be set, but it is ignored. + When default = True, name2 takes precedence over name, + while older IDLEs will just use name. """ - cfgname = 'highlight' if section == 'Theme' else 'keys' - default = self.GetOption('main', section, 'default', + default = self.GetOption('main', 'Theme', 'default', type='bool', default=True) - name = '' if default: - name = self.GetOption('main', section, 'name2', default='') - if not name: - name = self.GetOption('main', section, 'name', default='') - if name: - source = self.defaultCfg if default else self.userCfg - if source[cfgname].has_section(name): - return name - return "IDLE Classic" if section == 'Theme' else self.default_keys() - - @staticmethod - def default_keys(): - if sys.platform[:3] == 'win': - return 'IDLE Classic Windows' - elif sys.platform == 'darwin': - return 'IDLE Classic OSX' + 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 Modern Unix' + return "IDLE Classic" - def GetExtensions(self, active_only=True, - editor_only=False, shell_only=False): + def CurrentKeys(self): + "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 @@ -424,18 +418,13 @@ class IdleConf: for extn in userExtns: if extn not in extns: #user has added own extension extns.append(extn) - for extn in ('AutoComplete','CodeContext', - 'FormatParagraph','ParenMatch'): - extns.remove(extn) - # specific exclusions because we are storing config for mainlined old - # extensions in config-extensions.def for backward compatibility if active_only: 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: # TODO both True contradict + if editor_only or shell_only: # TODO if both, contradictory if editor_only: option = "enable_editor" else: @@ -452,7 +441,16 @@ class IdleConf: def RemoveKeyBindNames(self, extnNameList): "Return extnNameList with keybinding section names removed." - return [n for n in extnNameList if not n.endswith(('_bindings', '_cfgBindings'))] + # 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(reverse=True) + for index in kbNameIndicies: #delete each keybinding section name + del(names[index]) + return names def GetExtnNameForEvent(self, virtualEvent): """Return the name of the extension binding virtualEvent, or None. @@ -531,8 +529,7 @@ class IdleConf: eventStr - virtual event, including brackets, as in '<<event>>'. """ eventName = eventStr[2:-2] #trim off the angle brackets - binding = self.GetOption('keys', keySetName, eventName, default='', - warn_on_default=False).split() + binding = self.GetOption('keys', keySetName, eventName, default='').split() return binding def GetCurrentKeySet(self): @@ -540,11 +537,12 @@ class IdleConf: result = self.GetKeySet(self.CurrentKeys()) if sys.platform == "darwin": - # macOS (OS X) Tk variants do not support the "Alt" - # keyboard modifier. Replace it with "Option". - # TODO (Ned?): the "Option" modifier does not work properly - # for Cocoa Tk and XQuartz Tk so we should not use it - # in the default 'OSX' keyset. + # 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: @@ -579,14 +577,7 @@ class IdleConf: return ('<<'+virtualEvent+'>>') in self.GetCoreKeys() # TODO make keyBindins a file or class attribute used for test above -# and copied in function below. - - former_extension_events = { # Those with user-configurable keys. - '<<force-open-completions>>', '<<expand-word>>', - '<<force-open-calltip>>', '<<flash-paren>>', '<<format-paragraph>>', - '<<run-module>>', '<<check-module>>', '<<zoom-height>>', - '<<run-custom>>', - } +# and copied in function below def GetCoreKeys(self, keySetName=None): """Return dict of core virtual-key keybindings for keySetName. @@ -646,42 +637,23 @@ class IdleConf: '<<toggle-tabs>>': ['<Alt-Key-t>'], '<<change-indentwidth>>': ['<Alt-Key-u>'], '<<del-word-left>>': ['<Control-Key-BackSpace>'], - '<<del-word-right>>': ['<Control-Key-Delete>'], - '<<force-open-completions>>': ['<Control-Key-space>'], - '<<expand-word>>': ['<Alt-Key-slash>'], - '<<force-open-calltip>>': ['<Control-Key-backslash>'], - '<<flash-paren>>': ['<Control-Key-0>'], - '<<format-paragraph>>': ['<Alt-Key-q>'], - '<<run-module>>': ['<Key-F5>'], - '<<run-custom>>': ['<Shift-Key-F5>'], - '<<check-module>>': ['<Alt-Key-x>'], - '<<zoom-height>>': ['<Alt-Key-2>'], + '<<del-word-right>>': ['<Control-Key-Delete>'] } - if keySetName: - if not (self.userCfg['keys'].has_section(keySetName) or - self.defaultCfg['keys'].has_section(keySetName)): - warning = ( - '\n Warning: config.py - IdleConf.GetCoreKeys -\n' - ' key set %r is not defined, using default bindings.' % - (keySetName,) - ) - _warn(warning, 'keys', keySetName) - else: - for event in keyBindings: - binding = self.GetKeyBinding(keySetName, event) - if binding: - keyBindings[event] = binding - # Otherwise return default in keyBindings. - elif event not in self.former_extension_events: - warning = ( - '\n Warning: config.py - IdleConf.GetCoreKeys -\n' - ' problem retrieving key binding for event %r\n' - ' from key set %r.\n' - ' returning default value: %r' % - (event, keySetName, keyBindings[event]) - ) - _warn(warning, 'keys', keySetName, event) + for event in keyBindings: + binding = self.GetKeyBinding(keySetName, event) + if 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' % + (event, keySetName, keyBindings[event])) + try: + print(warning, file=sys.stderr) + except IOError: + pass return keyBindings def GetExtraHelpSourceList(self, configSet): @@ -713,7 +685,7 @@ class IdleConf: helpPath=value[1].strip() if menuItem and helpPath: #neither are empty strings helpSources.append( (menuItem,helpPath,option) ) - helpSources.sort(key=lambda x: x[2]) + helpSources.sort(key=lambda x: int(x[2])) return helpSources def GetAllExtraHelpSourcesList(self): @@ -742,13 +714,16 @@ class IdleConf: bold = self.GetOption(configType, section, 'font-bold', default=0, type='bool') if (family == 'TkFixedFont'): - 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' + 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): @@ -765,117 +740,9 @@ class IdleConf: idleConf = IdleConf() -_warned = set() -def _warn(msg, *key): - key = (msg,) + key - if key not in _warned: - try: - print(msg, file=sys.stderr) - except OSError: - pass - _warned.add(key) - - -class ConfigChanges(dict): - """Manage a user's proposed configuration option changes. - - Names used across multiple methods: - page -- one of the 4 top-level dicts representing a - .idlerc/config-x.cfg file. - config_type -- name of a page. - section -- a section within a page/file. - option -- name of an option within a section. - value -- value for the option. - - Methods - add_option: Add option and value to changes. - save_option: Save option and value to config parser. - save_all: Save all the changes to the config parser and file. - delete_section: If section exists, - delete from changes, userCfg, and file. - clear: Clear all changes by clearing each page. - """ - def __init__(self): - "Create a page for each configuration file" - self.pages = [] # List of unhashable dicts. - for config_type in idleConf.config_types: - self[config_type] = {} - self.pages.append(self[config_type]) - - def add_option(self, config_type, section, item, value): - "Add item/value pair for config_type and section." - page = self[config_type] - value = str(value) # Make sure we use a string. - if section not in page: - page[section] = {} - page[section][item] = value - - @staticmethod - def save_option(config_type, section, item, value): - """Return True if the configuration value was added or changed. - - Helper for save_all. - """ - if idleConf.defaultCfg[config_type].has_option(section, item): - if idleConf.defaultCfg[config_type].Get(section, item) == value: - # The setting equals a default setting, remove it from user cfg. - return idleConf.userCfg[config_type].RemoveOption(section, item) - # If we got here, set the option. - return idleConf.userCfg[config_type].SetOption(section, item, value) - - def save_all(self): - """Save configuration changes to the user config file. - - Clear self in preparation for additional changes. - Return changed for testing. - """ - idleConf.userCfg['main'].Save() - - changed = False - for config_type in self: - cfg_type_changed = False - page = self[config_type] - for section in page: - if section == 'HelpFiles': # Remove it for replacement. - idleConf.userCfg['main'].remove_section('HelpFiles') - cfg_type_changed = True - for item, value in page[section].items(): - if self.save_option(config_type, section, item, value): - cfg_type_changed = True - if cfg_type_changed: - idleConf.userCfg[config_type].Save() - changed = True - for config_type in ['keys', 'highlight']: - # Save these even if unchanged! - idleConf.userCfg[config_type].Save() - self.clear() - # ConfigDialog caller must add the following call - # self.save_all_changed_extensions() # Uses a different mechanism. - return changed - - def delete_section(self, config_type, section): - """Delete a section from self, userCfg, and file. - - Used to delete custom themes and keysets. - """ - if section in self[config_type]: - del self[config_type][section] - configpage = idleConf.userCfg[config_type] - configpage.remove_section(section) - configpage.Save() - - def clear(self): - """Clear all 4 pages. - - Called in save_all after saving to idleConf. - XXX Mark window *title* when there are changes; unmark here. - """ - for page in self.pages: - page.clear() - - # TODO Revise test output, write expanded unittest -def _dump(): # htest # (not really, but ignore in coverage) +# +if __name__ == '__main__': from zlib import crc32 line, crc = 0, 0 @@ -885,10 +752,10 @@ def _dump(): # htest # (not really, but ignore in coverage) line += 1 crc = crc32(txt.encode(encoding='utf-8'), crc) print(txt) - #print('***', line, crc, '***') # Uncomment for diagnosis. + #print('***', line, crc, '***') # uncomment for diagnosis def dumpCfg(cfg): - print('\n', cfg, '\n') # Cfg has variable '0xnnnnnnnn' address. + print('\n', cfg, '\n') # has variable '0xnnnnnnnn' addresses for key in sorted(cfg.keys()): sections = cfg[key].sections() sprint(key) @@ -903,9 +770,3 @@ def _dump(): # htest # (not really, but ignore in coverage) dumpCfg(idleConf.defaultCfg) dumpCfg(idleConf.userCfg) print('\nlines = ', line, ', crc = ', crc, sep='') - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_config', verbosity=2, exit=False) - - # Run revised _dump() as htest? diff --git a/Lib/idlelib/configHelpSourceEdit.py b/Lib/idlelib/configHelpSourceEdit.py new file mode 100644 index 0000000..62b010a --- /dev/null +++ b/Lib/idlelib/configHelpSourceEdit.py @@ -0,0 +1,168 @@ +"Dialog to specify or edit the parameters for a user configured help source." + +import os +import sys + +from Tkinter import * +import tkMessageBox +import tkFileDialog + +class GetHelpSourceDialog(Toplevel): + 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) + self.resizable(height=FALSE, width=FALSE) + self.title(title) + self.transient(parent) + self.grab_set() + self.protocol("WM_DELETE_WINDOW", self.Cancel) + self.parent = parent + self.result = None + self.CreateWidgets() + self.menu.set(menuItem) + self.path.set(filePath) + 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. 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() + + def CreateWidgets(self): + self.menu = StringVar(self) + self.path = StringVar(self) + self.fontSize = StringVar(self) + self.frameMain = Frame(self, borderwidth=2, relief=GROOVE) + self.frameMain.pack(side=TOP, expand=TRUE, fill=BOTH) + labelMenu = Label(self.frameMain, anchor=W, justify=LEFT, + text='Menu Item:') + self.entryMenu = Entry(self.frameMain, textvariable=self.menu, + width=30) + self.entryMenu.focus_set() + labelPath = Label(self.frameMain, anchor=W, justify=LEFT, + text='Help File Path: Enter URL or browse for file') + self.entryPath = Entry(self.frameMain, textvariable=self.path, + width=40) + self.entryMenu.focus_set() + labelMenu.pack(anchor=W, padx=5, pady=3) + self.entryMenu.pack(anchor=W, padx=5, pady=3) + labelPath.pack(anchor=W, padx=5, pady=3) + self.entryPath.pack(anchor=W, padx=5, pady=3) + browseButton = Button(self.frameMain, text='Browse', width=8, + command=self.browseFile) + browseButton.pack(pady=3) + frameButtons = Frame(self) + frameButtons.pack(side=BOTTOM, fill=X) + self.buttonOk = Button(frameButtons, text='OK', + width=8, default=ACTIVE, command=self.Ok) + self.buttonOk.grid(row=0, column=0, padx=5,pady=5) + self.buttonCancel = Button(frameButtons, text='Cancel', + width=8, command=self.Cancel) + self.buttonCancel.grid(row=0, column=1, padx=5, pady=5) + + def browseFile(self): + filetypes = [ + ("HTML Files", "*.htm *.html", "TEXT"), + ("PDF Files", "*.pdf", "TEXT"), + ("Windows Help Files", "*.chm"), + ("Text Files", "*.txt", "TEXT"), + ("All Files", "*")] + path = self.path.get() + if path: + dir, base = os.path.split(path) + else: + base = None + if sys.platform[:3] == 'win': + dir = os.path.join(os.path.dirname(sys.executable), 'Doc') + if not os.path.isdir(dir): + dir = os.getcwd() + else: + dir = os.getcwd() + opendialog = tkFileDialog.Open(parent=self, filetypes=filetypes) + file = opendialog.show(initialdir=dir, initialfile=base) + if file: + self.path.set(file) + + def MenuOk(self): + "Simple validity check for a sensible menu item name" + menuOk = True + menu = self.menu.get() + menu.strip() + if not menu: + tkMessageBox.showerror(title='Menu Item Error', + message='No menu item specified', + parent=self) + self.entryMenu.focus_set() + menuOk = False + elif len(menu) > 30: + tkMessageBox.showerror(title='Menu Item Error', + message='Menu item too long:' + '\nLimit 30 characters.', + parent=self) + self.entryMenu.focus_set() + menuOk = False + return menuOk + + def PathOk(self): + "Simple validity check for menu file path" + pathOk = True + path = self.path.get() + path.strip() + if not path: #no path specified + tkMessageBox.showerror(title='File Path Error', + message='No help file path specified.', + parent=self) + self.entryPath.focus_set() + pathOk = False + elif path.startswith(('www.', 'http')): + pass + else: + if path[:5] == 'file:': + path = path[5:] + if not os.path.exists(path): + tkMessageBox.showerror(title='File Path Error', + message='Help file path does not exist.', + parent=self) + self.entryPath.focus_set() + pathOk = False + return pathOk + + def Ok(self, event=None): + if self.MenuOk() and self.PathOk(): + self.result = (self.menu.get().strip(), + self.path.get().strip()) + if sys.platform == 'darwin': + path = self.result[1] + if path.startswith(('www', 'file:', 'http:')): + pass + else: + # Mac Safari insists on using the URI form for local files + self.result = list(self.result) + self.result[1] = "file://" + path + self.grab_release() + self.destroy() + + def Cancel(self, event=None): + self.result = None + self.grab_release() + self.destroy() + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(GetHelpSourceDialog) diff --git a/Lib/idlelib/configSectionNameDialog.py b/Lib/idlelib/configSectionNameDialog.py new file mode 100644 index 0000000..f28dc1a --- /dev/null +++ b/Lib/idlelib/configSectionNameDialog.py @@ -0,0 +1,95 @@ +""" +Dialog that allows user to specify a new config file section name. +Used to get new highlight theme and keybinding set names. +The 'return value' for the dialog, used two placed in configDialog.py, +is the .result attribute set in the Ok and Cancel methods. +""" +from Tkinter import * +import tkMessageBox +class GetCfgSectionNameDialog(Toplevel): + 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) + self.resizable(height=FALSE, width=FALSE) + self.title(title) + self.transient(parent) + self.grab_set() + self.protocol("WM_DELETE_WINDOW", self.Cancel) + self.parent = parent + self.message = message + self.used_names = used_names + self.create_widgets() + self.withdraw() #hide while setting geometry + self.update_idletasks() + #needs to be done here so that the winfo_reqwidth is valid + 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) + if not _htest else 100) + ) ) #centre dialog over parent (or below htest box) + self.deiconify() #geometry set, unhide + self.wait_window() + def create_widgets(self): + self.name = StringVar(self.parent) + self.fontSize = StringVar(self.parent) + self.frameMain = Frame(self, borderwidth=2, relief=SUNKEN) + self.frameMain.pack(side=TOP, expand=TRUE, fill=BOTH) + self.messageInfo = Message(self.frameMain, anchor=W, justify=LEFT, + padx=5, pady=5, text=self.message) #,aspect=200) + entryName = Entry(self.frameMain, textvariable=self.name, width=30) + entryName.focus_set() + self.messageInfo.pack(padx=5, pady=5) #, expand=TRUE, fill=BOTH) + entryName.pack(padx=5, pady=5) + frameButtons = Frame(self, pady=2) + frameButtons.pack(side=BOTTOM) + self.buttonOk = Button(frameButtons, text='Ok', + width=8, command=self.Ok) + self.buttonOk.pack(side=LEFT, padx=5) + self.buttonCancel = Button(frameButtons, text='Cancel', + width=8, command=self.Cancel) + self.buttonCancel.pack(side=RIGHT, padx=5) + + def name_ok(self): + ''' After stripping entered name, check that it is a sensible + ConfigParser file section name. Return it if it is, '' if not. + ''' + name = self.name.get().strip() + if not name: #no name specified + tkMessageBox.showerror(title='Name Error', + message='No name specified.', parent=self) + elif len(name)>30: #name too long + tkMessageBox.showerror(title='Name Error', + message='Name too long. It should be no more than '+ + '30 characters.', parent=self) + name = '' + elif name in self.used_names: + tkMessageBox.showerror(title='Name Error', + message='This name is already in use.', parent=self) + name = '' + return name + def Ok(self, event=None): + name = self.name_ok() + if name: + self.result = name + self.grab_release() + self.destroy() + def Cancel(self, event=None): + self.result = '' + self.grab_release() + self.destroy() + +if __name__ == '__main__': + import unittest + unittest.main('idlelib.idle_test.test_config_name', verbosity=2, exit=False) + + from idlelib.idle_test.htest import run + run(GetCfgSectionNameDialog) diff --git a/Lib/idlelib/config_key.py b/Lib/idlelib/config_key.py deleted file mode 100644 index 4478323..0000000 --- a/Lib/idlelib/config_key.py +++ /dev/null @@ -1,325 +0,0 @@ -""" -Dialog for building Tkinter accelerator key bindings -""" -from tkinter import Toplevel, Listbox, Text, StringVar, TclError -from tkinter.ttk import Frame, Button, Checkbutton, Entry, Label, Scrollbar -from tkinter import messagebox -import string -import sys - - -FUNCTION_KEYS = ('F1', 'F2' ,'F3' ,'F4' ,'F5' ,'F6', - 'F7', 'F8' ,'F9' ,'F10' ,'F11' ,'F12') -ALPHANUM_KEYS = tuple(string.ascii_lowercase + string.digits) -PUNCTUATION_KEYS = tuple('~!@#%^&*()_-+={}[]|;:,.<>/?') -WHITESPACE_KEYS = ('Tab', 'Space', 'Return') -EDIT_KEYS = ('BackSpace', 'Delete', 'Insert') -MOVE_KEYS = ('Home', 'End', 'Page Up', 'Page Down', 'Left Arrow', - 'Right Arrow', 'Up Arrow', 'Down Arrow') -AVAILABLE_KEYS = (ALPHANUM_KEYS + PUNCTUATION_KEYS + FUNCTION_KEYS + - WHITESPACE_KEYS + EDIT_KEYS + MOVE_KEYS) - - -def translate_key(key, modifiers): - "Translate from keycap symbol to the Tkinter keysym." - mapping = {'Space':'space', - '~':'asciitilde', '!':'exclam', '@':'at', '#':'numbersign', - '%':'percent', '^':'asciicircum', '&':'ampersand', - '*':'asterisk', '(':'parenleft', ')':'parenright', - '_':'underscore', '-':'minus', '+':'plus', '=':'equal', - '{':'braceleft', '}':'braceright', - '[':'bracketleft', ']':'bracketright', '|':'bar', - ';':'semicolon', ':':'colon', ',':'comma', '.':'period', - '<':'less', '>':'greater', '/':'slash', '?':'question', - 'Page Up':'Prior', 'Page Down':'Next', - 'Left Arrow':'Left', 'Right Arrow':'Right', - 'Up Arrow':'Up', 'Down Arrow': 'Down', 'Tab':'Tab'} - key = mapping.get(key, key) - if 'Shift' in modifiers and key in string.ascii_lowercase: - key = key.upper() - return f'Key-{key}' - - -class GetKeysDialog(Toplevel): - - # Dialog title for invalid key sequence - keyerror_title = 'Key Sequence Error' - - def __init__(self, parent, title, action, current_key_sequences, - *, _htest=False, _utest=False): - """ - parent - parent of this dialog - title - string which is the title of the popup dialog - action - string, the name of the virtual event these keys will be - mapped to - current_key_sequences - list, a list of all key sequence lists - currently mapped to virtual events, for overlap checking - _htest - bool, change box location when running htest - _utest - bool, do not wait when running unittest - """ - Toplevel.__init__(self, parent) - self.withdraw() # Hide while setting geometry. - self.configure(borderwidth=5) - self.resizable(height=False, width=False) - self.title(title) - self.transient(parent) - self.grab_set() - self.protocol("WM_DELETE_WINDOW", self.cancel) - self.parent = parent - self.action = action - self.current_key_sequences = current_key_sequences - self.result = '' - self.key_string = StringVar(self) - self.key_string.set('') - # Set self.modifiers, self.modifier_label. - self.set_modifiers_for_platform() - self.modifier_vars = [] - for modifier in self.modifiers: - variable = StringVar(self) - variable.set('') - self.modifier_vars.append(variable) - self.advanced = False - self.create_widgets() - 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) - if not _htest else 150) - ) ) # Center dialog over parent (or below htest box). - if not _utest: - self.deiconify() # Geometry set, unhide. - self.wait_window() - - def showerror(self, *args, **kwargs): - # Make testing easier. Replace in #30751. - messagebox.showerror(*args, **kwargs) - - def create_widgets(self): - self.frame = frame = Frame(self, borderwidth=2, relief='sunken') - frame.pack(side='top', expand=True, fill='both') - - frame_buttons = Frame(self) - frame_buttons.pack(side='bottom', fill='x') - - self.button_ok = Button(frame_buttons, text='OK', - width=8, command=self.ok) - self.button_ok.grid(row=0, column=0, padx=5, pady=5) - self.button_cancel = Button(frame_buttons, text='Cancel', - width=8, command=self.cancel) - self.button_cancel.grid(row=0, column=1, padx=5, pady=5) - - # Basic entry key sequence. - self.frame_keyseq_basic = Frame(frame, name='keyseq_basic') - self.frame_keyseq_basic.grid(row=0, column=0, sticky='nsew', - padx=5, pady=5) - basic_title = Label(self.frame_keyseq_basic, - text=f"New keys for '{self.action}' :") - basic_title.pack(anchor='w') - - basic_keys = Label(self.frame_keyseq_basic, justify='left', - textvariable=self.key_string, relief='groove', - borderwidth=2) - basic_keys.pack(ipadx=5, ipady=5, fill='x') - - # Basic entry controls. - self.frame_controls_basic = Frame(frame) - self.frame_controls_basic.grid(row=1, column=0, sticky='nsew', padx=5) - - # Basic entry modifiers. - self.modifier_checkbuttons = {} - column = 0 - for modifier, variable in zip(self.modifiers, self.modifier_vars): - label = self.modifier_label.get(modifier, modifier) - check = Checkbutton(self.frame_controls_basic, - command=self.build_key_string, text=label, - variable=variable, onvalue=modifier, offvalue='') - check.grid(row=0, column=column, padx=2, sticky='w') - self.modifier_checkbuttons[modifier] = check - column += 1 - - # Basic entry help text. - help_basic = Label(self.frame_controls_basic, justify='left', - text="Select the desired modifier keys\n"+ - "above, and the final key from the\n"+ - "list on the right.\n\n" + - "Use upper case Symbols when using\n" + - "the Shift modifier. (Letters will be\n" + - "converted automatically.)") - help_basic.grid(row=1, column=0, columnspan=4, padx=2, sticky='w') - - # Basic entry key list. - self.list_keys_final = Listbox(self.frame_controls_basic, width=15, - height=10, selectmode='single') - self.list_keys_final.insert('end', *AVAILABLE_KEYS) - self.list_keys_final.bind('<ButtonRelease-1>', self.final_key_selected) - self.list_keys_final.grid(row=0, column=4, rowspan=4, sticky='ns') - scroll_keys_final = Scrollbar(self.frame_controls_basic, - orient='vertical', - command=self.list_keys_final.yview) - self.list_keys_final.config(yscrollcommand=scroll_keys_final.set) - scroll_keys_final.grid(row=0, column=5, rowspan=4, sticky='ns') - self.button_clear = Button(self.frame_controls_basic, - text='Clear Keys', - command=self.clear_key_seq) - self.button_clear.grid(row=2, column=0, columnspan=4) - - # Advanced entry key sequence. - self.frame_keyseq_advanced = Frame(frame, name='keyseq_advanced') - self.frame_keyseq_advanced.grid(row=0, column=0, sticky='nsew', - padx=5, pady=5) - advanced_title = Label(self.frame_keyseq_advanced, justify='left', - text=f"Enter new binding(s) for '{self.action}' :\n" + - "(These bindings will not be checked for validity!)") - advanced_title.pack(anchor='w') - self.advanced_keys = Entry(self.frame_keyseq_advanced, - textvariable=self.key_string) - self.advanced_keys.pack(fill='x') - - # Advanced entry help text. - self.frame_help_advanced = Frame(frame) - self.frame_help_advanced.grid(row=1, column=0, sticky='nsew', padx=5) - help_advanced = Label(self.frame_help_advanced, justify='left', - text="Key bindings are specified using Tkinter keysyms as\n"+ - "in these samples: <Control-f>, <Shift-F2>, <F12>,\n" - "<Control-space>, <Meta-less>, <Control-Alt-Shift-X>.\n" - "Upper case is used when the Shift modifier is present!\n\n" + - "'Emacs style' multi-keystroke bindings are specified as\n" + - "follows: <Control-x><Control-y>, where the first key\n" + - "is the 'do-nothing' keybinding.\n\n" + - "Multiple separate bindings for one action should be\n"+ - "separated by a space, eg., <Alt-v> <Meta-v>." ) - help_advanced.grid(row=0, column=0, sticky='nsew') - - # Switch between basic and advanced. - self.button_level = Button(frame, command=self.toggle_level, - text='<< Basic Key Binding Entry') - self.button_level.grid(row=2, column=0, stick='ew', padx=5, pady=5) - self.toggle_level() - - def set_modifiers_for_platform(self): - """Determine list of names of key modifiers for this platform. - - The names are used to build Tk bindings -- it doesn't matter if the - keyboard has these keys; it matters if Tk understands them. The - order is also important: key binding equality depends on it, so - config-keys.def must use the same ordering. - """ - if sys.platform == "darwin": - self.modifiers = ['Shift', 'Control', 'Option', 'Command'] - else: - self.modifiers = ['Control', 'Alt', 'Shift'] - self.modifier_label = {'Control': 'Ctrl'} # Short name. - - def toggle_level(self): - "Toggle between basic and advanced keys." - if self.button_level.cget('text').startswith('Advanced'): - self.clear_key_seq() - self.button_level.config(text='<< Basic Key Binding Entry') - self.frame_keyseq_advanced.lift() - self.frame_help_advanced.lift() - self.advanced_keys.focus_set() - self.advanced = True - else: - self.clear_key_seq() - self.button_level.config(text='Advanced Key Binding Entry >>') - self.frame_keyseq_basic.lift() - self.frame_controls_basic.lift() - self.advanced = False - - def final_key_selected(self, event=None): - "Handler for clicking on key in basic settings list." - self.build_key_string() - - def build_key_string(self): - "Create formatted string of modifiers plus the key." - keylist = modifiers = self.get_modifiers() - final_key = self.list_keys_final.get('anchor') - if final_key: - final_key = translate_key(final_key, modifiers) - keylist.append(final_key) - self.key_string.set(f"<{'-'.join(keylist)}>") - - def get_modifiers(self): - "Return ordered list of modifiers that have been selected." - mod_list = [variable.get() for variable in self.modifier_vars] - return [mod for mod in mod_list if mod] - - def clear_key_seq(self): - "Clear modifiers and keys selection." - self.list_keys_final.select_clear(0, 'end') - self.list_keys_final.yview('moveto', '0.0') - for variable in self.modifier_vars: - variable.set('') - self.key_string.set('') - - def ok(self, event=None): - keys = self.key_string.get().strip() - if not keys: - self.showerror(title=self.keyerror_title, parent=self, - message="No key specified.") - return - if (self.advanced or self.keys_ok(keys)) and self.bind_ok(keys): - self.result = keys - self.grab_release() - self.destroy() - - def cancel(self, event=None): - self.result = '' - self.grab_release() - self.destroy() - - def keys_ok(self, keys): - """Validity check on user's 'basic' keybinding selection. - - Doesn't check the string produced by the advanced dialog because - 'modifiers' isn't set. - """ - final_key = self.list_keys_final.get('anchor') - modifiers = self.get_modifiers() - title = self.keyerror_title - key_sequences = [key for keylist in self.current_key_sequences - for key in keylist] - if not keys.endswith('>'): - self.showerror(title, parent=self, - message='Missing the final Key') - elif (not modifiers - and final_key not in FUNCTION_KEYS + MOVE_KEYS): - self.showerror(title=title, parent=self, - message='No modifier key(s) specified.') - elif (modifiers == ['Shift']) \ - and (final_key not in - FUNCTION_KEYS + MOVE_KEYS + ('Tab', 'Space')): - msg = 'The shift modifier by itself may not be used with'\ - ' this key symbol.' - self.showerror(title=title, parent=self, message=msg) - elif keys in key_sequences: - msg = 'This key combination is already in use.' - self.showerror(title=title, parent=self, message=msg) - else: - return True - return False - - def bind_ok(self, keys): - "Return True if Tcl accepts the new keys else show message." - try: - binding = self.bind(keys, lambda: None) - except TclError as err: - self.showerror( - title=self.keyerror_title, parent=self, - message=(f'The entered key sequence is not accepted.\n\n' - f'Error: {err}')) - return False - else: - self.unbind(keys, binding) - return True - - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_config_key', verbosity=2, exit=False) - - from idlelib.idle_test.htest import run - run(GetKeysDialog) diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py deleted file mode 100644 index aaf319b..0000000 --- a/Lib/idlelib/configdialog.py +++ /dev/null @@ -1,2380 +0,0 @@ -"""IDLE Configuration Dialog: support user customization of IDLE by GUI - -Customize font faces, sizes, and colorization attributes. Set indentation -defaults. Customize keybindings. Colorization and keybindings can be -saved as user defined sets. Select startup options including shell/editor -and default window size. Define additional help sources. - -Note that tab width in IDLE is currently fixed at eight due to Tk issues. -Refer to comments in EditorWindow autoindent code for details. - -""" -import re - -from tkinter import (Toplevel, Listbox, Text, Scale, Canvas, - StringVar, BooleanVar, IntVar, TRUE, FALSE, - TOP, BOTTOM, RIGHT, LEFT, SOLID, GROOVE, - NONE, BOTH, X, Y, W, E, EW, NS, NSEW, NW, - HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END) -from tkinter.ttk import (Frame, LabelFrame, Button, Checkbutton, Entry, Label, - OptionMenu, Notebook, Radiobutton, Scrollbar, Style) -import tkinter.colorchooser as tkColorChooser -import tkinter.font as tkFont -from tkinter import messagebox - -from idlelib.config import idleConf, ConfigChanges -from idlelib.config_key import GetKeysDialog -from idlelib.dynoption import DynOptionMenu -from idlelib import macosx -from idlelib.query import SectionName, HelpSource -from idlelib.textview import view_text -from idlelib.autocomplete import AutoComplete -from idlelib.codecontext import CodeContext -from idlelib.parenmatch import ParenMatch -from idlelib.format import FormatParagraph -from idlelib.squeezer import Squeezer -from idlelib.textview import ScrollableTextFrame - -changes = ConfigChanges() -# Reload changed options in the following classes. -reloadables = (AutoComplete, CodeContext, ParenMatch, FormatParagraph, - Squeezer) - - -class ConfigDialog(Toplevel): - """Config dialog for IDLE. - """ - - def __init__(self, parent, title='', *, _htest=False, _utest=False): - """Show the tabbed dialog for user configuration. - - Args: - parent - parent of this dialog - title - string which is the title of this popup dialog - _htest - bool, change box location when running htest - _utest - bool, don't wait_window when running unittest - - Note: Focus set on font page fontlist. - - Methods: - create_widgets - cancel: Bound to DELETE_WINDOW protocol. - """ - Toplevel.__init__(self, parent) - self.parent = parent - if _htest: - parent.instance_dict = {} - if not _utest: - self.withdraw() - - self.configure(borderwidth=5) - self.title(title or 'IDLE Preferences') - x = parent.winfo_rootx() + 20 - y = parent.winfo_rooty() + (30 if not _htest else 150) - self.geometry(f'+{x}+{y}') - # 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.create_widgets() - self.resizable(height=FALSE, width=FALSE) - self.transient(parent) - self.protocol("WM_DELETE_WINDOW", self.cancel) - self.fontpage.fontlist.focus_set() - # XXX Decide whether to keep or delete these key bindings. - # 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 - # Attach callbacks after loading config to avoid calling them. - tracers.attach() - - if not _utest: - self.grab_set() - self.wm_deiconify() - self.wait_window() - - def create_widgets(self): - """Create and place widgets for tabbed dialog. - - Widgets Bound to self: - note: Notebook - highpage: HighPage - fontpage: FontPage - keyspage: KeysPage - genpage: GenPage - extpage: self.create_page_extensions - - Methods: - create_action_buttons - load_configs: Load pages except for extensions. - activate_config_changes: Tell editors to reload. - """ - self.note = note = Notebook(self) - self.highpage = HighPage(note) - self.fontpage = FontPage(note, self.highpage) - self.keyspage = KeysPage(note) - self.genpage = GenPage(note) - self.extpage = self.create_page_extensions() - note.add(self.fontpage, text='Fonts/Tabs') - note.add(self.highpage, text='Highlights') - note.add(self.keyspage, text=' Keys ') - note.add(self.genpage, text=' General ') - note.add(self.extpage, text='Extensions') - note.enable_traversal() - note.pack(side=TOP, expand=TRUE, fill=BOTH) - self.create_action_buttons().pack(side=BOTTOM) - - def create_action_buttons(self): - """Return frame of action buttons for dialog. - - Methods: - ok - apply - cancel - help - - Widget Structure: - outer: Frame - buttons: Frame - (no assignment): Button (ok) - (no assignment): Button (apply) - (no assignment): Button (cancel) - (no assignment): Button (help) - (no assignment): Frame - """ - if macosx.isAquaTk(): - # Changing the default padding on OSX results in unreadable - # text in the buttons. - padding_args = {} - else: - padding_args = {'padding': (6, 3)} - outer = Frame(self, padding=2) - buttons = Frame(outer, padding=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, - **padding_args).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 ok(self): - """Apply config changes, then dismiss dialog. - - Methods: - apply - destroy: inherited - """ - self.apply() - self.destroy() - - def apply(self): - """Apply config changes and leave dialog open. - - Methods: - deactivate_current_config - save_all_changed_extensions - activate_config_changes - """ - self.deactivate_current_config() - changes.save_all() - self.save_all_changed_extensions() - self.activate_config_changes() - - def cancel(self): - """Dismiss config dialog. - - Methods: - destroy: inherited - """ - self.destroy() - - def destroy(self): - global font_sample_text - font_sample_text = self.fontpage.font_sample.get('1.0', 'end') - self.grab_release() - super().destroy() - - def help(self): - """Create textview for config dialog help. - - Attributes accessed: - note - - Methods: - view_text: Method from textview module. - """ - page = self.note.tab(self.note.select(), option='text').strip() - view_text(self, title='Help for IDLE preferences', - text=help_common+help_pages.get(page, '')) - - def deactivate_current_config(self): - """Remove current key bindings. - Iterate over window instances defined in parent and remove - the keybindings. - """ - # Before a config is saved, some cleanup of current - # config must be done - remove the previous keybindings. - win_instances = self.parent.instance_dict.keys() - for instance in win_instances: - instance.RemoveKeybindings() - - def activate_config_changes(self): - """Apply configuration changes to current windows. - - Dynamically update the current parent window instances - with some of the configuration changes. - """ - win_instances = self.parent.instance_dict.keys() - for instance in win_instances: - instance.ResetColorizer() - instance.ResetFont() - instance.set_notabs_indentwidth() - instance.ApplyKeybindings() - instance.reset_help_menu_entries() - instance.update_cursor_blink() - for klass in reloadables: - klass.reload() - - def create_page_extensions(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 a True/False button. - - Methods: - load_extensions: - extension_selected: Handle selection from list. - create_extension_frame: Hold widgets for one extension. - set_extension_value: Set in userCfg['extensions']. - save_all_changed_extensions: Call extension page Save(). - """ - parent = self.parent - frame = Frame(self.note) - 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(padding=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) - - return frame - - 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): - # Former built-in extensions are already filtered out. - 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): - "Handle selection of an extension from the list." - 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, variable=var, - onvalue='True', offvalue='False', 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'), width=10 - ).grid(row=row, column=1, sticky=NSEW, padx=7) - - else: # type == 'str' - # Limit size to fit non-expanding space with larger font. - Entry(entry_area, textvariable=var, width=15 - ).grid(row=row, column=1, sticky=NSEW, padx=7) - return - - def set_extension_value(self, section, opt): - """Return True if the configuration was added or changed. - - If the value is the same as the default, then remove it - from user config file. - """ - 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. - - Attributes accessed: - extensions - - Methods: - set_extension_value - """ - 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() - - -# class TabPage(Frame): # A template for Page classes. -# def __init__(self, master): -# super().__init__(master) -# self.create_page_tab() -# self.load_tab_cfg() -# def create_page_tab(self): -# # Define tk vars and register var and callback with tracers. -# # Create subframes and widgets. -# # Pack widgets. -# def load_tab_cfg(self): -# # Initialize widgets with data from idleConf. -# def var_changed_var_name(): -# # For each tk var that needs other than default callback. -# def other_methods(): -# # Define tab-specific behavior. - -font_sample_text = ( - '<ASCII/Latin1>\n' - 'AaBbCcDdEeFfGgHhIiJj\n1234567890#:+=(){}[]\n' - '\u00a2\u00a3\u00a5\u00a7\u00a9\u00ab\u00ae\u00b6\u00bd\u011e' - '\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c7\u00d0\u00d8\u00df\n' - '\n<IPA,Greek,Cyrillic>\n' - '\u0250\u0255\u0258\u025e\u025f\u0264\u026b\u026e\u0270\u0277' - '\u027b\u0281\u0283\u0286\u028e\u029e\u02a2\u02ab\u02ad\u02af\n' - '\u0391\u03b1\u0392\u03b2\u0393\u03b3\u0394\u03b4\u0395\u03b5' - '\u0396\u03b6\u0397\u03b7\u0398\u03b8\u0399\u03b9\u039a\u03ba\n' - '\u0411\u0431\u0414\u0434\u0416\u0436\u041f\u043f\u0424\u0444' - '\u0427\u0447\u042a\u044a\u042d\u044d\u0460\u0464\u046c\u04dc\n' - '\n<Hebrew, Arabic>\n' - '\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7\u05d8\u05d9' - '\u05da\u05db\u05dc\u05dd\u05de\u05df\u05e0\u05e1\u05e2\u05e3\n' - '\u0627\u0628\u062c\u062f\u0647\u0648\u0632\u062d\u0637\u064a' - '\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\n' - '\n<Devanagari, Tamil>\n' - '\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f' - '\u0905\u0906\u0907\u0908\u0909\u090a\u090f\u0910\u0913\u0914\n' - '\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef' - '\u0b85\u0b87\u0b89\u0b8e\n' - '\n<East Asian>\n' - '\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\n' - '\u6c49\u5b57\u6f22\u5b57\u4eba\u6728\u706b\u571f\u91d1\u6c34\n' - '\uac00\ub0d0\ub354\ub824\ubaa8\ubd64\uc218\uc720\uc988\uce58\n' - '\u3042\u3044\u3046\u3048\u304a\u30a2\u30a4\u30a6\u30a8\u30aa\n' - ) - - -class FontPage(Frame): - - def __init__(self, master, highpage): - super().__init__(master) - self.highlight_sample = highpage.highlight_sample - self.create_page_font_tab() - self.load_font_cfg() - self.load_tab_cfg() - - def create_page_font_tab(self): - """Return frame of widgets for Font/Tabs tab. - - Fonts: Enable users to provisionally change font face, size, or - boldness and to see the consequence of proposed choices. Each - action set 3 options in changes structuree and changes the - corresponding aspect of the font sample on this page and - highlight sample on highlight page. - - Function load_font_cfg initializes font vars and widgets from - idleConf entries and tk. - - Fontlist: mouse button 1 click or up or down key invoke - on_fontlist_select(), which sets var font_name. - - Sizelist: clicking the menubutton opens the dropdown menu. A - mouse button 1 click or return key sets var font_size. - - Bold_toggle: clicking the box toggles var font_bold. - - Changing any of the font vars invokes var_changed_font, which - adds all 3 font options to changes and calls set_samples. - Set_samples applies a new font constructed from the font vars to - font_sample and to highlight_sample on the highlight page. - - Tabs: Enable users to change spaces entered for indent tabs. - Changing indent_scale value with the mouse sets Var space_num, - which invokes the default callback to add an entry to - changes. Load_tab_cfg initializes space_num to default. - - Widgets for FontPage(Frame): (*) widgets bound to self - frame_font: LabelFrame - frame_font_name: Frame - font_name_title: Label - (*)fontlist: ListBox - font_name - scroll_font: Scrollbar - frame_font_param: Frame - font_size_title: Label - (*)sizelist: DynOptionMenu - font_size - (*)bold_toggle: Checkbutton - font_bold - frame_sample: LabelFrame - (*)font_sample: Label - frame_indent: LabelFrame - indent_title: Label - (*)indent_scale: Scale - space_num - """ - self.font_name = tracers.add(StringVar(self), self.var_changed_font) - self.font_size = tracers.add(StringVar(self), self.var_changed_font) - self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font) - self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces')) - - # Define frames and widgets. - frame_font = LabelFrame( - self, borderwidth=2, relief=GROOVE, text=' Shell/Editor Font ') - frame_sample = LabelFrame( - self, borderwidth=2, relief=GROOVE, - text=' Font Sample (Editable) ') - frame_indent = LabelFrame( - self, borderwidth=2, relief=GROOVE, text=' Indentation Width ') - # frame_font. - frame_font_name = Frame(frame_font) - frame_font_param = Frame(frame_font) - font_name_title = Label( - frame_font_name, justify=LEFT, text='Font Face :') - self.fontlist = Listbox(frame_font_name, height=15, - takefocus=True, exportselection=FALSE) - self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select) - self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select) - self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select) - scroll_font = Scrollbar(frame_font_name) - scroll_font.config(command=self.fontlist.yview) - self.fontlist.config(yscrollcommand=scroll_font.set) - font_size_title = Label(frame_font_param, text='Size :') - self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None) - self.bold_toggle = Checkbutton( - frame_font_param, variable=self.font_bold, - onvalue=1, offvalue=0, text='Bold') - # frame_sample. - font_sample_frame = ScrollableTextFrame(frame_sample) - self.font_sample = font_sample_frame.text - self.font_sample.config(wrap=NONE, width=1, height=1) - self.font_sample.insert(END, font_sample_text) - # frame_indent. - indent_title = Label( - frame_indent, justify=LEFT, - text='Python Standard: 4 Spaces!') - self.indent_scale = Scale( - frame_indent, variable=self.space_num, - orient='horizontal', tickinterval=2, from_=2, to=16) - - # Grid and pack widgets: - self.columnconfigure(1, weight=1) - self.rowconfigure(2, weight=1) - frame_font.grid(row=0, column=0, padx=5, pady=5) - frame_sample.grid(row=0, column=1, rowspan=3, padx=5, pady=5, - sticky='nsew') - frame_indent.grid(row=1, column=0, padx=5, pady=5, sticky='ew') - # frame_font. - frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X) - frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X) - font_name_title.pack(side=TOP, anchor=W) - self.fontlist.pack(side=LEFT, expand=TRUE, fill=X) - scroll_font.pack(side=LEFT, fill=Y) - font_size_title.pack(side=LEFT, anchor=W) - self.sizelist.pack(side=LEFT, anchor=W) - self.bold_toggle.pack(side=LEFT, anchor=W, padx=20) - # frame_sample. - font_sample_frame.pack(expand=TRUE, fill=BOTH) - # frame_indent. - indent_title.pack(side=TOP, anchor=W, padx=5) - self.indent_scale.pack(side=TOP, padx=5, fill=X) - - def load_font_cfg(self): - """Load current configuration settings for the font options. - - Retrieve current font with idleConf.GetFont and font families - from tk. Setup fontlist and set font_name. Setup sizelist, - which sets font_size. Set font_bold. Call set_samples. - """ - configured_font = idleConf.GetFont(self, 'main', 'EditorWindow') - font_name = configured_font[0].lower() - font_size = configured_font[1] - font_bold = configured_font[2]=='bold' - - # Set editor font selection list and font_name. - fonts = list(tkFont.families(self)) - fonts.sort() - for font in fonts: - self.fontlist.insert(END, font) - self.font_name.set(font_name) - lc_fonts = [s.lower() for s in fonts] - try: - current_font_index = lc_fonts.index(font_name) - self.fontlist.see(current_font_index) - self.fontlist.select_set(current_font_index) - self.fontlist.select_anchor(current_font_index) - self.fontlist.activate(current_font_index) - except ValueError: - pass - # Set font size dropdown. - self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14', - '16', '18', '20', '22', '25', '29', '34', '40'), - font_size) - # Set font weight. - self.font_bold.set(font_bold) - self.set_samples() - - def var_changed_font(self, *params): - """Store changes to font attributes. - - 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.font_name.get() - changes.add_option('main', 'EditorWindow', 'font', value) - value = self.font_size.get() - changes.add_option('main', 'EditorWindow', 'font-size', value) - value = self.font_bold.get() - changes.add_option('main', 'EditorWindow', 'font-bold', value) - self.set_samples() - - def on_fontlist_select(self, event): - """Handle selecting a font from the list. - - Event can result from either mouse click or Up or Down key. - Set font_name and example displays to selection. - """ - font = self.fontlist.get( - ACTIVE if event.type.name == 'KeyRelease' else ANCHOR) - self.font_name.set(font.lower()) - - def set_samples(self, event=None): - """Update update both screen samples with the font settings. - - Called on font initialization and change events. - Accesses font_name, font_size, and font_bold Variables. - Updates font_sample and highlight page highlight_sample. - """ - font_name = self.font_name.get() - font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL - new_font = (font_name, self.font_size.get(), font_weight) - self.font_sample['font'] = new_font - self.highlight_sample['font'] = new_font - - def load_tab_cfg(self): - """Load current configuration settings for the tab options. - - Attributes updated: - space_num: Set to value from idleConf. - """ - # Set indent sizes. - space_num = idleConf.GetOption( - 'main', 'Indent', 'num-spaces', default=4, type='int') - self.space_num.set(space_num) - - def var_changed_space_num(self, *params): - "Store change to indentation size." - value = self.space_num.get() - changes.add_option('main', 'Indent', 'num-spaces', value) - - -class HighPage(Frame): - - def __init__(self, master): - super().__init__(master) - self.cd = master.master - self.style = Style(master) - self.create_page_highlight() - self.load_theme_cfg() - - def create_page_highlight(self): - """Return frame of widgets for Highlighting tab. - - Enable users to provisionally change foreground and background - colors applied to textual tags. Color mappings are stored in - complete listings called themes. Built-in themes in - idlelib/config-highlight.def are fixed as far as the dialog is - concerned. Any theme can be used as the base for a new custom - theme, stored in .idlerc/config-highlight.cfg. - - Function load_theme_cfg() initializes tk variables and theme - lists and calls paint_theme_sample() and set_highlight_target() - for the current theme. Radiobuttons builtin_theme_on and - custom_theme_on toggle var theme_source, which controls if the - current set of colors are from a builtin or custom theme. - DynOptionMenus builtinlist and customlist contain lists of the - builtin and custom themes, respectively, and the current item - from each list is stored in vars builtin_name and custom_name. - - Function paint_theme_sample() applies the colors from the theme - to the tags in text widget highlight_sample and then invokes - set_color_sample(). Function set_highlight_target() sets the state - of the radiobuttons fg_on and bg_on based on the tag and it also - invokes set_color_sample(). - - Function set_color_sample() sets the background color for the frame - holding the color selector. This provides a larger visual of the - color for the current tag and plane (foreground/background). - - Note: set_color_sample() is called from many places and is often - called more than once when a change is made. It is invoked when - foreground or background is selected (radiobuttons), from - paint_theme_sample() (theme is changed or load_cfg is called), and - from set_highlight_target() (target tag is changed or load_cfg called). - - Button delete_custom invokes delete_custom() to delete - a custom theme from idleConf.userCfg['highlight'] and changes. - Button save_custom invokes save_as_new_theme() which calls - get_new_theme_name() and create_new() to save a custom theme - and its colors to idleConf.userCfg['highlight']. - - Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control - if the current selected color for a tag is for the foreground or - background. - - DynOptionMenu targetlist contains a readable description of the - tags applied to Python source within IDLE. Selecting one of the - tags from this list populates highlight_target, which has a callback - function set_highlight_target(). - - Text widget highlight_sample displays a block of text (which is - mock Python code) in which is embedded the defined tags and reflects - the color attributes of the current theme and changes for those tags. - Mouse button 1 allows for selection of a tag and updates - highlight_target with that tag value. - - Note: The font in highlight_sample is set through the config in - the fonts tab. - - In other words, a tag can be selected either from targetlist or - by clicking on the sample text within highlight_sample. The - plane (foreground/background) is selected via the radiobutton. - Together, these two (tag and plane) control what color is - shown in set_color_sample() for the current theme. Button set_color - invokes get_color() which displays a ColorChooser to change the - color for the selected tag/plane. If a new color is picked, - it will be saved to changes and the highlight_sample and - frame background will be updated. - - Tk Variables: - color: Color of selected target. - builtin_name: Menu variable for built-in theme. - custom_name: Menu variable for custom theme. - fg_bg_toggle: Toggle for foreground/background color. - Note: this has no callback. - theme_source: Selector for built-in or custom theme. - highlight_target: Menu variable for the highlight tag target. - - Instance Data Attributes: - theme_elements: Dictionary of tags for text highlighting. - The key is the display name and the value is a tuple of - (tag name, display sort order). - - Methods [attachment]: - load_theme_cfg: Load current highlight colors. - get_color: Invoke colorchooser [button_set_color]. - set_color_sample_binding: Call set_color_sample [fg_bg_toggle]. - set_highlight_target: set fg_bg_toggle, set_color_sample(). - set_color_sample: Set frame background to target. - on_new_color_set: Set new color and add option. - paint_theme_sample: Recolor sample. - get_new_theme_name: Get from popup. - create_new: Combine theme with changes and save. - save_as_new_theme: Save [button_save_custom]. - set_theme_type: Command for [theme_source]. - delete_custom: Activate default [button_delete_custom]. - save_new: Save to userCfg['theme'] (is function). - - Widgets of highlights page frame: (*) widgets bound to self - frame_custom: LabelFrame - (*)highlight_sample: Text - (*)frame_color_set: Frame - (*)button_set_color: Button - (*)targetlist: DynOptionMenu - highlight_target - frame_fg_bg_toggle: Frame - (*)fg_on: Radiobutton - fg_bg_toggle - (*)bg_on: Radiobutton - fg_bg_toggle - (*)button_save_custom: Button - frame_theme: LabelFrame - theme_type_title: Label - (*)builtin_theme_on: Radiobutton - theme_source - (*)custom_theme_on: Radiobutton - theme_source - (*)builtinlist: DynOptionMenu - builtin_name - (*)customlist: DynOptionMenu - custom_name - (*)button_delete_custom: Button - (*)theme_message: Label - """ - self.theme_elements = { - 'Normal Code or Text': ('normal', '00'), - 'Code Context': ('context', '01'), - 'Python Keywords': ('keyword', '02'), - 'Python Definitions': ('definition', '03'), - 'Python Builtins': ('builtin', '04'), - 'Python Comments': ('comment', '05'), - 'Python Strings': ('string', '06'), - 'Selected Text': ('hilite', '07'), - 'Found Text': ('hit', '08'), - 'Cursor': ('cursor', '09'), - 'Editor Breakpoint': ('break', '10'), - 'Shell Prompt': ('console', '11'), - 'Error Text': ('error', '12'), - 'Shell User Output': ('stdout', '13'), - 'Shell User Exception': ('stderr', '14'), - 'Line Number': ('linenumber', '16'), - } - self.builtin_name = tracers.add( - StringVar(self), self.var_changed_builtin_name) - self.custom_name = tracers.add( - StringVar(self), self.var_changed_custom_name) - self.fg_bg_toggle = BooleanVar(self) - self.color = tracers.add( - StringVar(self), self.var_changed_color) - self.theme_source = tracers.add( - BooleanVar(self), self.var_changed_theme_source) - self.highlight_target = tracers.add( - StringVar(self), self.var_changed_highlight_target) - - # Create widgets: - # body frame and section frames. - frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE, - text=' Custom Highlighting ') - frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE, - text=' Highlighting Theme ') - # frame_custom. - sample_frame = ScrollableTextFrame( - frame_custom, relief=SOLID, borderwidth=1) - text = self.highlight_sample = sample_frame.text - text.configure( - font=('courier', 12, ''), cursor='hand2', width=1, height=1, - takefocus=FALSE, highlightthickness=0, wrap=NONE) - text.bind('<Double-Button-1>', lambda e: 'break') - text.bind('<B1-Motion>', lambda e: 'break') - string_tags=( - ('# Click selects item.', 'comment'), ('\n', 'normal'), - ('code context section', 'context'), ('\n', 'normal'), - ('| cursor', 'cursor'), ('\n', 'normal'), - ('def', 'keyword'), (' ', 'normal'), - ('func', 'definition'), ('(param):\n ', 'normal'), - ('"Return None."', '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'), - ('>>>', 'console'), (' 3.14**2\n', 'normal'), - ('9.8596', 'stdout'), ('\n', 'normal'), - ('>>>', 'console'), (' pri ', 'normal'), - ('n', 'error'), ('t(\n', 'normal'), - ('SyntaxError', 'stderr'), ('\n', 'normal')) - for string, tag in string_tags: - text.insert(END, string, tag) - n_lines = len(text.get('1.0', END).splitlines()) - for lineno in range(1, n_lines): - text.insert(f'{lineno}.0', - f'{lineno:{len(str(n_lines))}d} ', - 'linenumber') - for element in self.theme_elements: - def tem(event, elem=element): - # event.widget.winfo_top_level().highlight_target.set(elem) - self.highlight_target.set(elem) - text.tag_bind( - self.theme_elements[element][0], '<ButtonPress-1>', tem) - text['state'] = 'disabled' - self.style.configure('frame_color_set.TFrame', borderwidth=1, - relief='solid') - self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame') - frame_fg_bg_toggle = Frame(frame_custom) - self.button_set_color = Button( - self.frame_color_set, text='Choose Color for :', - command=self.get_color) - self.targetlist = DynOptionMenu( - self.frame_color_set, self.highlight_target, None, - highlightthickness=0) #, command=self.set_highlight_targetBinding - self.fg_on = Radiobutton( - frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1, - text='Foreground', command=self.set_color_sample_binding) - self.bg_on = Radiobutton( - frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0, - text='Background', command=self.set_color_sample_binding) - self.fg_bg_toggle.set(1) - self.button_save_custom = Button( - frame_custom, text='Save as New Custom Theme', - command=self.save_as_new_theme) - # frame_theme. - theme_type_title = Label(frame_theme, text='Select : ') - self.builtin_theme_on = Radiobutton( - frame_theme, variable=self.theme_source, value=1, - command=self.set_theme_type, text='a Built-in Theme') - self.custom_theme_on = Radiobutton( - frame_theme, variable=self.theme_source, value=0, - command=self.set_theme_type, text='a Custom Theme') - self.builtinlist = DynOptionMenu( - frame_theme, self.builtin_name, None, command=None) - self.customlist = DynOptionMenu( - frame_theme, self.custom_name, None, command=None) - self.button_delete_custom = Button( - frame_theme, text='Delete Custom Theme', - command=self.delete_custom) - self.theme_message = Label(frame_theme, borderwidth=2) - # Pack widgets: - # body. - frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) - frame_theme.pack(side=TOP, padx=5, pady=5, fill=X) - # frame_custom. - self.frame_color_set.pack(side=TOP, padx=5, pady=5, fill=X) - frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0) - sample_frame.pack( - side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) - self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4) - self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3) - self.fg_on.pack(side=LEFT, anchor=E) - self.bg_on.pack(side=RIGHT, anchor=W) - self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5) - # frame_theme. - theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5) - self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5) - self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2) - self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5) - self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5) - self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5) - self.theme_message.pack(side=TOP, fill=X, pady=5) - - def load_theme_cfg(self): - """Load current configuration settings for the theme options. - - Based on the theme_source toggle, the theme is set as - either builtin or custom and the initial widget values - reflect the current settings from idleConf. - - Attributes updated: - theme_source: Set from idleConf. - builtinlist: List of default themes from idleConf. - customlist: List of custom themes from idleConf. - custom_theme_on: Disabled if there are no custom themes. - custom_theme: Message with additional information. - targetlist: Create menu from self.theme_elements. - - Methods: - set_theme_type - paint_theme_sample - set_highlight_target - """ - # Set current theme type radiobutton. - self.theme_source.set(idleConf.GetOption( - 'main', 'Theme', 'default', type='bool', default=1)) - # Set current theme. - current_option = idleConf.CurrentTheme() - # Load available theme option menus. - if self.theme_source.get(): # Default theme selected. - item_list = idleConf.GetSectionList('default', 'highlight') - item_list.sort() - self.builtinlist.SetMenu(item_list, current_option) - item_list = idleConf.GetSectionList('user', 'highlight') - item_list.sort() - if not item_list: - self.custom_theme_on.state(('disabled',)) - self.custom_name.set('- no custom themes -') - else: - self.customlist.SetMenu(item_list, item_list[0]) - else: # User theme selected. - item_list = idleConf.GetSectionList('user', 'highlight') - item_list.sort() - self.customlist.SetMenu(item_list, current_option) - item_list = idleConf.GetSectionList('default', 'highlight') - item_list.sort() - self.builtinlist.SetMenu(item_list, item_list[0]) - self.set_theme_type() - # Load theme element option menu. - theme_names = list(self.theme_elements.keys()) - theme_names.sort(key=lambda x: self.theme_elements[x][1]) - self.targetlist.SetMenu(theme_names, theme_names[0]) - self.paint_theme_sample() - self.set_highlight_target() - - def var_changed_builtin_name(self, *params): - """Process new builtin theme selection. - - Add the changed theme's name to the changed_items and recreate - the sample with the values from the selected theme. - """ - old_themes = ('IDLE Classic', 'IDLE New') - value = self.builtin_name.get() - if value not in old_themes: - if idleConf.GetOption('main', 'Theme', 'name') not in old_themes: - changes.add_option('main', 'Theme', 'name', old_themes[0]) - changes.add_option('main', 'Theme', 'name2', value) - self.theme_message['text'] = 'New theme, see Help' - else: - changes.add_option('main', 'Theme', 'name', value) - changes.add_option('main', 'Theme', 'name2', '') - self.theme_message['text'] = '' - self.paint_theme_sample() - - def var_changed_custom_name(self, *params): - """Process new custom theme selection. - - If a new custom theme is selected, add the name to the - changed_items and apply the theme to the sample. - """ - value = self.custom_name.get() - if value != '- no custom themes -': - changes.add_option('main', 'Theme', 'name', value) - self.paint_theme_sample() - - def var_changed_theme_source(self, *params): - """Process toggle between builtin and custom theme. - - Update the default toggle value and apply the newly - selected theme type. - """ - value = self.theme_source.get() - changes.add_option('main', 'Theme', 'default', value) - if value: - self.var_changed_builtin_name() - else: - self.var_changed_custom_name() - - def var_changed_color(self, *params): - "Process change to color choice." - self.on_new_color_set() - - def var_changed_highlight_target(self, *params): - "Process selection of new target tag for highlighting." - self.set_highlight_target() - - def set_theme_type(self): - """Set available screen options based on builtin or custom theme. - - Attributes accessed: - theme_source - - Attributes updated: - builtinlist - customlist - button_delete_custom - custom_theme_on - - Called from: - handler for builtin_theme_on and custom_theme_on - delete_custom - create_new - load_theme_cfg - """ - if self.theme_source.get(): - self.builtinlist['state'] = 'normal' - self.customlist['state'] = 'disabled' - self.button_delete_custom.state(('disabled',)) - else: - self.builtinlist['state'] = 'disabled' - self.custom_theme_on.state(('!disabled',)) - self.customlist['state'] = 'normal' - self.button_delete_custom.state(('!disabled',)) - - def get_color(self): - """Handle button to select a new color for the target tag. - - If a new color is selected while using a builtin theme, a - name must be supplied to create a custom theme. - - Attributes accessed: - highlight_target - frame_color_set - theme_source - - Attributes updated: - color - - Methods: - get_new_theme_name - create_new - """ - target = self.highlight_target.get() - prev_color = self.style.lookup(self.frame_color_set['style'], - 'background') - rgbTuplet, color_string = tkColorChooser.askcolor( - parent=self, title='Pick new color for : '+target, - initialcolor=prev_color) - if color_string and (color_string != prev_color): - # User didn't cancel and they chose a new color. - if self.theme_source.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.') - new_theme = self.get_new_theme_name(message) - if not new_theme: # User cancelled custom theme creation. - return - else: # Create new custom theme based on previously active theme. - self.create_new(new_theme) - self.color.set(color_string) - else: # Current theme is user defined. - self.color.set(color_string) - - def on_new_color_set(self): - "Display sample of new color selection on the dialog." - new_color = self.color.get() - self.style.configure('frame_color_set.TFrame', background=new_color) - plane = 'foreground' if self.fg_bg_toggle.get() else 'background' - sample_element = self.theme_elements[self.highlight_target.get()][0] - self.highlight_sample.tag_config(sample_element, **{plane: new_color}) - theme = self.custom_name.get() - theme_element = sample_element + '-' + plane - changes.add_option('highlight', theme, theme_element, new_color) - - def get_new_theme_name(self, message): - "Return name of new theme from query popup." - used_names = (idleConf.GetSectionList('user', 'highlight') + - idleConf.GetSectionList('default', 'highlight')) - new_theme = SectionName( - self, 'New Custom Theme', message, used_names).result - return new_theme - - def save_as_new_theme(self): - """Prompt for new theme name and create the theme. - - Methods: - get_new_theme_name - create_new - """ - new_theme_name = self.get_new_theme_name('New Theme Name:') - if new_theme_name: - self.create_new(new_theme_name) - - def create_new(self, new_theme_name): - """Create a new custom theme with the given name. - - Create the new theme based on the previously active theme - with the current changes applied. Once it is saved, then - activate the new theme. - - Attributes accessed: - builtin_name - custom_name - - Attributes updated: - customlist - theme_source - - Method: - save_new - set_theme_type - """ - if self.theme_source.get(): - theme_type = 'default' - theme_name = self.builtin_name.get() - else: - theme_type = 'user' - theme_name = self.custom_name.get() - new_theme = idleConf.GetThemeDict(theme_type, theme_name) - # Apply any of the old theme's unsaved changes to the new theme. - if theme_name in changes['highlight']: - theme_changes = changes['highlight'][theme_name] - for element in theme_changes: - new_theme[element] = theme_changes[element] - # Save the new theme. - self.save_new(new_theme_name, new_theme) - # Change GUI over to the new theme. - custom_theme_list = idleConf.GetSectionList('user', 'highlight') - custom_theme_list.sort() - self.customlist.SetMenu(custom_theme_list, new_theme_name) - self.theme_source.set(0) - self.set_theme_type() - - def set_highlight_target(self): - """Set fg/bg toggle and color based on highlight tag target. - - Instance variables accessed: - highlight_target - - Attributes updated: - fg_on - bg_on - fg_bg_toggle - - Methods: - set_color_sample - - Called from: - var_changed_highlight_target - load_theme_cfg - """ - if self.highlight_target.get() == 'Cursor': # bg not possible - self.fg_on.state(('disabled',)) - self.bg_on.state(('disabled',)) - self.fg_bg_toggle.set(1) - else: # Both fg and bg can be set. - self.fg_on.state(('!disabled',)) - self.bg_on.state(('!disabled',)) - self.fg_bg_toggle.set(1) - self.set_color_sample() - - def set_color_sample_binding(self, *args): - """Change color sample based on foreground/background toggle. - - Methods: - set_color_sample - """ - self.set_color_sample() - - def set_color_sample(self): - """Set the color of the frame background to reflect the selected target. - - Instance variables accessed: - theme_elements - highlight_target - fg_bg_toggle - highlight_sample - - Attributes updated: - frame_color_set - """ - # Set the color sample area. - tag = self.theme_elements[self.highlight_target.get()][0] - plane = 'foreground' if self.fg_bg_toggle.get() else 'background' - color = self.highlight_sample.tag_cget(tag, plane) - self.style.configure('frame_color_set.TFrame', background=color) - - def paint_theme_sample(self): - """Apply the theme colors to each element tag in the sample text. - - Instance attributes accessed: - theme_elements - theme_source - builtin_name - custom_name - - Attributes updated: - highlight_sample: Set the tag elements to the theme. - - Methods: - set_color_sample - - Called from: - var_changed_builtin_name - var_changed_custom_name - load_theme_cfg - """ - if self.theme_source.get(): # Default theme - theme = self.builtin_name.get() - else: # User theme - theme = self.custom_name.get() - for element_title in self.theme_elements: - element = self.theme_elements[element_title][0] - colors = idleConf.GetHighlight(theme, element) - if element == 'cursor': # Cursor sample needs special painting. - colors['background'] = idleConf.GetHighlight( - theme, 'normal')['background'] - # Handle any unsaved changes to this theme. - if theme in changes['highlight']: - theme_dict = changes['highlight'][theme] - if element + '-foreground' in theme_dict: - colors['foreground'] = theme_dict[element + '-foreground'] - if element + '-background' in theme_dict: - colors['background'] = theme_dict[element + '-background'] - self.highlight_sample.tag_config(element, **colors) - self.set_color_sample() - - def save_new(self, theme_name, theme): - """Save a newly created theme to idleConf. - - theme_name - string, the name of the new theme - theme - dictionary containing the new theme - """ - if not idleConf.userCfg['highlight'].has_section(theme_name): - idleConf.userCfg['highlight'].add_section(theme_name) - for element in theme: - value = theme[element] - idleConf.userCfg['highlight'].SetOption(theme_name, element, value) - - def askyesno(self, *args, **kwargs): - # Make testing easier. Could change implementation. - return messagebox.askyesno(*args, **kwargs) - - def delete_custom(self): - """Handle event to delete custom theme. - - The current theme is deactivated and the default theme is - activated. The custom theme is permanently removed from - the config file. - - Attributes accessed: - custom_name - - Attributes updated: - custom_theme_on - customlist - theme_source - builtin_name - - Methods: - deactivate_current_config - save_all_changed_extensions - activate_config_changes - set_theme_type - """ - theme_name = self.custom_name.get() - delmsg = 'Are you sure you wish to delete the theme %r ?' - if not self.askyesno( - 'Delete Theme', delmsg % theme_name, parent=self): - return - self.cd.deactivate_current_config() - # Remove theme from changes, config, and file. - changes.delete_section('highlight', theme_name) - # Reload user theme list. - item_list = idleConf.GetSectionList('user', 'highlight') - item_list.sort() - if not item_list: - self.custom_theme_on.state(('disabled',)) - self.customlist.SetMenu(item_list, '- no custom themes -') - else: - self.customlist.SetMenu(item_list, item_list[0]) - # Revert to default theme. - self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default')) - self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name')) - # User can't back out of these changes, they must be applied now. - changes.save_all() - self.cd.save_all_changed_extensions() - self.cd.activate_config_changes() - self.set_theme_type() - - -class KeysPage(Frame): - - def __init__(self, master): - super().__init__(master) - self.cd = master.master - self.create_page_keys() - self.load_key_cfg() - - def create_page_keys(self): - """Return frame of widgets for Keys tab. - - Enable users to provisionally change both individual and sets of - keybindings (shortcut keys). Except for features implemented as - extensions, keybindings are stored in complete sets called - keysets. Built-in keysets in idlelib/config-keys.def are fixed - as far as the dialog is concerned. Any keyset can be used as the - base for a new custom keyset, stored in .idlerc/config-keys.cfg. - - Function load_key_cfg() initializes tk variables and keyset - lists and calls load_keys_list for the current keyset. - Radiobuttons builtin_keyset_on and custom_keyset_on toggle var - keyset_source, which controls if the current set of keybindings - are from a builtin or custom keyset. DynOptionMenus builtinlist - and customlist contain lists of the builtin and custom keysets, - respectively, and the current item from each list is stored in - vars builtin_name and custom_name. - - Button delete_custom_keys invokes delete_custom_keys() to delete - a custom keyset from idleConf.userCfg['keys'] and changes. Button - save_custom_keys invokes save_as_new_key_set() which calls - get_new_keys_name() and create_new_key_set() to save a custom keyset - and its keybindings to idleConf.userCfg['keys']. - - Listbox bindingslist contains all of the keybindings for the - selected keyset. The keybindings are loaded in load_keys_list() - and are pairs of (event, [keys]) where keys can be a list - of one or more key combinations to bind to the same event. - Mouse button 1 click invokes on_bindingslist_select(), which - allows button_new_keys to be clicked. - - So, an item is selected in listbindings, which activates - button_new_keys, and clicking button_new_keys calls function - get_new_keys(). Function get_new_keys() gets the key mappings from the - current keyset for the binding event item that was selected. The - function then displays another dialog, GetKeysDialog, with the - selected binding event and current keys and allows new key sequences - to be entered for that binding event. If the keys aren't - changed, nothing happens. If the keys are changed and the keyset - is a builtin, function get_new_keys_name() will be called - for input of a custom keyset name. If no name is given, then the - change to the keybinding will abort and no updates will be made. If - a custom name is entered in the prompt or if the current keyset was - already custom (and thus didn't require a prompt), then - idleConf.userCfg['keys'] is updated in function create_new_key_set() - with the change to the event binding. The item listing in bindingslist - is updated with the new keys. Var keybinding is also set which invokes - the callback function, var_changed_keybinding, to add the change to - the 'keys' or 'extensions' changes tracker based on the binding type. - - Tk Variables: - keybinding: Action/key bindings. - - Methods: - load_keys_list: Reload active set. - create_new_key_set: Combine active keyset and changes. - set_keys_type: Command for keyset_source. - save_new_key_set: Save to idleConf.userCfg['keys'] (is function). - deactivate_current_config: Remove keys bindings in editors. - - Widgets for KeysPage(frame): (*) widgets bound to self - frame_key_sets: LabelFrame - frames[0]: Frame - (*)builtin_keyset_on: Radiobutton - var keyset_source - (*)custom_keyset_on: Radiobutton - var keyset_source - (*)builtinlist: DynOptionMenu - var builtin_name, - func keybinding_selected - (*)customlist: DynOptionMenu - var custom_name, - func keybinding_selected - (*)keys_message: Label - frames[1]: Frame - (*)button_delete_custom_keys: Button - delete_custom_keys - (*)button_save_custom_keys: Button - save_as_new_key_set - frame_custom: LabelFrame - frame_target: Frame - target_title: Label - scroll_target_y: Scrollbar - scroll_target_x: Scrollbar - (*)bindingslist: ListBox - on_bindingslist_select - (*)button_new_keys: Button - get_new_keys & ..._name - """ - self.builtin_name = tracers.add( - StringVar(self), self.var_changed_builtin_name) - self.custom_name = tracers.add( - StringVar(self), self.var_changed_custom_name) - self.keyset_source = tracers.add( - BooleanVar(self), self.var_changed_keyset_source) - self.keybinding = tracers.add( - StringVar(self), self.var_changed_keybinding) - - # Create widgets: - # body and section frames. - frame_custom = LabelFrame( - self, borderwidth=2, relief=GROOVE, - text=' Custom Key Bindings ') - frame_key_sets = LabelFrame( - self, borderwidth=2, relief=GROOVE, text=' Key Set ') - # frame_custom. - frame_target = Frame(frame_custom) - target_title = Label(frame_target, text='Action - Key(s)') - scroll_target_y = Scrollbar(frame_target) - scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL) - self.bindingslist = Listbox( - frame_target, takefocus=FALSE, exportselection=FALSE) - self.bindingslist.bind('<ButtonRelease-1>', - self.on_bindingslist_select) - scroll_target_y['command'] = self.bindingslist.yview - scroll_target_x['command'] = self.bindingslist.xview - self.bindingslist['yscrollcommand'] = scroll_target_y.set - self.bindingslist['xscrollcommand'] = scroll_target_x.set - self.button_new_keys = Button( - frame_custom, text='Get New Keys for Selection', - command=self.get_new_keys, state='disabled') - # frame_key_sets. - frames = [Frame(frame_key_sets, padding=2, borderwidth=0) - for i in range(2)] - self.builtin_keyset_on = Radiobutton( - frames[0], variable=self.keyset_source, value=1, - command=self.set_keys_type, text='Use a Built-in Key Set') - self.custom_keyset_on = Radiobutton( - frames[0], variable=self.keyset_source, value=0, - command=self.set_keys_type, text='Use a Custom Key Set') - self.builtinlist = DynOptionMenu( - frames[0], self.builtin_name, None, command=None) - self.customlist = DynOptionMenu( - frames[0], self.custom_name, None, command=None) - self.button_delete_custom_keys = Button( - frames[1], text='Delete Custom Key Set', - command=self.delete_custom_keys) - self.button_save_custom_keys = Button( - frames[1], text='Save as New Custom Key Set', - command=self.save_as_new_key_set) - self.keys_message = Label(frames[0], borderwidth=2) - - # Pack widgets: - # body. - frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH) - frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH) - # frame_custom. - self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5) - frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) - # frame_target. - frame_target.columnconfigure(0, weight=1) - frame_target.rowconfigure(1, weight=1) - target_title.grid(row=0, column=0, columnspan=2, sticky=W) - self.bindingslist.grid(row=1, column=0, sticky=NSEW) - scroll_target_y.grid(row=1, column=1, sticky=NS) - scroll_target_x.grid(row=2, column=0, sticky=EW) - # frame_key_sets. - self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS) - self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS) - self.builtinlist.grid(row=0, column=1, sticky=NSEW) - self.customlist.grid(row=1, column=1, sticky=NSEW) - self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5) - self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2) - self.button_save_custom_keys.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) - - def load_key_cfg(self): - "Load current configuration settings for the keybinding options." - # Set current keys type radiobutton. - self.keyset_source.set(idleConf.GetOption( - 'main', 'Keys', 'default', type='bool', default=1)) - # Set current keys. - current_option = idleConf.CurrentKeys() - # Load available keyset option menus. - if self.keyset_source.get(): # Default theme selected. - item_list = idleConf.GetSectionList('default', 'keys') - item_list.sort() - self.builtinlist.SetMenu(item_list, current_option) - item_list = idleConf.GetSectionList('user', 'keys') - item_list.sort() - if not item_list: - self.custom_keyset_on.state(('disabled',)) - self.custom_name.set('- no custom keys -') - else: - self.customlist.SetMenu(item_list, item_list[0]) - else: # User key set selected. - item_list = idleConf.GetSectionList('user', 'keys') - item_list.sort() - self.customlist.SetMenu(item_list, current_option) - item_list = idleConf.GetSectionList('default', 'keys') - item_list.sort() - self.builtinlist.SetMenu(item_list, idleConf.default_keys()) - self.set_keys_type() - # Load keyset element list. - keyset_name = idleConf.CurrentKeys() - self.load_keys_list(keyset_name) - - def var_changed_builtin_name(self, *params): - "Process selection of builtin key set." - old_keys = ( - 'IDLE Classic Windows', - 'IDLE Classic Unix', - 'IDLE Classic Mac', - 'IDLE Classic OSX', - ) - value = self.builtin_name.get() - if value not in old_keys: - if idleConf.GetOption('main', 'Keys', 'name') not in old_keys: - changes.add_option('main', 'Keys', 'name', old_keys[0]) - changes.add_option('main', 'Keys', 'name2', value) - self.keys_message['text'] = 'New key set, see Help' - else: - changes.add_option('main', 'Keys', 'name', value) - changes.add_option('main', 'Keys', 'name2', '') - self.keys_message['text'] = '' - self.load_keys_list(value) - - def var_changed_custom_name(self, *params): - "Process selection of custom key set." - value = self.custom_name.get() - if value != '- no custom keys -': - changes.add_option('main', 'Keys', 'name', value) - self.load_keys_list(value) - - def var_changed_keyset_source(self, *params): - "Process toggle between builtin key set and custom key set." - value = self.keyset_source.get() - changes.add_option('main', 'Keys', 'default', value) - if value: - self.var_changed_builtin_name() - else: - self.var_changed_custom_name() - - def var_changed_keybinding(self, *params): - "Store change to a keybinding." - value = self.keybinding.get() - key_set = self.custom_name.get() - event = self.bindingslist.get(ANCHOR).split()[0] - if idleConf.IsCoreBinding(event): - changes.add_option('keys', key_set, event, value) - else: # Event is an extension binding. - ext_name = idleConf.GetExtnNameForEvent(event) - ext_keybind_section = ext_name + '_cfgBindings' - changes.add_option('extensions', ext_keybind_section, event, value) - - def set_keys_type(self): - "Set available screen options based on builtin or custom key set." - if self.keyset_source.get(): - self.builtinlist['state'] = 'normal' - self.customlist['state'] = 'disabled' - self.button_delete_custom_keys.state(('disabled',)) - else: - self.builtinlist['state'] = 'disabled' - self.custom_keyset_on.state(('!disabled',)) - self.customlist['state'] = 'normal' - self.button_delete_custom_keys.state(('!disabled',)) - - def get_new_keys(self): - """Handle event to change key binding for selected line. - - A selection of a key/binding in the list of current - bindings pops up a dialog to enter a new binding. If - the current key set is builtin and a binding has - changed, then a name for a custom key set needs to be - entered for the change to be applied. - """ - list_index = self.bindingslist.index(ANCHOR) - binding = self.bindingslist.get(list_index) - bind_name = binding.split()[0] - if self.keyset_source.get(): - current_key_set_name = self.builtin_name.get() - else: - current_key_set_name = self.custom_name.get() - current_bindings = idleConf.GetCurrentKeySet() - if current_key_set_name in changes['keys']: # unsaved changes - key_set_changes = changes['keys'][current_key_set_name] - for event in key_set_changes: - current_bindings[event] = key_set_changes[event].split() - current_key_sequences = list(current_bindings.values()) - new_keys = GetKeysDialog(self, 'Get New Keys', bind_name, - current_key_sequences).result - if new_keys: - if self.keyset_source.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.') - new_keyset = self.get_new_keys_name(message) - if not new_keyset: # User cancelled custom key set creation. - self.bindingslist.select_set(list_index) - self.bindingslist.select_anchor(list_index) - return - else: # Create new custom key set based on previously active key set. - self.create_new_key_set(new_keyset) - self.bindingslist.delete(list_index) - self.bindingslist.insert(list_index, bind_name+' - '+new_keys) - self.bindingslist.select_set(list_index) - self.bindingslist.select_anchor(list_index) - self.keybinding.set(new_keys) - else: - self.bindingslist.select_set(list_index) - self.bindingslist.select_anchor(list_index) - - def get_new_keys_name(self, message): - "Return new key set name from query popup." - used_names = (idleConf.GetSectionList('user', 'keys') + - idleConf.GetSectionList('default', 'keys')) - new_keyset = SectionName( - self, 'New Custom Key Set', message, used_names).result - return new_keyset - - def save_as_new_key_set(self): - "Prompt for name of new key set and save changes using that name." - new_keys_name = self.get_new_keys_name('New Key Set Name:') - if new_keys_name: - self.create_new_key_set(new_keys_name) - - def on_bindingslist_select(self, event): - "Activate button to assign new keys to selected action." - self.button_new_keys.state(('!disabled',)) - - def create_new_key_set(self, new_key_set_name): - """Create a new custom key set with the given name. - - Copy the bindings/keys from the previously active keyset - to the new keyset and activate the new custom keyset. - """ - if self.keyset_source.get(): - prev_key_set_name = self.builtin_name.get() - else: - prev_key_set_name = self.custom_name.get() - prev_keys = idleConf.GetCoreKeys(prev_key_set_name) - new_keys = {} - for event in prev_keys: # Add key set to changed items. - event_name = event[2:-2] # Trim off the angle brackets. - binding = ' '.join(prev_keys[event]) - new_keys[event_name] = binding - # Handle any unsaved changes to prev key set. - if prev_key_set_name in changes['keys']: - key_set_changes = changes['keys'][prev_key_set_name] - for event in key_set_changes: - new_keys[event] = key_set_changes[event] - # Save the new key set. - self.save_new_key_set(new_key_set_name, new_keys) - # Change GUI over to the new key set. - custom_key_list = idleConf.GetSectionList('user', 'keys') - custom_key_list.sort() - self.customlist.SetMenu(custom_key_list, new_key_set_name) - self.keyset_source.set(0) - self.set_keys_type() - - def load_keys_list(self, keyset_name): - """Reload the list of action/key binding pairs for the active key set. - - An action/key binding can be selected to change the key binding. - """ - reselect = False - if self.bindingslist.curselection(): - reselect = True - list_index = self.bindingslist.index(ANCHOR) - keyset = idleConf.GetKeySet(keyset_name) - bind_names = list(keyset.keys()) - bind_names.sort() - self.bindingslist.delete(0, END) - for bind_name in bind_names: - key = ' '.join(keyset[bind_name]) - bind_name = bind_name[2:-2] # Trim off the angle brackets. - if keyset_name in changes['keys']: - # Handle any unsaved changes to this key set. - if bind_name in changes['keys'][keyset_name]: - key = changes['keys'][keyset_name][bind_name] - self.bindingslist.insert(END, bind_name+' - '+key) - if reselect: - self.bindingslist.see(list_index) - self.bindingslist.select_set(list_index) - self.bindingslist.select_anchor(list_index) - - @staticmethod - def save_new_key_set(keyset_name, keyset): - """Save a newly created core key set. - - Add keyset to idleConf.userCfg['keys'], not to disk. - If the keyset doesn't exist, it is created. The - binding/keys are taken from the keyset argument. - - keyset_name - string, the name of the new key set - keyset - dictionary containing the new keybindings - """ - if not idleConf.userCfg['keys'].has_section(keyset_name): - idleConf.userCfg['keys'].add_section(keyset_name) - for event in keyset: - value = keyset[event] - idleConf.userCfg['keys'].SetOption(keyset_name, event, value) - - def askyesno(self, *args, **kwargs): - # Make testing easier. Could change implementation. - return messagebox.askyesno(*args, **kwargs) - - def delete_custom_keys(self): - """Handle event to delete a custom key set. - - Applying the delete deactivates the current configuration and - reverts to the default. The custom key set is permanently - deleted from the config file. - """ - keyset_name = self.custom_name.get() - delmsg = 'Are you sure you wish to delete the key set %r ?' - if not self.askyesno( - 'Delete Key Set', delmsg % keyset_name, parent=self): - return - self.cd.deactivate_current_config() - # Remove key set from changes, config, and file. - changes.delete_section('keys', keyset_name) - # Reload user key set list. - item_list = idleConf.GetSectionList('user', 'keys') - item_list.sort() - if not item_list: - self.custom_keyset_on.state(('disabled',)) - self.customlist.SetMenu(item_list, '- no custom keys -') - else: - self.customlist.SetMenu(item_list, item_list[0]) - # Revert to default key set. - self.keyset_source.set(idleConf.defaultCfg['main'] - .Get('Keys', 'default')) - self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name') - or idleConf.default_keys()) - # User can't back out of these changes, they must be applied now. - changes.save_all() - self.cd.save_all_changed_extensions() - self.cd.activate_config_changes() - self.set_keys_type() - - -class GenPage(Frame): - - def __init__(self, master): - super().__init__(master) - - self.init_validators() - self.create_page_general() - self.load_general_cfg() - - def init_validators(self): - digits_or_empty_re = re.compile(r'[0-9]*') - def is_digits_or_empty(s): - "Return 's is blank or contains only digits'" - return digits_or_empty_re.fullmatch(s) is not None - self.digits_only = (self.register(is_digits_or_empty), '%P',) - - def create_page_general(self): - """Return frame of widgets for General tab. - - Enable users to provisionally change general options. Function - load_general_cfg initializes tk variables and helplist using - idleConf. Radiobuttons startup_shell_on and startup_editor_on - set var startup_edit. Radiobuttons save_ask_on and save_auto_on - set var autosave. Entry boxes win_width_int and win_height_int - set var win_width and win_height. Setting var_name invokes the - default callback that adds option to changes. - - Helplist: load_general_cfg loads list user_helplist with - name, position pairs and copies names to listbox helplist. - Clicking a name invokes help_source selected. Clicking - button_helplist_name invokes helplist_item_name, which also - changes user_helplist. These functions all call - set_add_delete_state. All but load call update_help_changes to - rewrite changes['main']['HelpFiles']. - - Widgets for GenPage(Frame): (*) widgets bound to self - frame_window: LabelFrame - frame_run: Frame - startup_title: Label - (*)startup_editor_on: Radiobutton - startup_edit - (*)startup_shell_on: Radiobutton - startup_edit - frame_win_size: Frame - win_size_title: Label - win_width_title: Label - (*)win_width_int: Entry - win_width - win_height_title: Label - (*)win_height_int: Entry - win_height - frame_cursor_blink: Frame - cursor_blink_title: Label - (*)cursor_blink_bool: Checkbutton - cursor_blink - frame_autocomplete: Frame - auto_wait_title: Label - (*)auto_wait_int: Entry - autocomplete_wait - frame_paren1: Frame - paren_style_title: Label - (*)paren_style_type: OptionMenu - paren_style - frame_paren2: Frame - paren_time_title: Label - (*)paren_flash_time: Entry - flash_delay - (*)bell_on: Checkbutton - paren_bell - frame_editor: LabelFrame - frame_save: Frame - run_save_title: Label - (*)save_ask_on: Radiobutton - autosave - (*)save_auto_on: Radiobutton - autosave - frame_format: Frame - format_width_title: Label - (*)format_width_int: Entry - format_width - frame_line_numbers_default: Frame - line_numbers_default_title: Label - (*)line_numbers_default_bool: Checkbutton - line_numbers_default - frame_context: Frame - context_title: Label - (*)context_int: Entry - context_lines - frame_shell: LabelFrame - frame_auto_squeeze_min_lines: Frame - auto_squeeze_min_lines_title: Label - (*)auto_squeeze_min_lines_int: Entry - auto_squeeze_min_lines - frame_help: LabelFrame - frame_helplist: Frame - frame_helplist_buttons: Frame - (*)button_helplist_edit - (*)button_helplist_add - (*)button_helplist_remove - (*)helplist: ListBox - scroll_helplist: Scrollbar - """ - # Integer values need StringVar because int('') raises. - self.startup_edit = tracers.add( - IntVar(self), ('main', 'General', 'editor-on-startup')) - self.win_width = tracers.add( - StringVar(self), ('main', 'EditorWindow', 'width')) - self.win_height = tracers.add( - StringVar(self), ('main', 'EditorWindow', 'height')) - self.cursor_blink = tracers.add( - BooleanVar(self), ('main', 'EditorWindow', 'cursor-blink')) - self.autocomplete_wait = tracers.add( - StringVar(self), ('extensions', 'AutoComplete', 'popupwait')) - self.paren_style = tracers.add( - StringVar(self), ('extensions', 'ParenMatch', 'style')) - self.flash_delay = tracers.add( - StringVar(self), ('extensions', 'ParenMatch', 'flash-delay')) - self.paren_bell = tracers.add( - BooleanVar(self), ('extensions', 'ParenMatch', 'bell')) - - self.auto_squeeze_min_lines = tracers.add( - StringVar(self), ('main', 'PyShell', 'auto-squeeze-min-lines')) - - self.autosave = tracers.add( - IntVar(self), ('main', 'General', 'autosave')) - self.format_width = tracers.add( - StringVar(self), ('extensions', 'FormatParagraph', 'max-width')) - self.line_numbers_default = tracers.add( - BooleanVar(self), - ('main', 'EditorWindow', 'line-numbers-default')) - self.context_lines = tracers.add( - StringVar(self), ('extensions', 'CodeContext', 'maxlines')) - - # Create widgets: - # Section frames. - frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE, - text=' Window Preferences') - frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE, - text=' Editor Preferences') - frame_shell = LabelFrame(self, borderwidth=2, relief=GROOVE, - text=' Shell Preferences') - frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE, - text=' Additional Help Sources ') - # Frame_window. - frame_run = Frame(frame_window, borderwidth=0) - startup_title = Label(frame_run, text='At Startup') - self.startup_editor_on = Radiobutton( - frame_run, variable=self.startup_edit, value=1, - text="Open Edit Window") - self.startup_shell_on = Radiobutton( - frame_run, variable=self.startup_edit, value=0, - text='Open Shell Window') - - frame_win_size = Frame(frame_window, borderwidth=0) - win_size_title = Label( - frame_win_size, text='Initial Window Size (in characters)') - win_width_title = Label(frame_win_size, text='Width') - self.win_width_int = Entry( - frame_win_size, textvariable=self.win_width, width=3, - validatecommand=self.digits_only, validate='key', - ) - win_height_title = Label(frame_win_size, text='Height') - self.win_height_int = Entry( - frame_win_size, textvariable=self.win_height, width=3, - validatecommand=self.digits_only, validate='key', - ) - - frame_cursor_blink = Frame(frame_window, borderwidth=0) - cursor_blink_title = Label(frame_cursor_blink, text='Cursor Blink') - self.cursor_blink_bool = Checkbutton(frame_cursor_blink, - variable=self.cursor_blink, width=1) - - frame_autocomplete = Frame(frame_window, borderwidth=0,) - auto_wait_title = Label(frame_autocomplete, - text='Completions Popup Wait (milliseconds)') - self.auto_wait_int = Entry(frame_autocomplete, width=6, - textvariable=self.autocomplete_wait, - validatecommand=self.digits_only, - validate='key', - ) - - frame_paren1 = Frame(frame_window, borderwidth=0) - paren_style_title = Label(frame_paren1, text='Paren Match Style') - self.paren_style_type = OptionMenu( - frame_paren1, self.paren_style, 'expression', - "opener","parens","expression") - frame_paren2 = Frame(frame_window, borderwidth=0) - paren_time_title = Label( - frame_paren2, text='Time Match Displayed (milliseconds)\n' - '(0 is until next input)') - self.paren_flash_time = Entry( - frame_paren2, textvariable=self.flash_delay, width=6) - self.bell_on = Checkbutton( - frame_paren2, text="Bell on Mismatch", variable=self.paren_bell) - - # Frame_editor. - frame_save = Frame(frame_editor, borderwidth=0) - run_save_title = Label(frame_save, text='At Start of Run (F5) ') - self.save_ask_on = Radiobutton( - frame_save, variable=self.autosave, value=0, - text="Prompt to Save") - self.save_auto_on = Radiobutton( - frame_save, variable=self.autosave, value=1, - text='No Prompt') - - frame_format = Frame(frame_editor, borderwidth=0) - format_width_title = Label(frame_format, - text='Format Paragraph Max Width') - self.format_width_int = Entry( - frame_format, textvariable=self.format_width, width=4, - validatecommand=self.digits_only, validate='key', - ) - - frame_line_numbers_default = Frame(frame_editor, borderwidth=0) - line_numbers_default_title = Label( - frame_line_numbers_default, text='Show line numbers in new windows') - self.line_numbers_default_bool = Checkbutton( - frame_line_numbers_default, - variable=self.line_numbers_default, - width=1) - - frame_context = Frame(frame_editor, borderwidth=0) - context_title = Label(frame_context, text='Max Context Lines :') - self.context_int = Entry( - frame_context, textvariable=self.context_lines, width=3, - validatecommand=self.digits_only, validate='key', - ) - - # Frame_shell. - frame_auto_squeeze_min_lines = Frame(frame_shell, borderwidth=0) - auto_squeeze_min_lines_title = Label(frame_auto_squeeze_min_lines, - text='Auto-Squeeze Min. Lines:') - self.auto_squeeze_min_lines_int = Entry( - frame_auto_squeeze_min_lines, width=4, - textvariable=self.auto_squeeze_min_lines, - validatecommand=self.digits_only, validate='key', - ) - - # frame_help. - frame_helplist = Frame(frame_help) - frame_helplist_buttons = Frame(frame_helplist) - self.helplist = Listbox( - frame_helplist, height=5, takefocus=True, - exportselection=FALSE) - scroll_helplist = Scrollbar(frame_helplist) - scroll_helplist['command'] = self.helplist.yview - self.helplist['yscrollcommand'] = scroll_helplist.set - self.helplist.bind('<ButtonRelease-1>', self.help_source_selected) - self.button_helplist_edit = Button( - frame_helplist_buttons, text='Edit', state='disabled', - width=8, command=self.helplist_item_edit) - self.button_helplist_add = Button( - frame_helplist_buttons, text='Add', - width=8, command=self.helplist_item_add) - self.button_helplist_remove = Button( - frame_helplist_buttons, text='Remove', state='disabled', - width=8, command=self.helplist_item_remove) - - # Pack widgets: - # Body. - frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) - frame_editor.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) - frame_shell.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) - frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) - # frame_run. - frame_run.pack(side=TOP, padx=5, pady=0, fill=X) - startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5) - self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) - self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) - # frame_win_size. - frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X) - win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5) - self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5) - win_height_title.pack(side=RIGHT, anchor=E, pady=5) - self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5) - win_width_title.pack(side=RIGHT, anchor=E, pady=5) - # frame_cursor_blink. - frame_cursor_blink.pack(side=TOP, padx=5, pady=0, fill=X) - cursor_blink_title.pack(side=LEFT, anchor=W, padx=5, pady=5) - self.cursor_blink_bool.pack(side=LEFT, padx=5, pady=5) - # frame_autocomplete. - frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X) - auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5) - self.auto_wait_int.pack(side=TOP, padx=10, pady=5) - # frame_paren. - frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X) - paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5) - self.paren_style_type.pack(side=TOP, padx=10, pady=5) - frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X) - paren_time_title.pack(side=LEFT, anchor=W, padx=5) - self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5) - self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5) - - # frame_save. - frame_save.pack(side=TOP, padx=5, pady=0, fill=X) - run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5) - self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) - self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) - # frame_format. - frame_format.pack(side=TOP, padx=5, pady=0, fill=X) - format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5) - self.format_width_int.pack(side=TOP, padx=10, pady=5) - # frame_line_numbers_default. - frame_line_numbers_default.pack(side=TOP, padx=5, pady=0, fill=X) - line_numbers_default_title.pack(side=LEFT, anchor=W, padx=5, pady=5) - self.line_numbers_default_bool.pack(side=LEFT, padx=5, pady=5) - # frame_context. - frame_context.pack(side=TOP, padx=5, pady=0, fill=X) - context_title.pack(side=LEFT, anchor=W, padx=5, pady=5) - self.context_int.pack(side=TOP, padx=5, pady=5) - - # frame_auto_squeeze_min_lines - frame_auto_squeeze_min_lines.pack(side=TOP, padx=5, pady=0, fill=X) - auto_squeeze_min_lines_title.pack(side=LEFT, anchor=W, padx=5, pady=5) - self.auto_squeeze_min_lines_int.pack(side=TOP, padx=5, pady=5) - - # frame_help. - frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y) - frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) - scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y) - self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH) - self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5) - self.button_helplist_add.pack(side=TOP, anchor=W) - self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5) - - def load_general_cfg(self): - "Load current configuration settings for the general options." - # Set variables for all windows. - self.startup_edit.set(idleConf.GetOption( - 'main', 'General', 'editor-on-startup', type='bool')) - self.win_width.set(idleConf.GetOption( - 'main', 'EditorWindow', 'width', type='int')) - self.win_height.set(idleConf.GetOption( - 'main', 'EditorWindow', 'height', type='int')) - self.cursor_blink.set(idleConf.GetOption( - 'main', 'EditorWindow', 'cursor-blink', type='bool')) - self.autocomplete_wait.set(idleConf.GetOption( - 'extensions', 'AutoComplete', 'popupwait', type='int')) - self.paren_style.set(idleConf.GetOption( - 'extensions', 'ParenMatch', 'style')) - self.flash_delay.set(idleConf.GetOption( - 'extensions', 'ParenMatch', 'flash-delay', type='int')) - self.paren_bell.set(idleConf.GetOption( - 'extensions', 'ParenMatch', 'bell')) - - # Set variables for editor windows. - self.autosave.set(idleConf.GetOption( - 'main', 'General', 'autosave', default=0, type='bool')) - self.format_width.set(idleConf.GetOption( - 'extensions', 'FormatParagraph', 'max-width', type='int')) - self.line_numbers_default.set(idleConf.GetOption( - 'main', 'EditorWindow', 'line-numbers-default', type='bool')) - self.context_lines.set(idleConf.GetOption( - 'extensions', 'CodeContext', 'maxlines', type='int')) - - # Set variables for shell windows. - self.auto_squeeze_min_lines.set(idleConf.GetOption( - 'main', 'PyShell', 'auto-squeeze-min-lines', type='int')) - - # Set additional help sources. - self.user_helplist = idleConf.GetAllExtraHelpSourcesList() - self.helplist.delete(0, 'end') - for help_item in self.user_helplist: - self.helplist.insert(END, help_item[0]) - self.set_add_delete_state() - - def help_source_selected(self, event): - "Handle event for selecting additional help." - self.set_add_delete_state() - - def set_add_delete_state(self): - "Toggle the state for the help list buttons based on list entries." - if self.helplist.size() < 1: # No entries in list. - self.button_helplist_edit.state(('disabled',)) - self.button_helplist_remove.state(('disabled',)) - else: # Some entries. - if self.helplist.curselection(): # There currently is a selection. - self.button_helplist_edit.state(('!disabled',)) - self.button_helplist_remove.state(('!disabled',)) - else: # There currently is not a selection. - self.button_helplist_edit.state(('disabled',)) - self.button_helplist_remove.state(('disabled',)) - - def helplist_item_add(self): - """Handle add button for the help list. - - Query for name and location of new help sources and add - them to the list. - """ - help_source = HelpSource(self, 'New Help Source').result - if help_source: - self.user_helplist.append(help_source) - self.helplist.insert(END, help_source[0]) - self.update_help_changes() - - def helplist_item_edit(self): - """Handle edit button for the help list. - - Query with existing help source information and update - config if the values are changed. - """ - item_index = self.helplist.index(ANCHOR) - help_source = self.user_helplist[item_index] - new_help_source = HelpSource( - self, 'Edit Help Source', - menuitem=help_source[0], - filepath=help_source[1], - ).result - if new_help_source and new_help_source != help_source: - self.user_helplist[item_index] = new_help_source - self.helplist.delete(item_index) - self.helplist.insert(item_index, new_help_source[0]) - self.update_help_changes() - self.set_add_delete_state() # Selected will be un-selected - - def helplist_item_remove(self): - """Handle remove button for the help list. - - Delete the help list item from config. - """ - item_index = self.helplist.index(ANCHOR) - del(self.user_helplist[item_index]) - self.helplist.delete(item_index) - self.update_help_changes() - self.set_add_delete_state() - - def update_help_changes(self): - "Clear and rebuild the HelpFiles section in changes" - changes['main']['HelpFiles'] = {} - for num in range(1, len(self.user_helplist) + 1): - changes.add_option( - 'main', 'HelpFiles', str(num), - ';'.join(self.user_helplist[num-1][:2])) - - -class VarTrace: - """Maintain Tk variables trace state.""" - - def __init__(self): - """Store Tk variables and callbacks. - - untraced: List of tuples (var, callback) - that do not have the callback attached - to the Tk var. - traced: List of tuples (var, callback) where - that callback has been attached to the var. - """ - self.untraced = [] - self.traced = [] - - def clear(self): - "Clear lists (for tests)." - # Call after all tests in a module to avoid memory leaks. - self.untraced.clear() - self.traced.clear() - - def add(self, var, callback): - """Add (var, callback) tuple to untraced list. - - Args: - var: Tk variable instance. - callback: Either function name to be used as a callback - or a tuple with IdleConf config-type, section, and - option names used in the default callback. - - Return: - Tk variable instance. - """ - if isinstance(callback, tuple): - callback = self.make_callback(var, callback) - self.untraced.append((var, callback)) - return var - - @staticmethod - def make_callback(var, config): - "Return default callback function to add values to changes instance." - def default_callback(*params): - "Add config values to changes instance." - changes.add_option(*config, var.get()) - return default_callback - - def attach(self): - "Attach callback to all vars that are not traced." - while self.untraced: - var, callback = self.untraced.pop() - var.trace_add('write', callback) - self.traced.append((var, callback)) - - def detach(self): - "Remove callback from traced vars." - while self.traced: - var, callback = self.traced.pop() - var.trace_remove('write', var.trace_info()[0][1]) - self.untraced.append((var, callback)) - - -tracers = VarTrace() - -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. [Cancel] only cancels changes made since the last save. -''' -help_pages = { - 'Fonts/Tabs':''' -Font sample: This shows what a selection of Basic Multilingual Plane -unicode characters look like for the current font selection. If the -selected font does not define a character, Tk attempts to find another -font that does. Substitute glyphs depend on what is available on a -particular system and will not necessarily have the same size as the -font selected. Line contains 20 characters up to Devanagari, 14 for -Tamil, and 10 for East Asia. - -Hebrew and Arabic letters should display right to left, starting with -alef, \u05d0 and \u0627. Arabic digits display left to right. The -Devanagari and Tamil lines start with digits. The East Asian lines -are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese -Hiragana and Katakana. - -You can edit the font sample. Changes remain until IDLE is closed. -''', - 'Highlights': ''' -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. -''', - 'Keys': ''' -Keys: -The IDLE Modern Unix key set is new in June 2016. It can only -be used with older IDLE releases if it is saved as a custom -key set, with a different name. -''', - 'General': ''' -General: - -AutoComplete: Popupwait is milliseconds to wait after key char, without -cursor movement, before popping up completion box. Key char is '.' after -identifier or a '/' (or '\\' on Windows) within a string. - -FormatParagraph: Max-width is max chars in lines after re-formatting. -Use with paragraphs in both strings and comment blocks. - -ParenMatch: Style indicates what is highlighted when closer is entered: -'opener' - opener '({[' corresponding to closer; 'parens' - both chars; -'expression' (default) - also everything in between. Flash-delay is how -long to highlight if cursor is not moved (0 means forever). - -CodeContext: Maxlines is the maximum number of code context lines to -display when Code Context is turned on for an editor window. - -Shell Preferences: Auto-Squeeze Min. Lines is the minimum number of lines -of output to automatically "squeeze". -''' -} - - -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, borderwidth=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__': - from unittest import main - main('idlelib.idle_test.test_configdialog', verbosity=2, exit=False) - - from idlelib.idle_test.htest import run - run(ConfigDialog) diff --git a/Lib/idlelib/dynoption.py b/Lib/idlelib/dynOptionMenuWidget.py index 9c6ffa4..beca9e2 100644 --- a/Lib/idlelib/dynoption.py +++ b/Lib/idlelib/dynOptionMenuWidget.py @@ -3,8 +3,7 @@ OptionMenu widget modified to allow dynamic menu reconfiguration and setting of highlightthickness """ import copy - -from tkinter import OptionMenu, _setit, StringVar, Button +from Tkinter import OptionMenu, _setit, StringVar, Button class DynOptionMenu(OptionMenu): """ @@ -13,7 +12,7 @@ class DynOptionMenu(OptionMenu): def __init__(self, master, variable, value, *values, **kwargs): # TODO copy value instead of whole dict kwargsCopy=copy.copy(kwargs) - if 'highlightthickness' in list(kwargs.keys()): + if 'highlightthickness' in kwargs.keys(): del(kwargs['highlightthickness']) OptionMenu.__init__(self, master, variable, value, *values, **kwargs) self.config(highlightthickness=kwargsCopy.get('highlightthickness')) @@ -35,12 +34,12 @@ class DynOptionMenu(OptionMenu): self.variable.set(value) def _dyn_option_menu(parent): # htest # - from tkinter import Toplevel # + StringVar, Button + from Tkinter import Toplevel - top = Toplevel(parent) + top = Toplevel() top.title("Tets dynamic option menu") - x, y = map(int, parent.geometry().split('+')[1:]) - top.geometry("200x100+%d+%d" % (x + 250, y + 175)) + top.geometry("200x100+%d+%d" % (parent.winfo_rootx() + 200, + parent.winfo_rooty() + 150)) top.focus_set() var = StringVar(top) diff --git a/Lib/idlelib/format.py b/Lib/idlelib/format.py deleted file mode 100644 index 4b57a18..0000000 --- a/Lib/idlelib/format.py +++ /dev/null @@ -1,426 +0,0 @@ -"""Format all or a selected region (line slice) of text. - -Region formatting options: paragraph, comment block, indent, deindent, -comment, uncomment, tabify, and untabify. - -File renamed from paragraph.py with functions added from editor.py. -""" -import re -from tkinter.messagebox import askyesno -from tkinter.simpledialog import askinteger -from idlelib.config import idleConf - - -class FormatParagraph: - """Format a paragraph, comment block, or selection to a max width. - - Does basic, standard text formatting, and also understands Python - comment blocks. Thus, for editing Python source code, this - extension is really only suitable for reformatting these comment - blocks or triple-quoted strings. - - Known problems with comment reformatting: - * If there is a selection marked, and the first line of the - selection is not complete, the block will probably not be detected - as comments, and will have the normal "text formatting" rules - applied. - * If a comment block has leading whitespace that mixes tabs and - spaces, they will not be considered part of the same block. - * Fancy comments, like this bulleted list, aren't handled :-) - """ - def __init__(self, editwin): - self.editwin = editwin - - @classmethod - def reload(cls): - cls.max_width = idleConf.GetOption('extensions', 'FormatParagraph', - 'max-width', type='int', default=72) - - def close(self): - self.editwin = None - - 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 - at the max width, starting from the beginning selection. - - 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. - """ - limit = self.max_width if limit is None else limit - text = self.editwin.text - first, last = self.editwin.get_selection_indices() - if first and last: - data = text.get(first, last) - comment_header = get_comment_header(data) - else: - first, last, comment_header, data = \ - find_paragraph(text, text.index("insert")) - if comment_header: - newdata = reformat_comment(data, limit, comment_header) - else: - newdata = reformat_paragraph(data, limit) - text.tag_remove("sel", "1.0", "end") - - if newdata != data: - text.mark_set("insert", first) - text.undo_block_start() - text.delete(first, last) - text.insert(first, newdata) - text.undo_block_stop() - else: - text.mark_set("insert", last) - text.see("insert") - return "break" - - -FormatParagraph.reload() - -def find_paragraph(text, mark): - """Returns the start/stop indices enclosing the paragraph that mark is in. - - Also returns the comment format string, if any, and paragraph of text - between the start/stop indices. - """ - lineno, col = map(int, mark.split(".")) - line = text.get("%d.0" % lineno, "%d.end" % lineno) - - # Look for start of next paragraph if the index passed in is a blank line - while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line): - lineno = lineno + 1 - line = text.get("%d.0" % lineno, "%d.end" % lineno) - first_lineno = lineno - comment_header = get_comment_header(line) - comment_header_len = len(comment_header) - - # Once start line found, search for end of paragraph (a blank line) - while get_comment_header(line)==comment_header and \ - not is_all_white(line[comment_header_len:]): - lineno = lineno + 1 - line = text.get("%d.0" % lineno, "%d.end" % lineno) - last = "%d.0" % lineno - - # Search back to beginning of paragraph (first blank line before) - lineno = first_lineno - 1 - line = text.get("%d.0" % lineno, "%d.end" % lineno) - while lineno > 0 and \ - get_comment_header(line)==comment_header and \ - not is_all_white(line[comment_header_len:]): - lineno = lineno - 1 - line = text.get("%d.0" % lineno, "%d.end" % lineno) - first = "%d.0" % (lineno+1) - - return first, last, comment_header, text.get(first, last) - -# This should perhaps be replaced with textwrap.wrap -def reformat_paragraph(data, limit): - """Return data reformatted to specified width (limit).""" - lines = data.split("\n") - i = 0 - n = len(lines) - while i < n and is_all_white(lines[i]): - i = i+1 - if i >= n: - return data - indent1 = get_indent(lines[i]) - if i+1 < n and not is_all_white(lines[i+1]): - indent2 = get_indent(lines[i+1]) - else: - indent2 = indent1 - new = lines[:i] - partial = indent1 - while i < n and not is_all_white(lines[i]): - # XXX Should take double space after period (etc.) into account - words = re.split(r"(\s+)", lines[i]) - for j in range(0, len(words), 2): - word = words[j] - if not word: - continue # Can happen when line ends in whitespace - if len((partial + word).expandtabs()) > limit and \ - partial != indent1: - new.append(partial.rstrip()) - partial = indent2 - partial = partial + word + " " - if j+1 < len(words) and words[j+1] != " ": - partial = partial + " " - i = i+1 - new.append(partial.rstrip()) - # XXX Should reformat remaining paragraphs as well - new.extend(lines[i:]) - return "\n".join(new) - -def reformat_comment(data, limit, comment_header): - """Return data reformatted to specified width with comment header.""" - - # Remove header from the comment lines - lc = len(comment_header) - data = "\n".join(line[lc:] for line in data.split("\n")) - # Reformat to maxformatwidth chars or a 20 char width, - # whichever is greater. - format_width = max(limit - len(comment_header), 20) - newdata = reformat_paragraph(data, format_width) - # re-split and re-insert the comment header. - newdata = newdata.split("\n") - # If the block ends in a \n, we don't want the comment prefix - # inserted after it. (Im not sure it makes sense to reformat a - # comment block that is not made of complete lines, but whatever!) - # Can't think of a clean solution, so we hack away - block_suffix = "" - if not newdata[-1]: - block_suffix = "\n" - newdata = newdata[:-1] - return '\n'.join(comment_header+line for line in newdata) + block_suffix - -def is_all_white(line): - """Return True if line is empty or all whitespace.""" - - return re.match(r"^\s*$", line) is not None - -def get_indent(line): - """Return the initial space or tab indent of line.""" - return re.match(r"^([ \t]*)", line).group() - -def get_comment_header(line): - """Return string with leading whitespace and '#' from line or ''. - - A null return indicates that the line is not a comment line. A non- - null return, such as ' #', will be used to find the other lines of - a comment block with the same indent. - """ - m = re.match(r"^([ \t]*#*)", line) - if m is None: return "" - return m.group(1) - - -# Copied from editor.py; importing it would cause an import cycle. -_line_indent_re = re.compile(r'[ \t]*') - -def get_line_indent(line, tabwidth): - """Return a line's indentation as (# chars, effective # of spaces). - - The effective # of spaces is the length after properly "expanding" - the tabs into spaces, as done by str.expandtabs(tabwidth). - """ - m = _line_indent_re.match(line) - return m.end(), len(m.group().expandtabs(tabwidth)) - - -class FormatRegion: - "Format selected text (region)." - - def __init__(self, editwin): - self.editwin = editwin - - def get_region(self): - """Return line information about the selected text region. - - If text is selected, the first and last indices will be - for the selection. If there is no text selected, the - indices will be the current cursor location. - - Return a tuple containing (first index, last index, - string representation of text, list of text lines). - """ - text = self.editwin.text - first, last = self.editwin.get_selection_indices() - if first and last: - head = text.index(first + " linestart") - tail = text.index(last + "-1c lineend +1c") - else: - head = text.index("insert linestart") - tail = text.index("insert lineend +1c") - chars = text.get(head, tail) - lines = chars.split("\n") - return head, tail, chars, lines - - def set_region(self, head, tail, chars, lines): - """Replace the text between the given indices. - - Args: - head: Starting index of text to replace. - tail: Ending index of text to replace. - chars: Expected to be string of current text - between head and tail. - lines: List of new lines to insert between head - and tail. - """ - text = self.editwin.text - newchars = "\n".join(lines) - if newchars == chars: - text.bell() - return - text.tag_remove("sel", "1.0", "end") - text.mark_set("insert", head) - text.undo_block_start() - text.delete(head, tail) - text.insert(head, newchars) - text.undo_block_stop() - text.tag_add("sel", head, "insert") - - def indent_region_event(self, event=None): - "Indent region by indentwidth spaces." - head, tail, chars, lines = self.get_region() - for pos in range(len(lines)): - line = lines[pos] - if line: - raw, effective = get_line_indent(line, self.editwin.tabwidth) - effective = effective + self.editwin.indentwidth - lines[pos] = self.editwin._make_blanks(effective) + line[raw:] - self.set_region(head, tail, chars, lines) - return "break" - - def dedent_region_event(self, event=None): - "Dedent region by indentwidth spaces." - head, tail, chars, lines = self.get_region() - for pos in range(len(lines)): - line = lines[pos] - if line: - raw, effective = get_line_indent(line, self.editwin.tabwidth) - effective = max(effective - self.editwin.indentwidth, 0) - lines[pos] = self.editwin._make_blanks(effective) + line[raw:] - self.set_region(head, tail, chars, lines) - return "break" - - def comment_region_event(self, event=None): - """Comment out each line in region. - - ## is appended to the beginning of each line to comment it out. - """ - head, tail, chars, lines = self.get_region() - for pos in range(len(lines) - 1): - line = lines[pos] - lines[pos] = '##' + line - self.set_region(head, tail, chars, lines) - return "break" - - def uncomment_region_event(self, event=None): - """Uncomment each line in region. - - Remove ## or # in the first positions of a line. If the comment - is not in the beginning position, this command will have no effect. - """ - head, tail, chars, lines = self.get_region() - for pos in range(len(lines)): - line = lines[pos] - if not line: - continue - if line[:2] == '##': - line = line[2:] - elif line[:1] == '#': - line = line[1:] - lines[pos] = line - self.set_region(head, tail, chars, lines) - return "break" - - def tabify_region_event(self, event=None): - "Convert leading spaces to tabs for each line in selected region." - head, tail, chars, lines = self.get_region() - tabwidth = self._asktabwidth() - if tabwidth is None: - return - for pos in range(len(lines)): - line = lines[pos] - if line: - raw, effective = get_line_indent(line, tabwidth) - ntabs, nspaces = divmod(effective, tabwidth) - lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:] - self.set_region(head, tail, chars, lines) - return "break" - - def untabify_region_event(self, event=None): - "Expand tabs to spaces for each line in region." - head, tail, chars, lines = self.get_region() - tabwidth = self._asktabwidth() - if tabwidth is None: - return - for pos in range(len(lines)): - lines[pos] = lines[pos].expandtabs(tabwidth) - self.set_region(head, tail, chars, lines) - return "break" - - def _asktabwidth(self): - "Return value for tab width." - return askinteger( - "Tab width", - "Columns per tab? (2-16)", - parent=self.editwin.text, - initialvalue=self.editwin.indentwidth, - minvalue=2, - maxvalue=16) - - -class Indents: - "Change future indents." - - def __init__(self, editwin): - self.editwin = editwin - - def toggle_tabs_event(self, event): - editwin = self.editwin - usetabs = editwin.usetabs - if askyesno( - "Toggle tabs", - "Turn tabs " + ("on", "off")[usetabs] + - "?\nIndent width " + - ("will be", "remains at")[usetabs] + " 8." + - "\n Note: a tab is always 8 columns", - parent=editwin.text): - editwin.usetabs = not usetabs - # Try to prevent inconsistent indentation. - # User must change indent width manually after using tabs. - editwin.indentwidth = 8 - return "break" - - def change_indentwidth_event(self, event): - editwin = self.editwin - new = askinteger( - "Indent width", - "New indent width (2-16)\n(Always use 8 when using tabs)", - parent=editwin.text, - initialvalue=editwin.indentwidth, - minvalue=2, - maxvalue=16) - if new and new != editwin.indentwidth and not editwin.usetabs: - editwin.indentwidth = new - return "break" - - -class Rstrip: # 'Strip Trailing Whitespace" on "Format" menu. - def __init__(self, editwin): - self.editwin = editwin - - def do_rstrip(self, event=None): - text = self.editwin.text - undo = self.editwin.undo - undo.undo_block_start() - - end_line = int(float(text.index('end'))) - for cur in range(1, end_line): - txt = text.get('%i.0' % cur, '%i.end' % cur) - raw = len(txt) - cut = len(txt.rstrip()) - # Since text.delete() marks file as changed, even if not, - # only call it when needed to actually delete something. - if cut < raw: - text.delete('%i.%i' % (cur, cut), '%i.end' % cur) - - if (text.get('end-2c') == '\n' # File ends with at least 1 newline; - and not hasattr(self.editwin, 'interp')): # & is not Shell. - # Delete extra user endlines. - while (text.index('end-1c') > '1.0' # Stop if file empty. - and text.get('end-3c') == '\n'): - text.delete('end-3c') - # Because tk indexes are slice indexes and never raise, - # a file with only newlines will be emptied. - # patchcheck.py does the same. - - undo.undo_block_stop() - - -if __name__ == "__main__": - from unittest import main - main('idlelib.idle_test.test_format', verbosity=2, exit=False) diff --git a/Lib/idlelib/grep.py b/Lib/idlelib/grep.py deleted file mode 100644 index 1251359..0000000 --- a/Lib/idlelib/grep.py +++ /dev/null @@ -1,221 +0,0 @@ -"""Grep dialog for Find in Files functionality. - - Inherits from SearchDialogBase for GUI and uses searchengine - to prepare search pattern. -""" -import fnmatch -import os -import sys - -from tkinter import StringVar, BooleanVar -from tkinter.ttk import Checkbutton # Frame imported in ...Base - -from idlelib.searchbase import SearchDialogBase -from idlelib import searchengine - -# Importing OutputWindow here fails due to import loop -# EditorWindow -> GrepDialog -> OutputWindow -> EditorWindow - - -def grep(text, io=None, flist=None): - """Open the Find in Files dialog. - - Module-level function to access the singleton GrepDialog - instance and open the dialog. If text is selected, it is - used as the search phrase; otherwise, the previous entry - is used. - - Args: - text: Text widget that contains the selected text for - default search phrase. - io: iomenu.IOBinding instance with default path to search. - flist: filelist.FileList instance for OutputWindow parent. - """ - root = text._root() - engine = searchengine.get(root) - if not hasattr(engine, "_grepdialog"): - engine._grepdialog = GrepDialog(root, engine, flist) - dialog = engine._grepdialog - searchphrase = text.get("sel.first", "sel.last") - dialog.open(text, searchphrase, io) - - -def walk_error(msg): - "Handle os.walk error." - print(msg) - - -def findfiles(folder, pattern, recursive): - """Generate file names in dir that match pattern. - - Args: - folder: Root directory to search. - pattern: File pattern to match. - recursive: True to include subdirectories. - """ - for dirpath, _, filenames in os.walk(folder, onerror=walk_error): - yield from (os.path.join(dirpath, name) - for name in filenames - if fnmatch.fnmatch(name, pattern)) - if not recursive: - break - - -class GrepDialog(SearchDialogBase): - "Dialog for searching multiple files." - - title = "Find in Files Dialog" - icon = "Grep" - needwrapbutton = 0 - - def __init__(self, root, engine, flist): - """Create search dialog for searching for a phrase in the file system. - - Uses SearchDialogBase as the basis for the GUI and a - searchengine instance to prepare the search. - - Attributes: - flist: filelist.Filelist instance for OutputWindow parent. - globvar: String value of Entry widget for path to search. - globent: Entry widget for globvar. Created in - create_entries(). - recvar: Boolean value of Checkbutton widget for - traversing through subdirectories. - """ - super().__init__(root, engine) - self.flist = flist - self.globvar = StringVar(root) - self.recvar = BooleanVar(root) - - def open(self, text, searchphrase, io=None): - """Make dialog visible on top of others and ready to use. - - Extend the SearchDialogBase open() to set the initial value - for globvar. - - Args: - text: Multicall object containing the text information. - searchphrase: String phrase to search. - io: iomenu.IOBinding instance containing file path. - """ - SearchDialogBase.open(self, text, searchphrase) - if io: - path = io.filename or "" - else: - path = "" - dir, base = os.path.split(path) - head, tail = os.path.splitext(base) - if not tail: - tail = ".py" - self.globvar.set(os.path.join(dir, "*" + tail)) - - def create_entries(self): - "Create base entry widgets and add widget for search path." - SearchDialogBase.create_entries(self) - self.globent = self.make_entry("In files:", self.globvar)[0] - - def create_other_buttons(self): - "Add check button to recurse down subdirectories." - btn = Checkbutton( - self.make_frame()[0], variable=self.recvar, - text="Recurse down subdirectories") - btn.pack(side="top", fill="both") - - def create_command_buttons(self): - "Create base command buttons and add button for Search Files." - SearchDialogBase.create_command_buttons(self) - self.make_button("Search Files", self.default_command, isdef=True) - - def default_command(self, event=None): - """Grep for search pattern in file path. The default command is bound - to <Return>. - - If entry values are populated, set OutputWindow as stdout - and perform search. The search dialog is closed automatically - when the search begins. - """ - prog = self.engine.getprog() - if not prog: - return - path = self.globvar.get() - if not path: - self.top.bell() - return - from idlelib.outwin import OutputWindow # leave here! - save = sys.stdout - try: - sys.stdout = OutputWindow(self.flist) - self.grep_it(prog, path) - finally: - sys.stdout = save - - def grep_it(self, prog, path): - """Search for prog within the lines of the files in path. - - For the each file in the path directory, open the file and - search each line for the matching pattern. If the pattern is - found, write the file and line information to stdout (which - is an OutputWindow). - - Args: - prog: The compiled, cooked search pattern. - path: String containing the search path. - """ - folder, filepat = os.path.split(path) - if not folder: - folder = os.curdir - filelist = sorted(findfiles(folder, filepat, self.recvar.get())) - self.close() - pat = self.engine.getpat() - print(f"Searching {pat!r} in {path} ...") - hits = 0 - try: - for fn in filelist: - 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(f"{fn}: {lineno}: {line}\n") - hits += 1 - except OSError as msg: - print(msg) - print(f"Hits found: {hits}\n(Hint: right-click to open locations.)" - 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 _grep_dialog(parent): # htest # - from tkinter import Toplevel, Text, SEL, END - from tkinter.ttk import Frame, Button - from idlelib.pyshell import PyShellFileList - - top = Toplevel(parent) - top.title("Test GrepDialog") - x, y = map(int, parent.geometry().split('+')[1:]) - top.geometry(f"+{x}+{y + 175}") - - flist = PyShellFileList(top) - frame = Frame(top) - frame.pack() - text = Text(frame, 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(frame, text="Show GrepDialog", command=show_grep_dialog) - button.pack() - -if __name__ == "__main__": - from unittest import main - 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/help.html b/Lib/idlelib/help.html index 09dc4c5..9a20c39 100644 --- a/Lib/idlelib/help.html +++ b/Lib/idlelib/help.html @@ -1,54 +1,46 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> - <meta charset="utf-8" /> - <title>IDLE — Python 3.9.0a1 documentation</title> - <link rel="stylesheet" href="../_static/pydoctheme.css" type="text/css" /> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + + <title>24.6. IDLE — Python 2.7.12 documentation</title> + + <link rel="stylesheet" href="../_static/classic.css" type="text/css" /> <link rel="stylesheet" href="../_static/pygments.css" type="text/css" /> - <script type="text/javascript" id="documentation_options" data-url_root="../" src="../_static/documentation_options.js"></script> + <script type="text/javascript"> + var DOCUMENTATION_OPTIONS = { + URL_ROOT: '../', + VERSION: '2.7.12', + 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/language_data.js"></script> - <script type="text/javascript" src="../_static/sidebar.js"></script> - <link rel="search" type="application/opensearchdescription+xml" - title="Search within Python 3.9.0a1 documentation" + title="Search within Python 2.7.12 documentation" href="../_static/opensearch.xml"/> <link rel="author" title="About these documents" href="../about.html" /> - <link rel="index" title="Index" href="../genindex.html" /> - <link rel="search" title="Search" href="../search.html" /> <link rel="copyright" title="Copyright" href="../copyright.html" /> - <link rel="next" title="Other Graphical User Interface Packages" href="othergui.html" /> - <link rel="prev" title="tkinter.tix — Extension widgets for Tk" href="tkinter.tix.html" /> - <link rel="canonical" href="https://docs.python.org/3/library/idle.html" /> - - - - - - - <style> - @media only screen { - table.full-width-table { - width: 100%; - } - } - </style> - + <link rel="top" title="Python 2.7.12 documentation" href="../contents.html" /> + <link rel="up" title="24. Graphical User Interfaces with Tk" href="tk.html" /> + <link rel="next" title="24.7. Other Graphical User Interface Packages" href="othergui.html" /> + <link rel="prev" title="24.5. turtle — Turtle graphics for Tk" href="turtle.html" /> <link rel="shortcut icon" type="image/png" href="../_static/py.png" /> - <script type="text/javascript" src="../_static/copybutton.js"></script> - </head><body> - + </head> + <body role="document"> <div class="related" role="navigation" aria-label="related navigation"> <h3>Navigation</h3> <ul> @@ -59,38 +51,20 @@ <a href="../py-modindex.html" title="Python Module Index" >modules</a> |</li> <li class="right" > - <a href="othergui.html" title="Other Graphical User Interface Packages" + <a href="othergui.html" title="24.7. Other Graphical User Interface Packages" accesskey="N">next</a> |</li> <li class="right" > - <a href="tkinter.tix.html" title="tkinter.tix — Extension widgets for Tk" + <a href="turtle.html" title="24.5. turtle — Turtle graphics for Tk" 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.9.0a1 Documentation</a> » - </li> - - <li class="nav-item nav-item-1"><a href="index.html" >The Python Standard Library</a> »</li> - <li class="nav-item nav-item-2"><a href="tk.html" accesskey="U">Graphical User Interfaces with Tk</a> »</li> - <li class="right"> - - - <div class="inline-search" style="display: none" role="search"> - <form class="inline-search" action="../search.html" method="get"> - <input placeholder="Quick search" 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> - </div> - <script type="text/javascript">$('.inline-search').show(0);</script> - | - </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">Python 2.7.12 documentation</a> » + </li> + + <li class="nav-item nav-item-1"><a href="index.html" >The Python Standard Library</a> »</li> + <li class="nav-item nav-item-2"><a href="tk.html" accesskey="U">24. Graphical User Interfaces with Tk</a> »</li> </ul> </div> @@ -100,407 +74,359 @@ <div class="body" role="main"> <div class="section" id="idle"> -<span id="id1"></span><h1>IDLE<a class="headerlink" href="#idle" title="Permalink to this headline">¶</a></h1> -<p><strong>Source code:</strong> <a class="reference external" href="https://github.com/python/cpython/tree/master/Lib/idlelib/">Lib/idlelib/</a></p> -<hr class="docutils" id="index-0" /> -<p>IDLE is Python’s Integrated Development and Learning Environment.</p> +<span id="id1"></span><h1>24.6. 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><p>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"><code class="xref py py-mod docutils literal notranslate"><span class="pre">tkinter</span></code></a> GUI toolkit</p></li> -<li><p>cross-platform: works mostly the same on Windows, Unix, and macOS</p></li> -<li><p>Python shell window (interactive interpreter) with colorizing -of code input, output, and error messages</p></li> -<li><p>multi-window text editor with multiple undo, Python colorizing, -smart indent, call tips, auto completion, and other features</p></li> -<li><p>search within any window, replace within editor windows, and search -through multiple files (grep)</p></li> -<li><p>debugger with persistent breakpoints, stepping, and viewing -of global and local namespaces</p></li> -<li><p>configuration, browsers, and other dialogs</p></li> +<li>coded in 100% pure Python, using the <code class="xref py py-mod docutils literal"><span class="pre">tkinter</span></code> 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>Menus<a class="headerlink" href="#menus" title="Permalink to this headline">¶</a></h2> +<h2>24.6.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. On Windows and -Linux, each has its own top menu. Each menu documented below indicates -which window type it is associated with.</p> -<p>Output windows, such as used for Edit => Find in Files, are a subtype of editor -window. They currently have the same top menu but a different -default title and context menu.</p> -<p>On macOS, there is one application menu. It dynamically changes according -to the window currently selected. It has an IDLE menu, and some entries -described below are moved around to conform to Apple guidelines.</p> +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>File menu (Shell and Editor)<a class="headerlink" href="#file-menu-shell-and-editor" title="Permalink to this headline">¶</a></h3> -<dl class="simple"> -<dt>New File</dt><dd><p>Create a new file editing window.</p> -</dd> -<dt>Open…</dt><dd><p>Open an existing file with an Open dialog.</p> -</dd> -<dt>Recent Files</dt><dd><p>Open a list of recent files. Click one to open it.</p> -</dd> -<dt>Open Module…</dt><dd><p>Open an existing module (searches sys.path).</p> -</dd> +<h3>24.6.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="simple" id="index-1"> -<dt>Class Browser</dt><dd><p>Show functions, classes, and methods in the current Editor file in a -tree structure. In the shell, open a module first.</p> -</dd> -<dt>Path Browser</dt><dd><p>Show sys.path directories, modules, functions, classes and methods in a -tree structure.</p> -</dd> -<dt>Save</dt><dd><p>Save the current window to the associated file, if there is one. Windows +<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.</p> -</dd> -<dt>Save As…</dt><dd><p>Save the current window with a Save As dialog. The file saved becomes the -new associated file for the window.</p> -</dd> -<dt>Save Copy As…</dt><dd><p>Save the current window to different file without changing the associated -file.</p> -</dd> -<dt>Print Window</dt><dd><p>Print the current window to the default printer.</p> -</dd> -<dt>Close</dt><dd><p>Close the current window (ask to save if unsaved).</p> -</dd> -<dt>Exit</dt><dd><p>Close all windows and quit IDLE (ask to save unsaved windows).</p> -</dd> +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>Edit menu (Shell and Editor)<a class="headerlink" href="#edit-menu-shell-and-editor" title="Permalink to this headline">¶</a></h3> -<dl class="simple"> -<dt>Undo</dt><dd><p>Undo the last change to the current window. A maximum of 1000 changes may -be undone.</p> -</dd> -<dt>Redo</dt><dd><p>Redo the last undone change to the current window.</p> -</dd> -<dt>Cut</dt><dd><p>Copy selection into the system-wide clipboard; then delete the selection.</p> -</dd> -<dt>Copy</dt><dd><p>Copy selection into the system-wide clipboard.</p> -</dd> -<dt>Paste</dt><dd><p>Insert contents of the system-wide clipboard into the current window.</p> -</dd> +<h3>24.6.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="simple"> -<dt>Select All</dt><dd><p>Select the entire contents of the current window.</p> -</dd> -<dt>Find…</dt><dd><p>Open a search dialog with many options</p> -</dd> -<dt>Find Again</dt><dd><p>Repeat the last search, if there is one.</p> -</dd> -<dt>Find Selection</dt><dd><p>Search for the currently selected string, if there is one.</p> -</dd> -<dt>Find in Files…</dt><dd><p>Open a file search dialog. Put results in a new output window.</p> -</dd> -<dt>Replace…</dt><dd><p>Open a search-and-replace dialog.</p> -</dd> -<dt>Go to Line</dt><dd><p>Move cursor to the line number requested and make that line visible.</p> -</dd> -<dt>Show Completions</dt><dd><p>Open a scrollable list allowing selection of keywords and attributes. See -<a class="reference internal" href="#completions"><span class="std std-ref">Completions</span></a> in the Editing and navigation section below.</p> -</dd> -<dt>Expand Word</dt><dd><p>Expand a prefix you have typed to match a full word in the same window; -repeat to get a different expansion.</p> -</dd> -<dt>Show call tip</dt><dd><p>After an unclosed parenthesis for a function, open a small window with -function parameter hints. See <a class="reference internal" href="#calltips"><span class="std std-ref">Calltips</span></a> in the -Editing and navigation section below.</p> -</dd> -<dt>Show surrounding parens</dt><dd><p>Highlight the surrounding parenthesis.</p> -</dd> +<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 a 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"> -<span id="format-menu"></span><h3>Format menu (Editor window only)<a class="headerlink" href="#format-menu-editor-window-only" title="Permalink to this headline">¶</a></h3> -<dl class="simple"> -<dt>Indent Region</dt><dd><p>Shift selected lines right by the indent width (default 4 spaces).</p> -</dd> -<dt>Dedent Region</dt><dd><p>Shift selected lines left by the indent width (default 4 spaces).</p> -</dd> -<dt>Comment Out Region</dt><dd><p>Insert ## in front of selected lines.</p> -</dd> -<dt>Uncomment Region</dt><dd><p>Remove leading # or ## from selected lines.</p> -</dd> -<dt>Tabify Region</dt><dd><p>Turn <em>leading</em> stretches of spaces into tabs. (Note: We recommend using -4 space blocks to indent Python code.)</p> -</dd> -<dt>Untabify Region</dt><dd><p>Turn <em>all</em> tabs into the correct number of spaces.</p> -</dd> -<dt>Toggle Tabs</dt><dd><p>Open a dialog to switch between indenting with spaces and tabs.</p> -</dd> -<dt>New Indent Width</dt><dd><p>Open a dialog to change indent width. The accepted default by the Python -community is 4 spaces.</p> -</dd> -<dt>Format Paragraph</dt><dd><p>Reformat the current blank-line-delimited paragraph in comment block or +<h3>24.6.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.</p> -</dd> -<dt>Strip trailing whitespace</dt><dd><p>Remove trailing space and other whitespace characters after the last -non-whitespace character of a line by applying str.rstrip to each line, -including lines within multiline strings. Except for Shell windows, -remove extra newlines at the end of the file.</p> -</dd> +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>Run menu (Editor window only)<a class="headerlink" href="#run-menu-editor-window-only" title="Permalink to this headline">¶</a></h3> -<dl class="simple" id="run-module"> -<dt>Run Module</dt><dd><p>Do <a class="reference internal" href="#check-module"><span class="std std-ref">Check Module</span></a>. 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 <code class="docutils literal notranslate"><span class="pre">print</span></code> or <code class="docutils literal notranslate"><span class="pre">write</span></code>. -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 <code class="docutils literal notranslate"><span class="pre">python</span> <span class="pre">-i</span> <span class="pre">file</span></code> at a command -line.</p> -</dd> -</dl> -<dl class="simple" id="run-custom"> -<dt>Run… Customized</dt><dd><p>Same as <a class="reference internal" href="#run-module"><span class="std std-ref">Run Module</span></a>, but run the module with customized -settings. <em>Command Line Arguments</em> extend <a class="reference internal" href="sys.html#sys.argv" title="sys.argv"><code class="xref py py-data docutils literal notranslate"><span class="pre">sys.argv</span></code></a> as if passed -on a command line. The module can be run in the Shell without restarting.</p> -</dd> -</dl> -<dl class="simple" id="check-module"> -<dt>Check Module</dt><dd><p>Check the syntax of the module currently open in the Editor window. If the +<span id="index-2"></span><h3>24.6.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.</p> -</dd> -</dl> -<dl class="simple" id="python-shell"> -<dt>Python Shell</dt><dd><p>Open or wake up the Python Shell window.</p> -</dd> +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 <code class="docutils literal"><span class="pre">print</span></code> or <code class="docutils literal"><span class="pre">write</span></code>. +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 <code class="docutils literal"><span class="pre">python</span> <span class="pre">-i</span> <span class="pre">file</span></code> at a command +line.</dd> </dl> </div> <div class="section" id="shell-menu-shell-window-only"> -<h3>Shell menu (Shell window only)<a class="headerlink" href="#shell-menu-shell-window-only" title="Permalink to this headline">¶</a></h3> -<dl class="simple"> -<dt>View Last Restart</dt><dd><p>Scroll the shell window to the last Shell restart.</p> -</dd> -<dt>Restart Shell</dt><dd><p>Restart the shell to clean the environment.</p> -</dd> -<dt>Previous History</dt><dd><p>Cycle through earlier commands in history which match the current entry.</p> -</dd> -<dt>Next History</dt><dd><p>Cycle through later commands in history which match the current entry.</p> -</dd> -<dt>Interrupt Execution</dt><dd><p>Stop a running program.</p> -</dd> +<h3>24.6.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> +<dt>Interrupt Execution</dt> +<dd>Stop a running program.</dd> </dl> </div> <div class="section" id="debug-menu-shell-window-only"> -<h3>Debug menu (Shell window only)<a class="headerlink" href="#debug-menu-shell-window-only" title="Permalink to this headline">¶</a></h3> -<dl class="simple"> -<dt>Go to File/Line</dt><dd><p>Look on the current line. with the cursor, and the line above for a filename +<h3>24.6.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.</p> -</dd> +the Shell window and Output windows.</dd> </dl> -<dl class="simple" id="index-3"> -<dt>Debugger (toggle)</dt><dd><p>When activated, code entered in the Shell or run from an Editor will run +<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.</p> -</dd> -<dt>Stack Viewer</dt><dd><p>Show the stack traceback of the last exception in a tree widget, with -access to locals and globals.</p> -</dd> -<dt>Auto-open Stack Viewer</dt><dd><p>Toggle automatically opening the stack viewer on an unhandled exception.</p> -</dd> +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>Options menu (Shell and Editor)<a class="headerlink" href="#options-menu-shell-and-editor" title="Permalink to this headline">¶</a></h3> -<dl class="simple"> -<dt>Configure IDLE</dt><dd><p>Open a configuration dialog and change preferences for the following: +<h3>24.6.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. On macOS, open the -configuration dialog by selecting Preferences in the application -menu. For more details, see -<a class="reference internal" href="#preferences"><span class="std std-ref">Setting preferences</span></a> under Help and preferences.</p> -</dd> -</dl> -<p>Most configuration options apply to all windows or all future windows. -The option items below only apply to the active window.</p> -<dl class="simple"> -<dt>Show/Hide Code Context (Editor Window only)</dt><dd><p>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. See -<a class="reference internal" href="#code-context"><span class="std std-ref">Code Context</span></a> in the Editing and Navigation section -below.</p> -</dd> -<dt>Show/Hide Line Numbers (Editor Window only)</dt><dd><p>Open a column to the left of the edit window which shows the number -of each line of text. The default is off, which may be changed in the -preferences (see <a class="reference internal" href="#preferences"><span class="std std-ref">Setting preferences</span></a>).</p> -</dd> -<dt>Zoom/Restore Height</dt><dd><p>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. The maximum height for a screen is determined by -momentarily maximizing a window the first time one is zoomed on the screen. -Changing screen settings may invalidate the saved height. This toggle has -no effect when a window is maximized.</p> +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>Window menu (Shell and Editor)<a class="headerlink" href="#window-menu-shell-and-editor" title="Permalink to this headline">¶</a></h3> -<p>Lists the names of all open windows; select one to bring it to the foreground -(deiconifying it if necessary).</p> +<h3>24.6.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>Help menu (Shell and Editor)<a class="headerlink" href="#help-menu-shell-and-editor" title="Permalink to this headline">¶</a></h3> -<dl class="simple"> -<dt>About IDLE</dt><dd><p>Display version, copyright, license, credits, and more.</p> -</dd> -<dt>IDLE Help</dt><dd><p>Display this IDLE document, detailing the menu options, basic editing and -navigation, and other tips.</p> -</dd> -<dt>Python Docs</dt><dd><p>Access local Python documentation, if installed, or start a web browser -and open docs.python.org showing the latest Python documentation.</p> -</dd> -<dt>Turtle Demo</dt><dd><p>Run the turtledemo module with example Python code and turtle drawings.</p> -</dd> +<h3>24.6.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. See the <a class="reference internal" href="#help-sources"><span class="std std-ref">Help sources</span></a> subsection below -for more on Help menu choices.</p> +the General tab.</p> </div> <div class="section" id="context-menus"> -<span id="index-4"></span><h3>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 macOS). +<span id="index-4"></span><h3>24.6.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="simple"> -<dt>Cut</dt><dd><p>Copy selection into the system-wide clipboard; then delete the selection.</p> -</dd> -<dt>Copy</dt><dd><p>Copy selection into the system-wide clipboard.</p> -</dd> -<dt>Paste</dt><dd><p>Insert contents of the system-wide clipboard into the current window.</p> -</dd> +<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="simple"> -<dt>Set Breakpoint</dt><dd><p>Set a breakpoint on the current line.</p> -</dd> -<dt>Clear Breakpoint</dt><dd><p>Clear the breakpoint on that line.</p> -</dd> -</dl> -<p>Shell and Output windows also have the following.</p> -<dl class="simple"> -<dt>Go to file/line</dt><dd><p>Same as in Debug menu.</p> -</dd> +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>The Shell window also has an output squeezing facility explained in the <em>Python -Shell window</em> subsection below.</p> -<dl class="simple"> -<dt>Squeeze</dt><dd><p>If the cursor is over an output line, squeeze all the output between -the code above and the prompt below down to a ‘Squeezed text’ label.</p> -</dd> +<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"> -<span id="id2"></span><h2>Editing and navigation<a class="headerlink" href="#editing-and-navigation" title="Permalink to this headline">¶</a></h2> -<div class="section" id="editor-windows"> -<h3>Editor windows<a class="headerlink" href="#editor-windows" title="Permalink to this headline">¶</a></h3> -<p>IDLE may open editor windows when it starts, depending on settings -and how you start IDLE. Thereafter, use the File menu. There can be only -one open editor window for a given file.</p> -<p>The title bar contains the name of the file, the full path, and the version -of Python and IDLE running the window. The status bar contains the line -number (‘Ln’) and column number (‘Col’). Line numbers start with 1; -column numbers with 0.</p> -<p>IDLE assumes that files with a known .py* extension contain Python code -and that other files do not. Run Python code with the Run menu.</p> -</div> -<div class="section" id="key-bindings"> -<h3>Key bindings<a class="headerlink" href="#key-bindings" title="Permalink to this headline">¶</a></h3> -<p>In this section, ‘C’ refers to the <kbd class="kbd docutils literal notranslate">Control</kbd> key on Windows and Unix and -the <kbd class="kbd docutils literal notranslate">Command</kbd> key on macOS.</p> +<h2>24.6.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 <code class="kbd docutils literal"><span class="pre">Control</span></code> key on Windows and Unix and +the <code class="kbd docutils literal"><span class="pre">Command</span></code> key on Mac OSX.</p> <ul> -<li><p><kbd class="kbd docutils literal notranslate">Backspace</kbd> deletes to the left; <kbd class="kbd docutils literal notranslate">Del</kbd> deletes to the right</p></li> -<li><p><kbd class="kbd docutils literal notranslate">C-Backspace</kbd> delete word left; <kbd class="kbd docutils literal notranslate">C-Del</kbd> delete word to the right</p></li> -<li><p>Arrow keys and <kbd class="kbd docutils literal notranslate">Page Up</kbd>/<kbd class="kbd docutils literal notranslate">Page Down</kbd> to move around</p></li> -<li><p><kbd class="kbd docutils literal notranslate">C-LeftArrow</kbd> and <kbd class="kbd docutils literal notranslate">C-RightArrow</kbd> moves by words</p></li> -<li><p><kbd class="kbd docutils literal notranslate">Home</kbd>/<kbd class="kbd docutils literal notranslate">End</kbd> go to begin/end of line</p></li> -<li><p><kbd class="kbd docutils literal notranslate">C-Home</kbd>/<kbd class="kbd docutils literal notranslate">C-End</kbd> go to begin/end of file</p></li> -<li><p>Some useful Emacs bindings are inherited from Tcl/Tk:</p> +<li><p class="first"><code class="kbd docutils literal"><span class="pre">Backspace</span></code> deletes to the left; <code class="kbd docutils literal"><span class="pre">Del</span></code> deletes to the right</p> +</li> +<li><p class="first"><code class="kbd docutils literal"><span class="pre">C-Backspace</span></code> delete word left; <code class="kbd docutils literal"><span class="pre">C-Del</span></code> delete word to the right</p> +</li> +<li><p class="first">Arrow keys and <code class="kbd docutils literal"><span class="pre">Page</span> <span class="pre">Up</span></code>/<code class="kbd docutils literal"><span class="pre">Page</span> <span class="pre">Down</span></code> to move around</p> +</li> +<li><p class="first"><code class="kbd docutils literal"><span class="pre">C-LeftArrow</span></code> and <code class="kbd docutils literal"><span class="pre">C-RightArrow</span></code> moves by words</p> +</li> +<li><p class="first"><code class="kbd docutils literal"><span class="pre">Home</span></code>/<code class="kbd docutils literal"><span class="pre">End</span></code> go to begin/end of line</p> +</li> +<li><p class="first"><code class="kbd docutils literal"><span class="pre">C-Home</span></code>/<code class="kbd docutils literal"><span class="pre">C-End</span></code> 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><p><kbd class="kbd docutils literal notranslate">C-a</kbd> beginning of line</p></li> -<li><p><kbd class="kbd docutils literal notranslate">C-e</kbd> end of line</p></li> -<li><p><kbd class="kbd docutils literal notranslate">C-k</kbd> kill line (but doesn’t put it in clipboard)</p></li> -<li><p><kbd class="kbd docutils literal notranslate">C-l</kbd> center window around the insertion point</p></li> -<li><p><kbd class="kbd docutils literal notranslate">C-b</kbd> go backward one character without deleting (usually you can -also use the cursor key for this)</p></li> -<li><p><kbd class="kbd docutils literal notranslate">C-f</kbd> go forward one character without deleting (usually you can -also use the cursor key for this)</p></li> -<li><p><kbd class="kbd docutils literal notranslate">C-p</kbd> go up one line (usually you can also use the cursor key for -this)</p></li> -<li><p><kbd class="kbd docutils literal notranslate">C-d</kbd> delete next character</p></li> +<li><code class="kbd docutils literal"><span class="pre">C-a</span></code> beginning of line</li> +<li><code class="kbd docutils literal"><span class="pre">C-e</span></code> end of line</li> +<li><code class="kbd docutils literal"><span class="pre">C-k</span></code> kill line (but doesn’t put it in clipboard)</li> +<li><code class="kbd docutils literal"><span class="pre">C-l</span></code> center window around the insertion point</li> +<li><code class="kbd docutils literal"><span class="pre">C-b</span></code> go backwards one character without deleting (usually you can +also use the cursor key for this)</li> +<li><code class="kbd docutils literal"><span class="pre">C-f</span></code> go forward one character without deleting (usually you can +also use the cursor key for this)</li> +<li><code class="kbd docutils literal"><span class="pre">C-p</span></code> go up one line (usually you can also use the cursor key for +this)</li> +<li><code class="kbd docutils literal"><span class="pre">C-d</span></code> delete next character</li> </ul> </div></blockquote> </li> </ul> -<p>Standard keybindings (like <kbd class="kbd docutils literal notranslate">C-c</kbd> to copy and <kbd class="kbd docutils literal notranslate">C-v</kbd> to paste) +<p>Standard keybindings (like <code class="kbd docutils literal"><span class="pre">C-c</span></code> to copy and <code class="kbd docutils literal"><span class="pre">C-v</span></code> to paste) may work. Keybindings are selected in the Configure IDLE dialog.</p> -</div> <div class="section" id="automatic-indentation"> -<h3>Automatic indentation<a class="headerlink" href="#automatic-indentation" title="Permalink to this headline">¶</a></h3> +<h3>24.6.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, <kbd class="kbd docutils literal notranslate">Backspace</kbd> deletes up -to 4 spaces if they are there. <kbd class="kbd docutils literal notranslate">Tab</kbd> inserts spaces (in the Python -Shell window one tab), number depends on Indent width. Currently, tabs +the next line is dedented. In leading indentation, <code class="kbd docutils literal"><span class="pre">Backspace</span></code> deletes up +to 4 spaces if they are there. <code class="kbd docutils literal"><span class="pre">Tab</span></code> 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 on the -<a class="reference internal" href="#format-menu"><span class="std std-ref">Format menu</span></a>.</p> +<p>See also the indent/dedent region commands in the edit menu.</p> </div> <div class="section" id="completions"> -<span id="id3"></span><h3>Completions<a class="headerlink" href="#completions" title="Permalink to this headline">¶</a></h3> +<h3>24.6.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 +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 -<kbd class="kbd docutils literal notranslate">Tab</kbd> will supply that completion without opening the ACW.</p> -<p>‘Show Completions’ will force open a completions window, by default the -<kbd class="kbd docutils literal notranslate">C-space</kbd> will open a completions window. In an empty +<code class="kbd docutils literal"><span class="pre">Tab</span></code> will supply that completion without opening the ACW.</p> +<p>‘Show Completions’ will force open a completions window, by default the +<code class="kbd docutils literal"><span class="pre">C-space</span></code> 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 namespaces, plus any modules imported. If some +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 <kbd class="kbd docutils literal notranslate">tab</kbd> will +entry most closely matching those characters. Entering a <code class="kbd docutils literal"><span class="pre">tab</span></code> will cause the longest non-ambiguous match to be entered in the Editor window or -Shell. Two <kbd class="kbd docutils literal notranslate">tab</kbd> in a row will supply the current ACW selection, as +Shell. Two <code class="kbd docutils literal"><span class="pre">tab</span></code> 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 -<code class="docutils literal notranslate"><span class="pre">__all__</span></code> set, or to class-private attributes.</p> -<p>Completions and the ‘Expand Word’ facility can save a lot of typing!</p> +<p>“Hidden” attributes can be accessed by typing the beginning of hidden +name after a ‘.’, e.g. ‘_’. This allows access to modules with +<code class="docutils literal"><span class="pre">__all__</span></code> 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 <code class="docutils literal notranslate"><span class="pre">__main__</span></code> and <a class="reference internal" href="sys.html#sys.modules" title="sys.modules"><code class="xref py py-data docutils literal notranslate"><span class="pre">sys.modules</span></code></a> will +an Editor window which are not via <code class="docutils literal"><span class="pre">__main__</span></code> and <a class="reference internal" href="sys.html#sys.modules" title="sys.modules"><code class="xref py py-data docutils literal"><span class="pre">sys.modules</span></code></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 +<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"> -<span id="id4"></span><h3>Calltips<a class="headerlink" href="#calltips" title="Permalink to this headline">¶</a></h3> -<p>A calltip is shown when one types <kbd class="kbd docutils literal notranslate">(</kbd> after the name of an <em>accessible</em> +<h3>24.6.2.3. Calltips<a class="headerlink" href="#calltips" title="Permalink to this headline">¶</a></h3> +<p>A calltip is shown when one types <code class="kbd docutils literal"><span class="pre">(</span></code> 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 <kbd class="kbd docutils literal notranslate">)</kbd> is typed. When the cursor is in the argument part of a definition, +or <code class="kbd docutils literal"><span class="pre">)</span></code> 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 @@ -509,61 +435,40 @@ 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 <code class="docutils literal notranslate"><span class="pre">itertools.count(</span></code>. A calltip +<p>For example, restart the Shell and enter <code class="docutils literal"><span class="pre">itertools.count(</span></code>. A calltip appears because Idle imports itertools into the user process for its own use. -(This could change.) Enter <code class="docutils literal notranslate"><span class="pre">turtle.write(</span></code> and nothing appears. Idle does +(This could change.) Enter <code class="docutils literal"><span class="pre">turtle.write(</span></code> and nothing appears. Idle does not import turtle. The menu or shortcut do nothing either. Enter -<code class="docutils literal notranslate"><span class="pre">import</span> <span class="pre">turtle</span></code> and then <code class="docutils literal notranslate"><span class="pre">turtle.write(</span></code> will work.</p> +<code class="docutils literal"><span class="pre">import</span> <span class="pre">turtle</span></code> and then <code class="docutils literal"><span class="pre">turtle.write(</span></code> 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="code-context"> -<span id="id5"></span><h3>Code Context<a class="headerlink" href="#code-context" title="Permalink to this headline">¶</a></h3> -<p>Within an editor window containing Python code, code context can be toggled -in order to show or hide a pane at the top of the window. When shown, this -pane freezes the opening lines for block code, such as those beginning with -<code class="docutils literal notranslate"><span class="pre">class</span></code>, <code class="docutils literal notranslate"><span class="pre">def</span></code>, or <code class="docutils literal notranslate"><span class="pre">if</span></code> keywords, that would have otherwise scrolled -out of view. The size of the pane will be expanded and contracted as needed -to show the all current levels of context, up to the maximum number of -lines defined in the Configure IDLE dialog (which defaults to 15). If there -are no current context lines and the feature is toggled on, a single blank -line will display. Clicking on a line in the context pane will move that -line to the top of the editor.</p> -<p>The text and background colors for the context pane can be configured under -the Highlights tab in the Configure IDLE dialog.</p> -</div> <div class="section" id="python-shell-window"> -<h3>Python Shell window<a class="headerlink" href="#python-shell-window" title="Permalink to this headline">¶</a></h3> -<p>With IDLE’s Shell, one enters, edits, and recalls complete statements. -Most consoles and terminals only work with a single physical line at a time.</p> -<p>When one pastes code into Shell, it is not compiled and possibly executed -until one hits <kbd class="kbd docutils literal notranslate">Return</kbd>. One may edit pasted code first. -If one pastes more that one statement into Shell, the result will be a -<a class="reference internal" href="exceptions.html#SyntaxError" title="SyntaxError"><code class="xref py py-exc docutils literal notranslate"><span class="pre">SyntaxError</span></code></a> when multiple statements are compiled as if they were one.</p> -<p>The editing features described in previous subsections work when entering -code interactively. IDLE’s Shell window also responds to the following keys.</p> +<h3>24.6.2.4. Python Shell window<a class="headerlink" href="#python-shell-window" title="Permalink to this headline">¶</a></h3> <ul> -<li><p><kbd class="kbd docutils literal notranslate">C-c</kbd> interrupts executing command</p></li> -<li><p><kbd class="kbd docutils literal notranslate">C-d</kbd> sends end-of-file; closes window if typed at a <code class="docutils literal notranslate"><span class="pre">>>></span></code> prompt</p></li> -<li><p><kbd class="kbd docutils literal notranslate">Alt-/</kbd> (Expand word) is also useful to reduce typing</p> +<li><p class="first"><code class="kbd docutils literal"><span class="pre">C-c</span></code> interrupts executing command</p> +</li> +<li><p class="first"><code class="kbd docutils literal"><span class="pre">C-d</span></code> sends end-of-file; closes window if typed at a <code class="docutils literal"><span class="pre">>>></span></code> prompt</p> +</li> +<li><p class="first"><code class="kbd docutils literal"><span class="pre">Alt-/</span></code> (Expand word) is also useful to reduce typing</p> <p>Command history</p> <ul class="simple"> -<li><p><kbd class="kbd docutils literal notranslate">Alt-p</kbd> retrieves previous command matching what you have typed. On -macOS use <kbd class="kbd docutils literal notranslate">C-p</kbd>.</p></li> -<li><p><kbd class="kbd docutils literal notranslate">Alt-n</kbd> retrieves next. On macOS use <kbd class="kbd docutils literal notranslate">C-n</kbd>.</p></li> -<li><p><kbd class="kbd docutils literal notranslate">Return</kbd> while on any previous command retrieves that command</p></li> +<li><code class="kbd docutils literal"><span class="pre">Alt-p</span></code> retrieves previous command matching what you have typed. On +OS X use <code class="kbd docutils literal"><span class="pre">C-p</span></code>.</li> +<li><code class="kbd docutils literal"><span class="pre">Alt-n</span></code> retrieves next. On OS X use <code class="kbd docutils literal"><span class="pre">C-n</span></code>.</li> +<li><code class="kbd docutils literal"><span class="pre">Return</span></code> while on any previous command retrieves that command</li> </ul> </li> </ul> </div> <div class="section" id="text-colors"> -<h3>Text colors<a class="headerlink" href="#text-colors" title="Permalink to this headline">¶</a></h3> +<h3>24.6.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 <code class="docutils literal notranslate"><span class="pre">class</span></code> and -<code class="docutils literal notranslate"><span class="pre">def</span></code>, strings, and comments. For any text window, these are the cursor (when +keywords, builtin class and function names, names following <code class="docutils literal"><span class="pre">class</span></code> and +<code class="docutils literal"><span class="pre">def</span></code>, 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 @@ -572,22 +477,22 @@ text in popups and dialogs is not user-configurable.</p> </div> </div> <div class="section" id="startup-and-code-execution"> -<h2>Startup and code execution<a class="headerlink" href="#startup-and-code-execution" title="Permalink to this headline">¶</a></h2> -<p>Upon startup with the <code class="docutils literal notranslate"><span class="pre">-s</span></code> option, IDLE will execute the file referenced by -the environment variables <span class="target" id="index-5"></span><code class="xref std std-envvar docutils literal notranslate"><span class="pre">IDLESTARTUP</span></code> or <span class="target" id="index-6"></span><a class="reference internal" href="../using/cmdline.html#envvar-PYTHONSTARTUP"><code class="xref std std-envvar docutils literal notranslate"><span class="pre">PYTHONSTARTUP</span></code></a>. -IDLE first checks for <code class="docutils literal notranslate"><span class="pre">IDLESTARTUP</span></code>; if <code class="docutils literal notranslate"><span class="pre">IDLESTARTUP</span></code> is present the file -referenced is run. If <code class="docutils literal notranslate"><span class="pre">IDLESTARTUP</span></code> is not present, IDLE checks for -<code class="docutils literal notranslate"><span class="pre">PYTHONSTARTUP</span></code>. Files referenced by these environment variables are +<h2>24.6.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 <code class="docutils literal"><span class="pre">-s</span></code> option, IDLE will execute the file referenced by +the environment variables <span class="target" id="index-5"></span><code class="xref std std-envvar docutils literal"><span class="pre">IDLESTARTUP</span></code> or <span class="target" id="index-6"></span><a class="reference internal" href="../using/cmdline.html#envvar-PYTHONSTARTUP"><code class="xref std std-envvar docutils literal"><span class="pre">PYTHONSTARTUP</span></code></a>. +IDLE first checks for <code class="docutils literal"><span class="pre">IDLESTARTUP</span></code>; if <code class="docutils literal"><span class="pre">IDLESTARTUP</span></code> is present the file +referenced is run. If <code class="docutils literal"><span class="pre">IDLESTARTUP</span></code> is not present, IDLE checks for +<code class="docutils literal"><span class="pre">PYTHONSTARTUP</span></code>. 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, <code class="docutils literal notranslate"><span class="pre">Tk</span></code> also loads a startup file if it is present. Note that the -Tk file is loaded unconditionally. This additional file is <code class="docutils literal notranslate"><span class="pre">.Idle.py</span></code> and is -looked for in the user’s home directory. Statements in this file will be +<p>In addition, <code class="docutils literal"><span class="pre">Tk</span></code> also loads a startup file if it is present. Note that the +Tk file is loaded unconditionally. This additional file is <code class="docutils literal"><span class="pre">.Idle.py</span></code> 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> +functions to be used from IDLE’s Python shell.</p> <div class="section" id="command-line-usage"> -<h3>Command line usage<a class="headerlink" href="#command-line-usage" title="Permalink to this headline">¶</a></h3> -<div class="highlight-none notranslate"><div class="highlight"><pre><span></span>idle.py [-c command] [-d] [-e] [-h] [-i] [-r file] [-s] [-t title] [-] [arg] ... +<h3>24.6.3.1. Command line usage<a class="headerlink" href="#command-line-usage" title="Permalink to this headline">¶</a></h3> +<div class="highlight-none"><div class="highlight"><pre><span></span>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 @@ -602,166 +507,41 @@ functions to be used from IDLE’s Python shell.</p> </div> <p>If there are arguments:</p> <ul class="simple"> -<li><p>If <code class="docutils literal notranslate"><span class="pre">-</span></code>, <code class="docutils literal notranslate"><span class="pre">-c</span></code>, or <code class="docutils literal notranslate"><span class="pre">r</span></code> is used, all arguments are placed in -<code class="docutils literal notranslate"><span class="pre">sys.argv[1:...]</span></code> and <code class="docutils literal notranslate"><span class="pre">sys.argv[0]</span></code> is set to <code class="docutils literal notranslate"><span class="pre">''</span></code>, <code class="docutils literal notranslate"><span class="pre">'-c'</span></code>, -or <code class="docutils literal notranslate"><span class="pre">'-r'</span></code>. No editor window is opened, even if that is the default -set in the Options dialog.</p></li> -<li><p>Otherwise, arguments are files opened for editing and -<code class="docutils literal notranslate"><span class="pre">sys.argv</span></code> reflects the arguments passed to IDLE itself.</p></li> +<li>If <code class="docutils literal"><span class="pre">-</span></code>, <code class="docutils literal"><span class="pre">-c</span></code>, or <code class="docutils literal"><span class="pre">r</span></code> is used, all arguments are placed in +<code class="docutils literal"><span class="pre">sys.argv[1:...]</span></code> and <code class="docutils literal"><span class="pre">sys.argv[0]</span></code> is set to <code class="docutils literal"><span class="pre">''</span></code>, <code class="docutils literal"><span class="pre">'-c'</span></code>, +or <code class="docutils literal"><span class="pre">'-r'</span></code>. 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 +<code class="docutils literal"><span class="pre">sys.argv</span></code> reflects the arguments passed to IDLE itself.</li> </ul> </div> -<div class="section" id="startup-failure"> -<h3>Startup failure<a class="headerlink" href="#startup-failure" title="Permalink to this headline">¶</a></h3> -<p>IDLE uses a socket to communicate between the IDLE GUI process and the user -code execution process. A connection must be established whenever the Shell -starts or restarts. (The latter is indicated by a divider line that says -‘RESTART’). If the user process fails to connect to the GUI process, it -displays a <code class="docutils literal notranslate"><span class="pre">Tk</span></code> error box with a ‘cannot connect’ message that directs the -user here. It then exits.</p> -<p>A common cause of failure is a user-written file with the same name as a -standard library module, such as <em>random.py</em> and <em>tkinter.py</em>. When such a -file is located in the same directory as a file that is about to be run, -IDLE cannot import the stdlib file. The current fix is to rename the -user file.</p> -<p>Though less common than in the past, an antivirus or firewall program may -stop the connection. If the program cannot be taught to allow the -connection, then it must be turned off for IDLE to work. It is safe to -allow this internal connection because no data is visible on external -ports. A similar problem is a network mis-configuration that blocks -connections.</p> -<p>Python installation issues occasionally stop IDLE: multiple versions can -clash, or a single installation might need admin access. If one undo the -clash, or cannot or does not want to run as admin, it might be easiest to -completely remove Python and start over.</p> -<p>A zombie pythonw.exe process could be a problem. On Windows, use Task -Manager to detect and stop one. Sometimes a restart initiated by a program -crash or Keyboard Interrupt (control-C) may fail to connect. Dismissing -the error box or Restart Shell on the Shell menu may fix a temporary problem.</p> -<p>When IDLE first starts, it attempts to read user configuration files in -~/.idlerc/ (~ is one’s home directory). If there is a problem, an error -message should be displayed. Leaving aside random disk glitches, this can -be prevented by never editing the files by hand, using the configuration -dialog, under Options, instead Options. Once it happens, the solution may -be to delete one or more of the configuration files.</p> -<p>If IDLE quits with no message, and it was not started from a console, try -starting from a console (<code class="docutils literal notranslate"><span class="pre">python</span> <span class="pre">-m</span> <span class="pre">idlelib)</span></code> and see if a message appears.</p> -</div> -<div class="section" id="running-user-code"> -<h3>Running user code<a class="headerlink" href="#running-user-code" title="Permalink to this headline">¶</a></h3> -<p>With rare exceptions, the result of executing Python code with IDLE is -intended to be the same as executing the same code by the default method, -directly with Python in a text-mode system console or terminal window. -However, the different interface and operation occasionally affect -visible results. For instance, <code class="docutils literal notranslate"><span class="pre">sys.modules</span></code> starts with more entries, -and <code class="docutils literal notranslate"><span class="pre">threading.activeCount()</span></code> returns 2 instead of 1.</p> -<p>By default, IDLE runs user code in a separate OS process rather than in -the user interface process that runs the shell and editor. In the execution -process, it replaces <code class="docutils literal notranslate"><span class="pre">sys.stdin</span></code>, <code class="docutils literal notranslate"><span class="pre">sys.stdout</span></code>, and <code class="docutils literal notranslate"><span class="pre">sys.stderr</span></code> -with objects that get input from and send output to the Shell window. -The original values stored in <code class="docutils literal notranslate"><span class="pre">sys.__stdin__</span></code>, <code class="docutils literal notranslate"><span class="pre">sys.__stdout__</span></code>, and -<code class="docutils literal notranslate"><span class="pre">sys.__stderr__</span></code> are not touched, but may be <code class="docutils literal notranslate"><span class="pre">None</span></code>.</p> -<p>When Shell has the focus, it controls the keyboard and screen. This is -normally transparent, but functions that directly access the keyboard -and screen will not work. These include system-specific functions that -determine whether a key has been pressed and if so, which.</p> -<p>IDLE’s standard stream replacements are not inherited by subprocesses -created in the execution process, whether directly by user code or by modules -such as multiprocessing. If such subprocess use <code class="docutils literal notranslate"><span class="pre">input</span></code> from sys.stdin -or <code class="docutils literal notranslate"><span class="pre">print</span></code> or <code class="docutils literal notranslate"><span class="pre">write</span></code> to sys.stdout or sys.stderr, -IDLE should be started in a command line window. The secondary subprocess -will then be attached to that window for input and output.</p> -<p>The IDLE code running in the execution process adds frames to the call stack -that would not be there otherwise. IDLE wraps <code class="docutils literal notranslate"><span class="pre">sys.getrecursionlimit</span></code> and -<code class="docutils literal notranslate"><span class="pre">sys.setrecursionlimit</span></code> to reduce the effect of the additional stack frames.</p> -<p>If <code class="docutils literal notranslate"><span class="pre">sys</span></code> is reset by user code, such as with <code class="docutils literal notranslate"><span class="pre">importlib.reload(sys)</span></code>, -IDLE’s changes are lost and input from the keyboard and output to the screen -will not work correctly.</p> -<p>When user code raises SystemExit either directly or by calling sys.exit, IDLE -returns to a Shell prompt instead of exiting.</p> -</div> -<div class="section" id="user-output-in-shell"> -<h3>User output in Shell<a class="headerlink" href="#user-output-in-shell" title="Permalink to this headline">¶</a></h3> -<p>When a program outputs text, the result is determined by the -corresponding output device. When IDLE executes user code, <code class="docutils literal notranslate"><span class="pre">sys.stdout</span></code> -and <code class="docutils literal notranslate"><span class="pre">sys.stderr</span></code> are connected to the display area of IDLE’s Shell. Some of -its features are inherited from the underlying Tk Text widget. Others -are programmed additions. Where it matters, Shell is designed for development -rather than production runs.</p> -<p>For instance, Shell never throws away output. A program that sends unlimited -output to Shell will eventually fill memory, resulting in a memory error. -In contrast, some system text windows only keep the last n lines of output. -A Windows console, for instance, keeps a user-settable 1 to 9999 lines, -with 300 the default.</p> -<p>A Tk Text widget, and hence IDLE’s Shell, displays characters (codepoints) in -the BMP (Basic Multilingual Plane) subset of Unicode. Which characters are -displayed with a proper glyph and which with a replacement box depends on the -operating system and installed fonts. Tab characters cause the following text -to begin after the next tab stop. (They occur every 8 ‘characters’). Newline -characters cause following text to appear on a new line. Other control -characters are ignored or displayed as a space, box, or something else, -depending on the operating system and font. (Moving the text cursor through -such output with arrow keys may exhibit some surprising spacing behavior.)</p> -<div class="highlight-python3 notranslate"><div class="highlight"><pre><span></span><span class="gp">>>> </span><span class="n">s</span> <span class="o">=</span> <span class="s1">'a</span><span class="se">\t</span><span class="s1">b</span><span class="se">\a</span><span class="s1"><</span><span class="se">\x02</span><span class="s1">><</span><span class="se">\r</span><span class="s1">></span><span class="se">\b</span><span class="s1">c</span><span class="se">\n</span><span class="s1">d'</span> <span class="c1"># Enter 22 chars.</span> -<span class="gp">>>> </span><span class="nb">len</span><span class="p">(</span><span class="n">s</span><span class="p">)</span> -<span class="go">14</span> -<span class="gp">>>> </span><span class="n">s</span> <span class="c1"># Display repr(s)</span> -<span class="go">'a\tb\x07<\x02><\r>\x08c\nd'</span> -<span class="gp">>>> </span><span class="nb">print</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="n">end</span><span class="o">=</span><span class="s1">''</span><span class="p">)</span> <span class="c1"># Display s as is.</span> -<span class="go"># Result varies by OS and font. Try it.</span> -</pre></div> -</div> -<p>The <code class="docutils literal notranslate"><span class="pre">repr</span></code> function is used for interactive echo of expression -values. It returns an altered version of the input string in which -control codes, some BMP codepoints, and all non-BMP codepoints are -replaced with escape codes. As demonstrated above, it allows one to -identify the characters in a string, regardless of how they are displayed.</p> -<p>Normal and error output are generally kept separate (on separate lines) -from code input and each other. They each get different highlight colors.</p> -<p>For SyntaxError tracebacks, the normal ‘^’ marking where the error was -detected is replaced by coloring the text with an error highlight. -When code run from a file causes other exceptions, one may right click -on a traceback line to jump to the corresponding line in an IDLE editor. -The file will be opened if necessary.</p> -<p>Shell has a special facility for squeezing output lines down to a -‘Squeezed text’ label. This is done automatically -for output over N lines (N = 50 by default). -N can be changed in the PyShell section of the General -page of the Settings dialog. Output with fewer lines can be squeezed by -right clicking on the output. This can be useful lines long enough to slow -down scrolling.</p> -<p>Squeezed output is expanded in place by double-clicking the label. -It can also be sent to the clipboard or a separate view window by -right-clicking the label.</p> -</div> -<div class="section" id="developing-tkinter-applications"> -<h3>Developing tkinter applications<a class="headerlink" href="#developing-tkinter-applications" title="Permalink to this headline">¶</a></h3> -<p>IDLE is intentionally different from standard Python in order to -facilitate development of tkinter programs. Enter <code class="docutils literal notranslate"><span class="pre">import</span> <span class="pre">tkinter</span> <span class="pre">as</span> <span class="pre">tk;</span> -<span class="pre">root</span> <span class="pre">=</span> <span class="pre">tk.Tk()</span></code> in standard Python and nothing appears. Enter the same -in IDLE and a tk window appears. In standard Python, one must also enter -<code class="docutils literal notranslate"><span class="pre">root.update()</span></code> to see the window. IDLE does the equivalent in the -background, about 20 times a second, which is about every 50 milliseconds. -Next enter <code class="docutils literal notranslate"><span class="pre">b</span> <span class="pre">=</span> <span class="pre">tk.Button(root,</span> <span class="pre">text='button');</span> <span class="pre">b.pack()</span></code>. Again, -nothing visibly changes in standard Python until one enters <code class="docutils literal notranslate"><span class="pre">root.update()</span></code>.</p> -<p>Most tkinter programs run <code class="docutils literal notranslate"><span class="pre">root.mainloop()</span></code>, which usually does not -return until the tk app is destroyed. If the program is run with -<code class="docutils literal notranslate"><span class="pre">python</span> <span class="pre">-i</span></code> or from an IDLE editor, a <code class="docutils literal notranslate"><span class="pre">>>></span></code> shell prompt does not -appear until <code class="docutils literal notranslate"><span class="pre">mainloop()</span></code> returns, at which time there is nothing left -to interact with.</p> -<p>When running a tkinter program from an IDLE editor, one can comment out -the mainloop call. One then gets a shell prompt immediately and can -interact with the live application. One just has to remember to -re-enable the mainloop call when running in standard Python.</p> +<div class="section" id="idle-console-differences"> +<h3>24.6.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 visible results. For instance, +<code class="docutils literal"><span class="pre">sys.modules</span></code> starts with more entries.</p> +<p>IDLE also replaces <code class="docutils literal"><span class="pre">sys.stdin</span></code>, <code class="docutils literal"><span class="pre">sys.stdout</span></code>, and <code class="docutils literal"><span class="pre">sys.stderr</span></code> with +objects that get input from and send output to the Shell window. +When this window has the focus, it controls the keyboard and screen. +This is normally transparent, but functions that directly access the keyboard +and screen will not work. If <code class="docutils literal"><span class="pre">sys</span></code> is reset with <code class="docutils literal"><span class="pre">reload(sys)</span></code>, +IDLE’s changes are lost and things like <code class="docutils literal"><span class="pre">input</span></code>, <code class="docutils literal"><span class="pre">raw_input</span></code>, and +<code class="docutils literal"><span class="pre">print</span></code> will not work correctly.</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. IDLE uses +<code class="docutils literal"><span class="pre">exec</span></code> to run each statement. As a result, <code class="docutils literal"><span class="pre">'__builtins__'</span></code> is always +defined for each statement.</p> </div> <div class="section" id="running-without-a-subprocess"> -<h3>Running without a subprocess<a class="headerlink" href="#running-without-a-subprocess" title="Permalink to this headline">¶</a></h3> +<h3>24.6.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 connection or misconfiguration of +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 @@ -775,60 +555,44 @@ 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">Deprecated since version 3.4.</span></p> +<p><span class="versionmodified">Deprecated since version 3.4.</span></p> </div> </div> </div> <div class="section" id="help-and-preferences"> -<h2>Help and preferences<a class="headerlink" href="#help-and-preferences" title="Permalink to this headline">¶</a></h2> -<div class="section" id="help-sources"> -<span id="id6"></span><h3>Help sources<a class="headerlink" href="#help-sources" title="Permalink to this headline">¶</a></h3> -<p>Help menu entry “IDLE Help†displays a formatted html version of the -IDLE chapter of the Library Reference. The result, in a read-only -tkinter text window, is close to what one sees in a web browser. -Navigate through the text with a mousewheel, -the scrollbar, or up and down arrow keys held down. -Or click the TOC (Table of Contents) button and select a section -header in the opened box.</p> -<p>Help menu entry “Python Docs†opens the extensive sources of help, -including tutorials, available at docs.python.org/x.y, where ‘x.y’ -is the currently running Python version. If your system -has an off-line copy of the docs (this may be an installation option), -that will be opened instead.</p> -<p>Selected URLs can be added or removed from the help menu at any time using the -General tab of the Configure IDLE dialog .</p> +<h2>24.6.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>24.6.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"> -<span id="preferences"></span><h3>Setting preferences<a class="headerlink" href="#setting-preferences" title="Permalink to this headline">¶</a></h3> +<h3>24.6.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. -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> -<p>On the Font tab, see the text sample for the effect of font face and size -on multiple characters in multiple languages. Edit the sample to add -other characters of personal interest. Use the sample to select -monospaced fonts. If particular characters have problems in Shell or an -editor, add them to the top of the sample and try changing first size -and then font.</p> -<p>On the Highlights and Keys tab, select a built-in or custom color theme -and key set. To use a newer built-in color theme or key set with older -IDLEs, save it as a new custom theme or key set and it well be accessible -to older IDLEs.</p> -</div> -<div class="section" id="idle-on-macos"> -<h3>IDLE on macOS<a class="headerlink" href="#idle-on-macos" title="Permalink to this headline">¶</a></h3> -<p>Under System Preferences: Dock, one can set “Prefer tabs when opening -documents†to “Alwaysâ€. This setting is not compatible with the tk/tkinter -GUI framework used by IDLE, and it breaks a few IDLE features.</p> +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>Extensions<a class="headerlink" href="#extensions" title="Permalink to this headline">¶</a></h3> -<p>IDLE contains an extension facility. Preferences for extensions can be -changed with the Extensions tab of the preferences dialog. See the -beginning of config-extensions.def in the idlelib directory for further -information. The only current default extension is zzdummy, an example -also used for testing.</p> +<h3>24.6.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> @@ -839,47 +603,40 @@ also used for testing.</p> </div> <div class="sphinxsidebar" role="navigation" aria-label="main navigation"> <div class="sphinxsidebarwrapper"> - <h3><a href="../contents.html">Table of Contents</a></h3> + <h3><a href="../contents.html">Table Of Contents</a></h3> <ul> -<li><a class="reference internal" href="#">IDLE</a><ul> -<li><a class="reference internal" href="#menus">Menus</a><ul> -<li><a class="reference internal" href="#file-menu-shell-and-editor">File menu (Shell and Editor)</a></li> -<li><a class="reference internal" href="#edit-menu-shell-and-editor">Edit menu (Shell and Editor)</a></li> -<li><a class="reference internal" href="#format-menu-editor-window-only">Format menu (Editor window only)</a></li> -<li><a class="reference internal" href="#run-menu-editor-window-only">Run menu (Editor window only)</a></li> -<li><a class="reference internal" href="#shell-menu-shell-window-only">Shell menu (Shell window only)</a></li> -<li><a class="reference internal" href="#debug-menu-shell-window-only">Debug menu (Shell window only)</a></li> -<li><a class="reference internal" href="#options-menu-shell-and-editor">Options menu (Shell and Editor)</a></li> -<li><a class="reference internal" href="#window-menu-shell-and-editor">Window menu (Shell and Editor)</a></li> -<li><a class="reference internal" href="#help-menu-shell-and-editor">Help menu (Shell and Editor)</a></li> -<li><a class="reference internal" href="#context-menus">Context Menus</a></li> +<li><a class="reference internal" href="#">24.6. IDLE</a><ul> +<li><a class="reference internal" href="#menus">24.6.1. Menus</a><ul> +<li><a class="reference internal" href="#file-menu-shell-and-editor">24.6.1.1. File menu (Shell and Editor)</a></li> +<li><a class="reference internal" href="#edit-menu-shell-and-editor">24.6.1.2. Edit menu (Shell and Editor)</a></li> +<li><a class="reference internal" href="#format-menu-editor-window-only">24.6.1.3. Format menu (Editor window only)</a></li> +<li><a class="reference internal" href="#run-menu-editor-window-only">24.6.1.4. Run menu (Editor window only)</a></li> +<li><a class="reference internal" href="#shell-menu-shell-window-only">24.6.1.5. Shell menu (Shell window only)</a></li> +<li><a class="reference internal" href="#debug-menu-shell-window-only">24.6.1.6. Debug menu (Shell window only)</a></li> +<li><a class="reference internal" href="#options-menu-shell-and-editor">24.6.1.7. Options menu (Shell and Editor)</a></li> +<li><a class="reference internal" href="#window-menu-shell-and-editor">24.6.1.8. Window menu (Shell and Editor)</a></li> +<li><a class="reference internal" href="#help-menu-shell-and-editor">24.6.1.9. Help menu (Shell and Editor)</a></li> +<li><a class="reference internal" href="#context-menus">24.6.1.10. Context Menus</a></li> </ul> </li> -<li><a class="reference internal" href="#editing-and-navigation">Editing and navigation</a><ul> -<li><a class="reference internal" href="#editor-windows">Editor windows</a></li> -<li><a class="reference internal" href="#key-bindings">Key bindings</a></li> -<li><a class="reference internal" href="#automatic-indentation">Automatic indentation</a></li> -<li><a class="reference internal" href="#completions">Completions</a></li> -<li><a class="reference internal" href="#calltips">Calltips</a></li> -<li><a class="reference internal" href="#code-context">Code Context</a></li> -<li><a class="reference internal" href="#python-shell-window">Python Shell window</a></li> -<li><a class="reference internal" href="#text-colors">Text colors</a></li> +<li><a class="reference internal" href="#editing-and-navigation">24.6.2. Editing and navigation</a><ul> +<li><a class="reference internal" href="#automatic-indentation">24.6.2.1. Automatic indentation</a></li> +<li><a class="reference internal" href="#completions">24.6.2.2. Completions</a></li> +<li><a class="reference internal" href="#calltips">24.6.2.3. Calltips</a></li> +<li><a class="reference internal" href="#python-shell-window">24.6.2.4. Python Shell window</a></li> +<li><a class="reference internal" href="#text-colors">24.6.2.5. Text colors</a></li> </ul> </li> -<li><a class="reference internal" href="#startup-and-code-execution">Startup and code execution</a><ul> -<li><a class="reference internal" href="#command-line-usage">Command line usage</a></li> -<li><a class="reference internal" href="#startup-failure">Startup failure</a></li> -<li><a class="reference internal" href="#running-user-code">Running user code</a></li> -<li><a class="reference internal" href="#user-output-in-shell">User output in Shell</a></li> -<li><a class="reference internal" href="#developing-tkinter-applications">Developing tkinter applications</a></li> -<li><a class="reference internal" href="#running-without-a-subprocess">Running without a subprocess</a></li> +<li><a class="reference internal" href="#startup-and-code-execution">24.6.3. Startup and code execution</a><ul> +<li><a class="reference internal" href="#command-line-usage">24.6.3.1. Command line usage</a></li> +<li><a class="reference internal" href="#idle-console-differences">24.6.3.2. IDLE-console differences</a></li> +<li><a class="reference internal" href="#running-without-a-subprocess">24.6.3.3. Running without a subprocess</a></li> </ul> </li> -<li><a class="reference internal" href="#help-and-preferences">Help and preferences</a><ul> -<li><a class="reference internal" href="#help-sources">Help sources</a></li> -<li><a class="reference internal" href="#setting-preferences">Setting preferences</a></li> -<li><a class="reference internal" href="#idle-on-macos">IDLE on macOS</a></li> -<li><a class="reference internal" href="#extensions">Extensions</a></li> +<li><a class="reference internal" href="#help-and-preferences">24.6.4. Help and preferences</a><ul> +<li><a class="reference internal" href="#additional-help-sources">24.6.4.1. Additional help sources</a></li> +<li><a class="reference internal" href="#setting-preferences">24.6.4.2. Setting preferences</a></li> +<li><a class="reference internal" href="#extensions">24.6.4.3. Extensions</a></li> </ul> </li> </ul> @@ -887,22 +644,31 @@ also used for testing.</p> </ul> <h4>Previous topic</h4> - <p class="topless"><a href="tkinter.tix.html" - title="previous chapter"><code class="xref py py-mod docutils literal notranslate"><span class="pre">tkinter.tix</span></code> — Extension widgets for Tk</a></p> + <p class="topless"><a href="turtle.html" + title="previous chapter">24.5. <code class="docutils literal"><span class="pre">turtle</span></code> — Turtle graphics for Tk</a></p> <h4>Next topic</h4> <p class="topless"><a href="othergui.html" - title="next chapter">Other Graphical User Interface Packages</a></p> - <div role="note" aria-label="source link"> - <h3>This Page</h3> - <ul class="this-page-menu"> - <li><a href="../bugs.html">Report a Bug</a></li> - <li> - <a href="https://github.com/python/cpython/blob/master/Doc/library/idle.rst" - rel="nofollow">Show Source - </a> - </li> - </ul> - </div> + title="next chapter">24.7. 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" role="search"> + <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> @@ -917,54 +683,32 @@ also used for testing.</p> <a href="../py-modindex.html" title="Python Module Index" >modules</a> |</li> <li class="right" > - <a href="othergui.html" title="Other Graphical User Interface Packages" + <a href="othergui.html" title="24.7. Other Graphical User Interface Packages" >next</a> |</li> <li class="right" > - <a href="tkinter.tix.html" title="tkinter.tix — Extension widgets for Tk" + <a href="turtle.html" title="24.5. turtle — Turtle graphics for Tk" >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.9.0a1 Documentation</a> » - </li> - - <li class="nav-item nav-item-1"><a href="index.html" >The Python Standard Library</a> »</li> - <li class="nav-item nav-item-2"><a href="tk.html" >Graphical User Interfaces with Tk</a> »</li> - <li class="right"> - - - <div class="inline-search" style="display: none" role="search"> - <form class="inline-search" action="../search.html" method="get"> - <input placeholder="Quick search" 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> - </div> - <script type="text/javascript">$('.inline-search').show(0);</script> - | - </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">Python 2.7.12 documentation</a> » + </li> + + <li class="nav-item nav-item-1"><a href="index.html" >The Python Standard Library</a> »</li> + <li class="nav-item nav-item-2"><a href="tk.html" >24. Graphical User Interfaces with Tk</a> »</li> </ul> </div> <div class="footer"> - © <a href="../copyright.html">Copyright</a> 2001-2019, Python Software Foundation. + © <a href="../copyright.html">Copyright</a> 1990-2017, 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 /> + <a href="https://www.python.org/psf/donations/">Please donate.</a> <br /> - - Last updated on Nov 24, 2019. - <a href="https://docs.python.org/3/bugs.html">Found a bug</a>? + Last updated on Sep 12, 2016. + <a href="../bugs.html">Found a bug</a>? <br /> - - Created using <a href="http://sphinx.pocoo.org/">Sphinx</a> 2.1.1. + Created using <a href="http://sphinx.pocoo.org/">Sphinx</a> 1.3.6. </div> </body> diff --git a/Lib/idlelib/help.py b/Lib/idlelib/help.py index 9f63ea0..3ab4851 100644 --- a/Lib/idlelib/help.py +++ b/Lib/idlelib/help.py @@ -2,9 +2,9 @@ Contents are subject to revision at any time, without notice. -Help => About IDLE: display About Idle dialog +Help => About IDLE: diplay About Idle dialog -<to be moved here from help_about.py> +<to be moved here from aboutDialog.py> Help => IDLE Help: Display help.html with proper formatting. @@ -24,15 +24,16 @@ 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, isfile, join +from HTMLParser import HTMLParser +from os.path import abspath, dirname, isdir, isfile, join from platform import python_version +from Tkinter import Tk, Toplevel, Frame, Text, Scrollbar, Menu, Menubutton +import tkFont as tkfont +from idlelib.configHandler import idleConf -from tkinter import Toplevel, Frame, Text, Menu -from tkinter.ttk import Menubutton, Scrollbar -from tkinter import font as tkfont - -from idlelib.config import idleConf +use_ttk = False # until available to import +if use_ttk: + from tkinter.ttk import Menubutton ## About IDLE ## @@ -49,23 +50,21 @@ class HelpParser(HTMLParser): the handle_starttag and handle_endtags methods, might have to also. """ 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 # Exclude html page navigation. - self.hdrlink = False # Exclude html header links. - self.level = 0 # Track indentation level. - self.pre = False # Displaying preformatted text? - self.hprefix = '' # Heading prefix (like '25.5'?) to remove. - self.nested_dl = False # In a nested <dl>? - self.simplelist = False # In a simple list (no double spacing)? - self.toc = [] # Pair headers with text indexes for toc. - self.header = '' # Text within header tags for toc. - self.prevtag = None # Previous tag info (opener?, tag). + HTMLParser.__init__(self) + 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): - "Change indent (+1, 0, -1) and tags." self.level += amt self.tags = '' if self.level == 0 else 'l'+str(self.level) @@ -77,14 +76,11 @@ class HelpParser(HTMLParser): class_ = v s = '' if tag == 'div' and class_ == 'section': - self.show = True # Start main content. + self.show = True # start of main content elif tag == 'div' and class_ == 'sphinxsidebar': - self.show = False # End main content. - elif tag == 'p' and self.prevtag and not self.prevtag[0]: - # Begin a new block for <p> tags after a closed tag. - # Avoid extra lines, e.g. after <pre> tags. - lastline = self.text.get('end-1c linestart', 'end-1c') - s = '\n\n' if lastline and not lastline.isspace() else '\n' + 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': @@ -104,7 +100,7 @@ class HelpParser(HTMLParser): 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. + s = '\n\n' if not self.nested_dl else '\n' # avoid extra line self.nested_dl = False elif tag == 'dd': self.indent() @@ -125,18 +121,13 @@ class HelpParser(HTMLParser): self.tags = tag if self.show: self.text.insert('end', s, (self.tags, self.chartags)) - self.prevtag = (True, tag) def handle_endtag(self, tag): "Handle endtags in help.html." if tag in ['h1', 'h2', 'h3']: - assert self.level == 0 + self.indent(0) # clear tag, reset indent if self.show: - indent = (' ' if tag == 'h3' else - ' ' if tag == 'h2' else - '') - self.toc.append((indent+self.header, self.text.index('insert'))) - self.tags = '' + self.toc.append((self.header, self.text.index('insert'))) elif tag in ['span', 'em']: self.chartags = '' elif tag == 'a': @@ -145,25 +136,24 @@ class HelpParser(HTMLParser): self.pre = False self.tags = '' elif tag in ['ul', 'dd', 'ol']: - self.indent(-1) - self.prevtag = (False, tag) + 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': - try: - self.hprefix = d[0:d.index(' ')] - except ValueError: - self.hprefix = '' - if self.tags in ['h1', 'h2', 'h3']: - if (self.hprefix != '' and - d[0:len(self.hprefix)] == self.hprefix): - d = d[len(self.hprefix):] - self.header += d.strip() + 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)) + def handle_charref(self, name): + if self.show: + self.text.insert('end', unichr(int(name))) + class HelpText(Text): "Display help.html." @@ -171,7 +161,7 @@ class HelpText(Text): "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. + 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) @@ -191,8 +181,8 @@ class HelpText(Text): self.tag_configure('l4', lmargin1=100, lmargin2=100) self.parser = HelpParser(self) - with open(filename, encoding='utf-8') as f: - contents = f.read() + with open(filename) as f: + contents = f.read().decode(encoding='utf-8') self.parser.feed(contents) self['state'] = 'disabled' @@ -211,17 +201,15 @@ class HelpFrame(Frame): "Display html text, scrollbar, and toc." def __init__(self, parent, filename): Frame.__init__(self, parent) - self.text = text = HelpText(self, filename) + text = HelpText(self, filename) self['background'] = text['background'] - self.toc = toc = self.toc_menu(text) - self.scroll = scroll = Scrollbar(self, command=text.yview) + scroll = Scrollbar(self, command=text.yview) text['yscrollcommand'] = scroll.set - self.rowconfigure(0, weight=1) - self.columnconfigure(1, weight=1) # Only expand the text widget. - toc.grid(row=0, column=0, sticky='nw') - text.grid(row=0, column=1, sticky='nsew') - scroll.grid(row=0, column=2, sticky='ns') + 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." @@ -247,46 +235,43 @@ class HelpWindow(Toplevel): def copy_strip(): """Copy idle.html to idlelib/help.html, stripping trailing whitespace. - Files with trailing whitespace cannot be pushed to the git cpython + Files with trailing whitespace cannot be pushed to the hg cpython repository. For 3.x (on Windows), help.html is generated, after - editing idle.rst on the master branch, with + editing idle.rst in the earliest maintenance version, with sphinx-build -bhtml . build/html python_d.exe -c "from idlelib.help import copy_strip; copy_strip()" - Check build/html/library/idle.html, the help.html diff, and the text - displayed by Help => IDLE Help. Add a blurb and create a PR. - - It can be worthwhile to occasionally generate help.html without - touching idle.rst. Changes to the master version and to the doc - build system may result in changes that should not changed - the displayed text, but might break HelpParser. - - As long as master and maintenance versions of idle.rst remain the - same, help.html can be backported. The internal Python version - number is not displayed. If maintenance idle.rst diverges from - the master version, then instead of backporting help.html from - master, repeat the procedure above to generate a maintenance - version. + After refreshing TortoiseHG workshop to generate a diff, + check both the diff and displayed text. Push the diff along with + the idle.rst change and merge both into default (or an intermediate + maintenance version). + + When the 'earlist' version gets its final maintenance release, + do an update as described above, without editing idle.rst, to + rebase help.html on the next version of idle.rst. Do not worry + about version changes as version is not displayed. Examine other + changes and the result of Help -> IDLE Help. + + If maintenance and default versions of idle.rst diverge, and + merging does not go smoothly, then consider generating + separate help.html files from separate idle.htmls. """ src = join(abspath(dirname(dirname(dirname(__file__)))), - 'Doc', 'build', 'html', 'library', 'idle.html') + 'Doc', 'build', 'html', 'library', 'idle.html') dst = join(abspath(dirname(__file__)), 'help.html') - with open(src, 'rb') as inn,\ - open(dst, 'wb') as out: + with open(src, 'r') as inn,\ + open(dst, 'w') as out: for line in inn: - out.write(line.rstrip() + b'\n') - print(f'{src} copied to {dst}') + out.write(line.rstrip() + '\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. + # try copy_strip, present message return HelpWindow(parent, filename, 'IDLE Help (%s)' % python_version()) if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_help', verbosity=2, exit=False) - from idlelib.idle_test.htest import run run(show_idlehelp) diff --git a/Lib/idlelib/help.txt b/Lib/idlelib/help.txt new file mode 100644 index 0000000..296c78b --- /dev/null +++ b/Lib/idlelib/help.txt @@ -0,0 +1,302 @@ +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 +---------------------------------------------------------------------- + +[See the end of this file for ** TIPS ** on using IDLE !!] + +File Menu: + + New File -- Create a new 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 + +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 + +Shell Menu (only in Shell window): + + View Last Restart -- Scroll the shell window to the last restart + Restart Shell -- Restart the interpreter with a fresh environment + +Debug Menu (only in Shell window): + + 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 + +Options Menu: + + 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, open the + configuration dialog by selecting Preferences + in the application menu. + --- + 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.) + +Window 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). + +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, + 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 + +Shell context menu (Right-click / Control-click on OS X in Shell 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 + --- + Go to file/line -- Same as in Debug menu + + +** TIPS ** +========== + +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. + + 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. + +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. + +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.) + + 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.) + +Python Shell window: + + Control-c interrupts executing command. + Control-d sends end-of-file; closes window if typed at >>> prompt. + + 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. + + 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. + + Python default syntax colors: + + Keywords orange + Builtins royal purple + Strings green + Comments red + Definitions blue + + Shell default colors: + + 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. + +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. + +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 diff --git a/Lib/idlelib/help_about.py b/Lib/idlelib/help_about.py deleted file mode 100644 index 64b13ac..0000000 --- a/Lib/idlelib/help_about.py +++ /dev/null @@ -1,207 +0,0 @@ -"""About Dialog for IDLE - -""" -import os -import sys -from platform import python_version, architecture - -from tkinter import Toplevel, Frame, Label, Button, PhotoImage -from tkinter import SUNKEN, TOP, BOTTOM, LEFT, X, BOTH, W, EW, NSEW, E - -from idlelib import textview - - -def build_bits(): - "Return bits for platform." - if sys.platform == 'darwin': - return '64' if sys.maxsize > 2**32 else '32' - else: - return architecture()[0][:2] - - -class AboutDialog(Toplevel): - """Modal about dialog for idle - - """ - def __init__(self, parent, title=None, *, _htest=False, _utest=False): - """Create popup, do not return until tk widget destroyed. - - parent - parent of this dialog - title - string which is title of popup dialog - _htest - bool, change box location when running htest - _utest - bool, don't wait_window when running unittest - """ - Toplevel.__init__(self, parent) - self.configure(borderwidth=5) - # 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 = "#bbbbbb" - self.fg = "#000000" - self.create_widgets() - self.resizable(height=False, width=False) - self.title(title or - f'About IDLE {python_version()} ({build_bits()} bit)') - self.transient(parent) - self.grab_set() - self.protocol("WM_DELETE_WINDOW", self.ok) - self.parent = parent - self.button_ok.focus_set() - self.bind('<Return>', self.ok) # dismiss dialog - self.bind('<Escape>', self.ok) # dismiss dialog - self._current_textview = None - self._utest = _utest - - if not _utest: - self.deiconify() - self.wait_window() - - def create_widgets(self): - frame = Frame(self, borderwidth=2, relief=SUNKEN) - frame_buttons = Frame(self) - frame_buttons.pack(side=BOTTOM, fill=X) - frame.pack(side=TOP, expand=True, fill=BOTH) - self.button_ok = Button(frame_buttons, text='Close', - command=self.ok) - self.button_ok.pack(padx=5, pady=5) - - frame_background = Frame(frame, bg=self.bg) - frame_background.pack(expand=True, fill=BOTH) - - header = Label(frame_background, text='IDLE', fg=self.fg, - bg=self.bg, font=('courier', 24, 'bold')) - header.grid(row=0, column=0, sticky=E, padx=10, pady=10) - - tk_patchlevel = self.tk.call('info', 'patchlevel') - ext = '.png' if tk_patchlevel >= '8.6' else '.gif' - icon = os.path.join(os.path.abspath(os.path.dirname(__file__)), - 'Icons', f'idle_48{ext}') - self.icon_image = PhotoImage(master=self._root(), file=icon) - logo = Label(frame_background, image=self.icon_image, bg=self.bg) - logo.grid(row=0, column=0, sticky=W, rowspan=2, padx=10, pady=10) - - byline_text = "Python's Integrated Development\nand Learning Environment" + 5*'\n' - byline = Label(frame_background, text=byline_text, justify=LEFT, - fg=self.fg, bg=self.bg) - byline.grid(row=2, column=0, sticky=W, columnspan=3, padx=10, pady=5) - email = Label(frame_background, text='email: idle-dev@python.org', - justify=LEFT, fg=self.fg, bg=self.bg) - email.grid(row=6, column=0, columnspan=2, sticky=W, padx=10, pady=0) - docs = Label(frame_background, text='https://docs.python.org/' + - python_version()[:3] + '/library/idle.html', - justify=LEFT, fg=self.fg, bg=self.bg) - docs.grid(row=7, column=0, columnspan=2, sticky=W, padx=10, pady=0) - - Frame(frame_background, borderwidth=1, relief=SUNKEN, - height=2, bg=self.bg).grid(row=8, column=0, sticky=EW, - columnspan=3, padx=5, pady=5) - - pyver = Label(frame_background, - text='Python version: ' + python_version(), - fg=self.fg, bg=self.bg) - pyver.grid(row=9, column=0, sticky=W, padx=10, pady=0) - tkver = Label(frame_background, text='Tk version: ' + tk_patchlevel, - fg=self.fg, bg=self.bg) - tkver.grid(row=9, column=1, sticky=W, padx=2, pady=0) - py_buttons = Frame(frame_background, bg=self.bg) - py_buttons.grid(row=10, column=0, columnspan=2, sticky=NSEW) - self.py_license = Button(py_buttons, text='License', width=8, - highlightbackground=self.bg, - command=self.show_py_license) - self.py_license.pack(side=LEFT, padx=10, pady=10) - self.py_copyright = Button(py_buttons, text='Copyright', width=8, - highlightbackground=self.bg, - command=self.show_py_copyright) - self.py_copyright.pack(side=LEFT, padx=10, pady=10) - self.py_credits = Button(py_buttons, text='Credits', width=8, - highlightbackground=self.bg, - command=self.show_py_credits) - self.py_credits.pack(side=LEFT, padx=10, pady=10) - - Frame(frame_background, borderwidth=1, relief=SUNKEN, - height=2, bg=self.bg).grid(row=11, column=0, sticky=EW, - columnspan=3, padx=5, pady=5) - - idlever = Label(frame_background, - text='IDLE version: ' + python_version(), - fg=self.fg, bg=self.bg) - idlever.grid(row=12, column=0, sticky=W, padx=10, pady=0) - idle_buttons = Frame(frame_background, bg=self.bg) - idle_buttons.grid(row=13, column=0, columnspan=3, sticky=NSEW) - self.readme = Button(idle_buttons, text='README', width=8, - highlightbackground=self.bg, - command=self.show_readme) - self.readme.pack(side=LEFT, padx=10, pady=10) - self.idle_news = Button(idle_buttons, text='NEWS', width=8, - highlightbackground=self.bg, - command=self.show_idle_news) - self.idle_news.pack(side=LEFT, padx=10, pady=10) - self.idle_credits = Button(idle_buttons, text='Credits', width=8, - highlightbackground=self.bg, - command=self.show_idle_credits) - self.idle_credits.pack(side=LEFT, padx=10, pady=10) - - # License, copyright, and credits are of type _sitebuiltins._Printer - def show_py_license(self): - "Handle License button event." - self.display_printer_text('About - License', license) - - def show_py_copyright(self): - "Handle Copyright button event." - self.display_printer_text('About - Copyright', copyright) - - def show_py_credits(self): - "Handle Python Credits button event." - self.display_printer_text('About - Python Credits', credits) - - # Encode CREDITS.txt to utf-8 for proper version of Loewis. - # Specify others as ascii until need utf-8, so catch errors. - def show_idle_credits(self): - "Handle Idle Credits button event." - self.display_file_text('About - Credits', 'CREDITS.txt', 'utf-8') - - def show_readme(self): - "Handle Readme button event." - self.display_file_text('About - Readme', 'README.txt', 'ascii') - - def show_idle_news(self): - "Handle News button event." - self.display_file_text('About - NEWS', 'NEWS.txt', 'utf-8') - - def display_printer_text(self, title, printer): - """Create textview for built-in constants. - - Built-in constants have type _sitebuiltins._Printer. The - text is extracted from the built-in and then sent to a text - viewer with self as the parent and title as the title of - the popup. - """ - printer._Printer__setup() - text = '\n'.join(printer._Printer__lines) - self._current_textview = textview.view_text( - self, title, text, _utest=self._utest) - - def display_file_text(self, title, filename, encoding=None): - """Create textview for filename. - - The filename needs to be in the current directory. The path - is sent to a text viewer with self as the parent, title as - the title of the popup, and the file encoding. - """ - fn = os.path.join(os.path.abspath(os.path.dirname(__file__)), filename) - self._current_textview = textview.view_file( - self, title, fn, encoding, _utest=self._utest) - - def ok(self, event=None): - "Dismiss help_about dialog." - self.grab_release() - self.destroy() - - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_help_about', verbosity=2, exit=False) - - from idlelib.idle_test.htest import run - run(AboutDialog) diff --git a/Lib/idlelib/idle.bat b/Lib/idlelib/idle.bat index e77b96e..e77b96e 100644..100755 --- a/Lib/idlelib/idle.bat +++ b/Lib/idlelib/idle.bat diff --git a/Lib/idlelib/idle.py b/Lib/idlelib/idle.py index 485d5a7..141534d 100644 --- a/Lib/idlelib/idle.py +++ b/Lib/idlelib/idle.py @@ -1,7 +1,6 @@ import os.path import sys - # Enable running IDLE with idlelib in a non-standard location. # This was once used to run development versions of IDLE. # Because PEP 434 declared idle.py a public interface, @@ -10,5 +9,5 @@ idlelib_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if idlelib_dir not in sys.path: sys.path.insert(0, idlelib_dir) -from idlelib.pyshell import main # This is subject to change +from idlelib.PyShell import main # This is subject to change main() diff --git a/Lib/idlelib/idle.pyw b/Lib/idlelib/idle.pyw index e73c049..9ce4c9f 100644 --- a/Lib/idlelib/idle.pyw +++ b/Lib/idlelib/idle.pyw @@ -1,10 +1,10 @@ try: - import idlelib.pyshell + import idlelib.PyShell except ImportError: - # IDLE is not installed, but maybe pyshell is on sys.path: - from . import pyshell + # IDLE is not installed, but maybe PyShell is on sys.path: + import PyShell import os - idledir = os.path.dirname(os.path.abspath(pyshell.__file__)) + 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', '') @@ -12,6 +12,6 @@ except ImportError: os.environ['PYTHONPATH'] = pypath + ':' + idledir else: os.environ['PYTHONPATH'] = idledir - pyshell.main() + PyShell.main() else: - idlelib.pyshell.main() + idlelib.PyShell.main() diff --git a/Lib/idlelib/idle_test/README.txt b/Lib/idlelib/idle_test/README.txt index 566bfd1..6967d70 100644 --- a/Lib/idlelib/idle_test/README.txt +++ b/Lib/idlelib/idle_test/README.txt @@ -2,12 +2,12 @@ README FOR IDLE TESTS IN IDLELIB.IDLE_TEST 0. Quick Start -Automated unit tests were added in 3.3 for Python 3.x. +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 3.4. +Human-mediated tests were added later in 2.7 and in 3.4. python -m idlelib.idle_test.htest @@ -15,53 +15,46 @@ python -m idlelib.idle_test.htest 1. Test Files The idle directory, idlelib, has over 60 xyz.py files. The idle_test -subdirectory contains test_xyz.py for each implementation file xyz.py. -To add a test for abc.py, open idle_test/template.py and immediately -Save As test_abc.py. Insert 'abc' on the first line, and replace -'zzdummy' with 'abc. +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 +'.' and 'as', and before and after '_' to be filled in. -Remove the imports of requires and tkinter if not needed. Otherwise, -add to the tkinter imports as needed. +import unittest +from test.support import requires +import idlelib. as -Add a prefix to 'Test' for the initial test class. The template class -contains code needed or possibly needed for gui tests. See the next -section if doing gui tests. If not, and not needed for further classes, -this code can be removed. +class _Test(unittest.TestCase): -Add the following at the end of abc.py. If an htest was added first, -insert the import and main lines before the htest lines. + def test_(self): -if __name__ == "__main__": - from unittest import main - main('idlelib.idle_test.test_abc', verbosity=2, exit=False) +if __name__ == '__main__': + unittest.main(verbosity=2) -The ', exit=False' is only needed if an htest follows. +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. + +if __name__ == "__main__": + import unittest + unittest.main('idlelib.idle_test.test_', verbosity=2, exit=False) 2. GUI Tests When run as part of the Python test suite, Idle GUI tests need to run -test.support.requires('gui'). A test is a GUI test if it creates a -tkinter.Tk root or master object either directly or indirectly by -instantiating a tkinter or idle class. GUI tests cannot run in test -processes that either have no graphical environment available or are not -allowed to use it. - -To guard a module consisting entirely of GUI tests, start with - -from test.support import requires -requires('gui') - -To guard a test class, put "requires('gui')" in its setUpClass function. -The template.py file does this. - -To avoid interfering with other GUI tests, all GUI objects must be -destroyed and deleted by the end of the test. The Tk root created in a -setUpX function should be destroyed in the corresponding tearDownX and -the module or class attribute deleted. Others widgets should descend -from the single root and the attributes deleted BEFORE root is -destroyed. See https://bugs.python.org/issue20567. +test.test_support.requires('gui') (test.support in 3.x). 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. The Tk root created in a setUpX function should +be destroyed in the corresponding tearDownX and the module or class attribute +deleted. Others widgets should descend from the single root and the attributes +deleted BEFORE root is destroyed. See https://bugs.python.org/issue20567. @classmethod def setUpClass(cls): @@ -72,64 +65,48 @@ destroyed. See https://bugs.python.org/issue20567. @classmethod def tearDownClass(cls): del cls.text - cls.root.update_idletasks() cls.root.destroy() del cls.root -The update_idletasks call is sometimes needed to prevent the following -warning either when running a test alone or as part of the test suite -(#27196). It should not hurt if not needed. - - can't invoke "event" command: application has been destroyed - ... - "ttk::ThemeChanged" - -If a test creates instance 'e' of EditorWindow, call 'e._close()' before -or as the first part of teardown. The effect of omitting this depends -on the later shutdown. Then enable the after_cancel loop in the -template. This prevents messages like the following. - -bgerror failed to handle background error. - Original error: invalid command name "106096696timer_event" - Error in bgerror: can't invoke "tk" command: application has been destroyed +WARNING: In 2.7, "requires('gui') MUST NOT be called at module scope. +See https://bugs.python.org/issue18910 Requires('gui') causes the test(s) it guards to be skipped if any of these 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 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 Windows by a service that is not allowed to + interact with the graphical environment. - The tests are being run on Linux and X Windows is not available. - - The tests are being run on Mac OSX in a process that cannot make a - window manager connection. + - 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. +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. +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.) +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 @@ -139,100 +116,35 @@ 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'. +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'.) 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 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 or use the test -m option. The -latter allows use of other regrtest options. When using the latter, -all components of the pattern must be present, but any can be replaced -by '*'. +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 +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). -python -m unittest -v idlelib.idle_test.test_xyz.Test_case.test_meth -python -m test -m idlelib.idle_test.text_xyz.Test_case.test_meth test_idle +To run an individual Testcase or test method, extend the dotted name given to +unittest on the command line. -The test suite can be run in an IDLE user process from Shell. ->>> import test.autotest # Issue 25588, 2017/10/13, 3.6.4, 3.7.0a2. -There are currently failures not usually present, and this does not -work when run from the editor. +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: +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 - - -5. Test Coverage - -Install the coverage package into your Python 3.6 site-packages -directory. (Its exact location depends on the OS). -> python3 -m pip install coverage -(On Windows, replace 'python3 with 'py -3.6' or perhaps just 'python'.) - -The problem with running coverage with repository python is that -coverage uses absolute imports for its submodules, hence it needs to be -in a directory in sys.path. One solution: copy the package to the -directory containing the cpython repository. Call it 'dev'. Then run -coverage either directly or from a script in that directory so that -'dev' is prepended to sys.path. - -Either edit or add dev/.coveragerc so it looks something like this. ---- -# .coveragerc sets coverage options. -[run] -branch = True - -[report] -# Regexes for lines to exclude from consideration -exclude_lines = - # Don't complain if non-runnable code isn't run: - if 0: - if __name__ == .__main__.: - - .*# htest # - if not _utest: - if _htest: ---- -The additions for IDLE are 'branch = True', to test coverage both ways, -and the last three exclude lines, to exclude things peculiar to IDLE -that are not executed during tests. - -A script like the following cover.bat (for Windows) is very handy. ---- -@echo off -rem Usage: cover filename [test_ suffix] # proper case required by coverage -rem filename without .py, 2nd parameter if test is not test_filename -setlocal -set py=f:\dev\3x\pcbuild\win32\python_d.exe -set src=idlelib.%1 -if "%2" EQU "" set tst=f:/dev/3x/Lib/idlelib/idle_test/test_%1.py -if "%2" NEQ "" set tst=f:/dev/ex/Lib/idlelib/idle_test/test_%2.py - -%py% -m coverage run --pylib --source=%src% %tst% -%py% -m coverage report --show-missing -%py% -m coverage html -start htmlcov\3x_Lib_idlelib_%1_py.html -rem Above opens new report; htmlcov\index.html displays report index ---- -The second parameter was added for tests of module x not named test_x. -(There were several before modules were renamed, now only one is left.) diff --git a/Lib/idlelib/idle_test/__init__.py b/Lib/idlelib/idle_test/__init__.py index ad067b4..845c92d 100644 --- a/Lib/idlelib/idle_test/__init__.py +++ b/Lib/idlelib/idle_test/__init__.py @@ -1,8 +1,6 @@ '''idlelib.idle_test is a private implementation of test.test_idle, which tests the IDLE application as part of the stdlib test suite. Run IDLE tests alone with "python -m test.test_idle". -Starting with Python 3.6, IDLE requires tcl/tk 8.5 or later. - This package and its contained modules are subject to change and any direct use is at your own risk. ''' diff --git a/Lib/idlelib/idle_test/htest.py b/Lib/idlelib/idle_test/htest.py index 1373b76..9e2ddd2 100644 --- a/Lib/idlelib/idle_test/htest.py +++ b/Lib/idlelib/idle_test/htest.py @@ -8,11 +8,11 @@ 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__ attribute is also X (the usual situation). +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 corresponding class. Or the +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 @@ -59,40 +59,34 @@ msg: master window hints about testing the widget. Modules and classes not being tested at the moment: -pyshell.PyShellEditorWindow -debugger.Debugger -autocomplete_w.AutoCompleteWindow -outwin.OutputWindow (indirectly being tested with grep test) +PyShell.PyShellEditorWindow +Debugger.Debugger +AutoCompleteWindow.AutoCompleteWindow +OutputWindow.OutputWindow (indirectly being tested with grep test) ''' -import idlelib.pyshell # Set Windows DPI awareness before Tk(). from importlib import import_module -import textwrap -import tkinter as tk -from tkinter.ttk import Scrollbar -tk.NoDefaultRoot() +from idlelib.macosxSupport import _initializeTkVariantTests +import Tkinter as tk AboutDialog_spec = { - 'file': 'help_about', - 'kwds': {'title': 'help_about test', + '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.", } -# TODO implement ^\; adding '<Control-Key-\\>' to function does not work. _calltip_window_spec = { - 'file': 'calltip_w', + 'file': 'CallTipWindow', 'kwds': {}, 'msg': "Typing '(' should display a calltip.\n" "Typing ') should hide the calltip.\n" - "So should moving cursor out of argument area.\n" - "Force-open-calltip does not work here.\n" } -_module_browser_spec = { - 'file': 'browser', +_class_browser_spec = { + 'file': 'ClassBrowser', 'kwds': {}, 'msg': "Inspect names of module, class(with superclass if " "applicable), methods and functions.\nToggle nested items.\n" @@ -101,7 +95,7 @@ _module_browser_spec = { } _color_delegator_spec = { - 'file': 'colorizer', + 'file': 'ColorDelegator', 'kwds': {}, 'msg': "The text is sample Python code.\n" "Ensure components like comments, keywords, builtins,\n" @@ -109,18 +103,8 @@ _color_delegator_spec = { "The default color scheme is in idlelib/config-highlight.def" } -CustomRun_spec = { - 'file': 'query', - 'kwds': {'title': 'Customize query.py Run', - '_htest': True}, - 'msg': "Enter with <Return> or [Run]. Print valid entry to Shell\n" - "Arguments are parsed into a list\n" - "Mode is currently restart True or False\n" - "Close dialog with valid entry, <Escape>, [Cancel], [X]" - } - ConfigDialog_spec = { - 'file': 'configdialog', + 'file': 'configDialog', 'kwds': {'title': 'ConfigDialogTest', '_htest': True,}, 'msg': "IDLE preferences dialog.\n" @@ -137,7 +121,7 @@ ConfigDialog_spec = { # TODO Improve message _dyn_option_menu_spec = { - 'file': 'dynoption', + '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" @@ -146,55 +130,64 @@ _dyn_option_menu_spec = { # TODO edit wrapper _editor_window_spec = { - 'file': 'editor', + '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': 'config_key', + 'file': 'keybindingDialog', 'kwds': {'title': 'Test keybindings', 'action': 'find-again', - 'current_key_sequences': [['<Control-Key-g>', '<Key-F3>', '<Control-Key-G>']], + '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 " + "is invalid.\nNo validitity checking if advanced key binding " "entry is used." } _grep_dialog_spec = { - 'file': 'grep', + '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'->'Go to file/line' anywhere in the search results " + "'Right-click'->'Goto file/line' anywhere in the search results " "should open that file \nin a new EditorWindow." } -HelpSource_spec = { - 'file': 'query', - 'kwds': {'title': 'Help name and source', - 'menuitem': 'test', - 'filepath': __file__, - 'used_names': {'abc'}, - '_htest': True}, - 'msg': "Enter menu item name and help file path\n" - "'', > than 30 chars, and 'abc' are invalid menu item names.\n" - "'' and file does not exist are invalid path items.\n" - "Any url ('www...', 'http...') is accepted.\n" - "Test Browse with and without path, as cannot unittest.\n" - "[Ok] or <Return> prints valid entry to shell\n" - "[Cancel] or <Escape> prints None to shell" - } - _io_binding_spec = { - 'file': 'iomenu', + 'file': 'IOBinding', 'kwds': {}, 'msg': "Test the following bindings.\n" "<Control-o> to open file from dialog.\n" @@ -206,28 +199,8 @@ _io_binding_spec = { "Check that changes were saved by opening the file elsewhere." } -_linenumbers_drag_scrolling_spec = { - 'file': 'sidebar', - 'kwds': {}, - 'msg': textwrap.dedent("""\ - 1. Click on the line numbers and drag down below the edge of the - window, moving the mouse a bit and then leaving it there for a while. - The text and line numbers should gradually scroll down, with the - selection updated continuously. - - 2. With the lines still selected, click on a line number above the - selected lines. Only the line whose number was clicked should be - selected. - - 3. Repeat step #1, dragging to above the window. The text and line - numbers should gradually scroll up, with the selection updated - continuously. - - 4. Repeat step #2, clicking a line number below the selection."""), - } - _multi_call_spec = { - 'file': 'multicall', + 'file': 'MultiCall', 'kwds': {}, 'msg': "The following actions should trigger a print to console or IDLE" " Shell.\nEntering and leaving the text area, key entry, " @@ -237,14 +210,14 @@ _multi_call_spec = { } _multistatus_bar_spec = { - 'file': 'statusbar', + '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': 'debugobj', + 'file': 'ObjectBrowser', 'kwds': {}, 'msg': "Double click on items upto the lowest level.\n" "Attributes of the objects and related information " @@ -252,7 +225,7 @@ _object_browser_spec = { } _path_browser_spec = { - 'file': 'pathbrowser', + 'file': 'PathBrowser', 'kwds': {}, 'msg': "Test for correct display of all paths in sys.path.\n" "Toggle nested items upto the lowest level.\n" @@ -261,7 +234,7 @@ _path_browser_spec = { } _percolator_spec = { - 'file': 'percolator', + '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 " @@ -271,20 +244,8 @@ _percolator_spec = { "Test for actions like text entry, and removal." } -Query_spec = { - 'file': 'query', - 'kwds': {'title': 'Query', - 'message': 'Enter something', - 'text0': 'Go', - '_htest': True}, - 'msg': "Enter with <Return> or [Ok]. Print valid entry to Shell\n" - "Blank line, after stripping, is ignored\n" - "Close dialog with valid entry, <Escape>, [Cancel], [X]" - } - - _replace_dialog_spec = { - 'file': 'replace', + 'file': 'ReplaceDialog', 'kwds': {}, 'msg': "Click the 'Replace' button.\n" "Test various replace options in the 'Replace dialog'.\n" @@ -292,22 +253,15 @@ _replace_dialog_spec = { } _search_dialog_spec = { - 'file': 'search', + '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'." } -_searchbase_spec = { - 'file': 'searchbase', - 'kwds': {}, - 'msg': "Check the appearance of the base search dialog\n" - "Its only action is to close." - } - _scrolled_list_spec = { - 'file': 'scrolledlist', + 'file': 'ScrolledList', 'kwds': {}, 'msg': "You should see a scrollable list of items\n" "Selecting (clicking) or double clicking an item " @@ -323,29 +277,48 @@ show_idlehelp_spec = { } _stack_viewer_spec = { - 'file': 'stackviewer', + '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', + 'file': 'ToolTip', 'kwds': {}, 'msg': "Place mouse cursor over both the buttons\n" "A tooltip should appear with some text." } _tree_widget_spec = { - 'file': 'tree', + 'file': 'TreeWidget', 'kwds': {}, 'msg': "The canvas is scrollable.\n" "Click on folders upto to the lowest level." } _undo_delegator_spec = { - 'file': 'undo', + 'file': 'UndoDelegator', 'kwds': {}, 'msg': "Click [Undo] to undo any action.\n" "Click [Redo] to redo any action.\n" @@ -353,17 +326,8 @@ _undo_delegator_spec = { "by printing to the console or the IDLE shell.\n" } -ViewWindow_spec = { - 'file': 'textview', - 'kwds': {'title': 'Test textview', - 'contents': 'The quick brown fox jumps over the lazy dog.\n'*35, - '_htest': True}, - 'msg': "Test for read-only property of text.\n" - "Select text, scroll window, close" - } - _widget_redirector_spec = { - 'file': 'redirector', + 'file': 'WidgetRedirector', 'kwds': {}, 'msg': "Every text insert should be printed to the console " "or the IDLE shell." @@ -373,13 +337,14 @@ 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 = Scrollbar(frameLabel, command=text.yview) + 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) @@ -400,45 +365,38 @@ def run(*tests): test = getattr(mod, test_name) test_list.append((test_spec, test)) - test_name = tk.StringVar(root) - callable_object = None - test_kwds = None + test_name = [tk.StringVar('')] + callable_object = [None] + test_kwds = [None] - def next_test(): - nonlocal test_name, callable_object, test_kwds + def next(): 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']) + test_spec, callable_object[0] = test_list.pop() + test_kwds[0] = test_spec['kwds'] + test_kwds[0]['parent'] = root + test_name[0].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(_=None): - widget = callable_object(**test_kwds) + def run_test(): + widget = callable_object[0](**test_kwds[0]) try: print(widget.result) except AttributeError: pass - def close(_=None): - root.destroy() - - button = tk.Button(root, textvariable=test_name, - default='active', command=run_test) - next_button = tk.Button(root, text="Next", command=next_test) + button = tk.Button(root, textvariable=test_name[0], command=run_test) button.pack() + next_button = tk.Button(root, text="Next", command=next) next_button.pack() - next_button.focus_set() - root.bind('<Key-Return>', run_test) - root.bind('<Key-Escape>', close) - next_test() + next() + root.mainloop() if __name__ == '__main__': diff --git a/Lib/idlelib/idle_test/mock_idle.py b/Lib/idlelib/idle_test/mock_idle.py index 71fa480..7b09f83 100644 --- a/Lib/idlelib/idle_test/mock_idle.py +++ b/Lib/idlelib/idle_test/mock_idle.py @@ -5,44 +5,38 @@ Attributes and methods will be added as needed for tests. from idlelib.idle_test.mock_tk import Text -class Func: - '''Record call, capture args, return/raise result set by test. - - When mock function is called, set or use attributes: - self.called - increment call number even if no args, kwds passed. - self.args - capture positional arguments. - self.kwds - capture keyword arguments. - self.result - return or raise value set in __init__. - self.return_self - return self instead, to mock query class return. - - Most common use will probably be to mock instance methods. - Given class instance, can set and delete as instance attribute. +class Func(object): + '''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, return_self=False): - self.called = 0 + def __init__(self, result=None): + self.called = False self.result = result - self.return_self = return_self self.args = None self.kwds = None def __call__(self, *args, **kwds): - self.called += 1 + self.called = True self.args = args self.kwds = kwds if isinstance(self.result, BaseException): raise self.result - elif self.return_self: - return self else: return self.result -class Editor: - '''Minimally imitate editor.EditorWindow class. +class Editor(object): + '''Minimally imitate EditorWindow.EditorWindow class. ''' - def __init__(self, flist=None, filename=None, key=None, root=None, - text=None): # Allow real Text with mock Editor. - self.text = text or Text() + def __init__(self, flist=None, filename=None, key=None, root=None): + self.text = Text() self.undo = UndoDelegator() def get_selection_indices(self): @@ -51,8 +45,8 @@ class Editor: return first, last -class UndoDelegator: - '''Minimally imitate undo.UndoDelegator class. +class UndoDelegator(object): + '''Minimally imitate UndoDelegator,UndoDelegator class. ''' # A real undo block is only needed for user interaction. def undo_block_start(*args): diff --git a/Lib/idlelib/idle_test/mock_tk.py b/Lib/idlelib/idle_test/mock_tk.py index 576f7d5..56ca876 100644 --- a/Lib/idlelib/idle_test/mock_tk.py +++ b/Lib/idlelib/idle_test/mock_tk.py @@ -4,7 +4,7 @@ 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: +class Event(object): '''Minimal mock with attributes for testing event handlers. This is not a gui object, but is used as an argument for callbacks @@ -22,7 +22,7 @@ class Event: "Create event with attributes needed for test" self.__dict__.update(kwds) -class Var: +class Var(object): "Use for String/Int/BooleanVar: incomplete" def __init__(self, master=None, value=None, name=None): self.master = master @@ -33,11 +33,11 @@ class Var: def get(self): return self.value -class Mbox_func: +class Mbox_func(object): """Generic mock for messagebox functions, which all have the same signature. Instead of displaying a message box, the mock's call method saves the - arguments as instance attributes, which test functions can then examine. + arguments as instance attributes, which test functions can then examime. The test can set the result returned to ask function """ def __init__(self, result=None): @@ -50,7 +50,7 @@ class Mbox_func: self.kwds = kwds return self.result # Set by tester for ask functions -class Mbox: +class Mbox(object): """Mock for tkinter.messagebox with an Mbox_func for each function. This module was 'tkMessageBox' in 2.x; hence the 'import as' in 3.x. @@ -87,7 +87,7 @@ class Test(unittest.TestCase): from _tkinter import TclError -class Text: +class Text(object): """A semi-functional non-gui replacement for tkinter.Text text editors. The mock's data model is that a text is a list of \n-terminated lines. @@ -116,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 constrained by the number of lines and linelengths of + The result is contrained 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: @@ -133,7 +133,7 @@ class Text: try: index=index.lower() except AttributeError: - raise TclError('bad text index "%s"' % index) from None + raise TclError('bad text index "%s"' % index) lastline = len(self.data) - 1 # same as number of text lines if index == 'insert': @@ -296,8 +296,3 @@ class Text: def bind(sequence=None, func=None, add=None): "Bind to this widget at event sequence a call to function func." pass - -class Entry: - "Mock for tkinter.Entry." - def focus_set(self): - pass diff --git a/Lib/idlelib/idle_test/template.py b/Lib/idlelib/idle_test/template.py deleted file mode 100644 index 725a55b..0000000 --- a/Lib/idlelib/idle_test/template.py +++ /dev/null @@ -1,30 +0,0 @@ -"Test , coverage %." - -from idlelib import zzdummy -import unittest -from test.support import requires -from tkinter import Tk - - -class Test(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - - @classmethod - def tearDownClass(cls): - cls.root.update_idletasks() -## for id in cls.root.tk.call('after', 'info'): -## cls.root.after_cancel(id) # Need for EditorWindow. - cls.root.destroy() - del cls.root - - def test_init(self): - self.assertTrue(True) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_autocomplete.py b/Lib/idlelib/idle_test/test_autocomplete.py index 2c478cd..002751e 100644 --- a/Lib/idlelib/idle_test/test_autocomplete.py +++ b/Lib/idlelib/idle_test/test_autocomplete.py @@ -1,17 +1,15 @@ -"Test autocomplete, coverage 93%." - import unittest -from unittest.mock import Mock, patch -from test.support import requires -from tkinter import Tk, Text -import os -import __main__ +from test.test_support import requires +from Tkinter import Tk, Text -import idlelib.autocomplete as ac -import idlelib.autocomplete_w as acw +import idlelib.AutoComplete as ac +import idlelib.AutoCompleteWindow as acw 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): @@ -19,7 +17,7 @@ class DummyEditwin: self.text = text self.indentwidth = 8 self.tabwidth = 8 - self.prompt_last_line = '>>>' # Currently not used by autocomplete. + self.context_use_ps1 = True class AutoCompleteTest(unittest.TestCase): @@ -28,269 +26,114 @@ class AutoCompleteTest(unittest.TestCase): def setUpClass(cls): requires('gui') cls.root = Tk() - cls.root.withdraw() cls.text = Text(cls.root) cls.editor = DummyEditwin(cls.root, cls.text) @classmethod def tearDownClass(cls): del cls.editor, cls.text - cls.root.update_idletasks() cls.root.destroy() del cls.root def setUp(self): - self.text.delete('1.0', 'end') + self.editor.text.delete('1.0', 'end') self.autocomplete = ac.AutoComplete(self.editor) def test_init(self): self.assertEqual(self.autocomplete.editwin, self.editor) - self.assertEqual(self.autocomplete.text, self.text) def test_make_autocomplete_window(self): testwin = self.autocomplete._make_autocomplete_window() self.assertIsInstance(testwin, acw.AutoCompleteWindow) def test_remove_autocomplete_window(self): - acp = self.autocomplete - acp.autocompletewindow = m = Mock() - acp._remove_autocomplete_window() - m.hide_window.assert_called_once() - self.assertIsNone(acp.autocompletewindow) + 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): - # Call _open_completions and break. - acp = self.autocomplete - open_c = Func() - acp.open_completions = open_c - self.assertEqual(acp.force_open_completions_event('event'), 'break') - self.assertEqual(open_c.args[0], ac.FORCE) + # 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_autocomplete_event(self): + def test_try_open_completions_event(self): Equal = self.assertEqual - acp = self.autocomplete + autocomplete = self.autocomplete + trycompletions = self.autocomplete.try_open_completions_event + o_c_l = Func() + autocomplete._open_completions_later = o_c_l - # Result of autocomplete event: If modified tab, None. - ev = Event(mc_state=True) - self.assertIsNone(acp.autocomplete_event(ev)) - del ev.mc_state + # _open_completions_later should not be called with no text in editor + trycompletions('event') + Equal(o_c_l.args, None) - # If tab after whitespace, None. - self.text.insert('1.0', ' """Docstring.\n ') - self.assertIsNone(acp.autocomplete_event(ev)) - self.text.delete('1.0', 'end') - - # If active autocomplete window, complete() and 'break'. + # _open_completions_later should be called with COMPLETE_ATTRIBUTES (1) self.text.insert('1.0', 're.') - acp.autocompletewindow = mock = Mock() - mock.is_active = Mock(return_value=True) - Equal(acp.autocomplete_event(ev), 'break') - mock.complete.assert_called_once() - acp.autocompletewindow = None + trycompletions('event') + Equal(o_c_l.args, (False, False, False, 1)) - # If no active autocomplete window, open_completions(), None/break. - open_c = Func(result=False) - acp.open_completions = open_c - Equal(acp.autocomplete_event(ev), None) - Equal(open_c.args[0], ac.TAB) - open_c.result = True - Equal(acp.autocomplete_event(ev), 'break') - Equal(open_c.args[0], ac.TAB) + # _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_try_open_completions_event(self): + def test_autocomplete_event(self): Equal = self.assertEqual - text = self.text - acp = self.autocomplete - trycompletions = acp.try_open_completions_event - after = Func(result='after1') - acp.text.after = after + autocomplete = self.autocomplete - # If no text or trigger, after not called. - trycompletions() - Equal(after.called, 0) - text.insert('1.0', 're') - trycompletions() - Equal(after.called, 0) - - # Attribute needed, no existing callback. - text.insert('insert', ' re.') - acp._delayed_completion_id = None - trycompletions() - Equal(acp._delayed_completion_index, text.index('insert')) - Equal(after.args, - (acp.popupwait, acp._delayed_open_completions, ac.TRY_A)) - cb1 = acp._delayed_completion_id - Equal(cb1, 'after1') + # 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 - # File needed, existing callback cancelled. - text.insert('insert', ' "./Lib/') - after.result = 'after2' - cancel = Func() - acp.text.after_cancel = cancel - trycompletions() - Equal(acp._delayed_completion_index, text.index('insert')) - Equal(cancel.args, (cb1,)) - Equal(after.args, - (acp.popupwait, acp._delayed_open_completions, ac.TRY_F)) - Equal(acp._delayed_completion_id, 'after2') + # 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): - Equal = self.assertEqual - acp = self.autocomplete - open_c = Func() - acp.open_completions = open_c - self.text.insert('1.0', '"dict.') - - # Set autocomplete._delayed_completion_id to None. - # Text index changed, don't call open_completions. - acp._delayed_completion_id = 'after' - acp._delayed_completion_index = self.text.index('insert+1c') - acp._delayed_open_completions('dummy') - self.assertIsNone(acp._delayed_completion_id) - Equal(open_c.called, 0) - - # Text index unchanged, call open_completions. - acp._delayed_completion_index = self.text.index('insert') - acp._delayed_open_completions((1, 2, 3, ac.FILES)) - self.assertEqual(open_c.args[0], (1, 2, 3, ac.FILES)) - - def test_oc_cancel_comment(self): - none = self.assertIsNone - acp = self.autocomplete - - # Comment is in neither code or string. - acp._delayed_completion_id = 'after' - after = Func(result='after') - acp.text.after_cancel = after - self.text.insert(1.0, '# comment') - none(acp.open_completions(ac.TAB)) # From 'else' after 'elif'. - none(acp._delayed_completion_id) - - def test_oc_no_list(self): - acp = self.autocomplete - fetch = Func(result=([],[])) - acp.fetch_completions = fetch - self.text.insert('1.0', 'object') - self.assertIsNone(acp.open_completions(ac.TAB)) - self.text.insert('insert', '.') - self.assertIsNone(acp.open_completions(ac.TAB)) - self.assertEqual(fetch.called, 2) - - - def test_open_completions_none(self): - # Test other two None returns. - none = self.assertIsNone - acp = self.autocomplete - - # No object for attributes or need call not allowed. - self.text.insert(1.0, '.') - none(acp.open_completions(ac.TAB)) - self.text.insert('insert', ' int().') - none(acp.open_completions(ac.TAB)) - - # Blank or quote trigger 'if complete ...'. - self.text.delete(1.0, 'end') - self.assertFalse(acp.open_completions(ac.TAB)) - self.text.insert('1.0', '"') - self.assertFalse(acp.open_completions(ac.TAB)) - self.text.delete('1.0', 'end') - - class dummy_acw(): - __init__ = Func() - show_window = Func(result=False) - hide_window = Func() + # 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. - acp = self.autocomplete - fetch = Func(result=(['tem'],['tem', '_tem'])) - acp.fetch_completions = fetch - def make_acw(): return self.dummy_acw() - acp._make_autocomplete_window = make_acw - - self.text.insert('1.0', 'int.') - acp.open_completions(ac.TAB) - self.assertIsInstance(acp.autocompletewindow, self.dummy_acw) - self.text.delete('1.0', 'end') - - # Test files. - self.text.insert('1.0', '"t') - self.assertTrue(acp.open_completions(ac.TAB)) - self.text.delete('1.0', 'end') + # 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 '.'. - acp = self.autocomplete - small, large = acp.fetch_completions( - '', ac.ATTRS) - if __main__.__file__ != ac.__file__: - self.assertNotIn('AutoComplete', small) # See issue 36405. - - # Test attributes - s, b = acp.fetch_completions('', ac.ATTRS) - self.assertLess(len(small), len(large)) - self.assertTrue(all(filter(lambda x: x.startswith('_'), s))) - self.assertTrue(any(filter(lambda x: x.startswith('_'), b))) - - # Test smalll should respect to __all__. - with patch.dict('__main__.__dict__', {'__all__': ['a', 'b']}): - s, b = acp.fetch_completions('', ac.ATTRS) - self.assertEqual(s, ['a', 'b']) - self.assertIn('__name__', b) # From __main__.__dict__ - self.assertIn('sum', b) # From __main__.__builtins__.__dict__ - - # Test attributes with name entity. - mock = Mock() - mock._private = Mock() - with patch.dict('__main__.__dict__', {'foo': mock}): - s, b = acp.fetch_completions('foo', ac.ATTRS) - self.assertNotIn('_private', s) - self.assertIn('_private', b) - self.assertEqual(s, [i for i in sorted(dir(mock)) if i[:1] != '_']) - self.assertEqual(b, sorted(dir(mock))) - - # Test files - def _listdir(path): - # This will be patch and used in fetch_completions. - if path == '.': - return ['foo', 'bar', '.hidden'] - return ['monty', 'python', '.hidden'] - - with patch.object(os, 'listdir', _listdir): - s, b = acp.fetch_completions('', ac.FILES) - self.assertEqual(s, ['bar', 'foo']) - self.assertEqual(b, ['.hidden', 'bar', 'foo']) - - s, b = acp.fetch_completions('~', ac.FILES) - self.assertEqual(s, ['monty', 'python']) - self.assertEqual(b, ['.hidden', 'monty', 'python']) + # 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__. - acp = self.autocomplete - Equal = self.assertEqual - - Equal(acp.get_entity('int'), int) - - # Test name from sys.modules. - mock = Mock() - with patch.dict('sys.modules', {'tempfile': mock}): - Equal(acp.get_entity('tempfile'), mock) - - # Test name from __main__.__dict__. - di = {'foo': 10, 'bar': 20} - with patch.dict('__main__.__dict__', {'d': di}): - Equal(acp.get_entity('d'), di) - - # Test name not in namespace. - with patch.dict('__main__.__dict__', {}): - with self.assertRaises(NameError): - acp.get_entity('not_exist') + # __main__.__dict__ + pass if __name__ == '__main__': diff --git a/Lib/idlelib/idle_test/test_autocomplete_w.py b/Lib/idlelib/idle_test/test_autocomplete_w.py deleted file mode 100644 index b1bdc6c..0000000 --- a/Lib/idlelib/idle_test/test_autocomplete_w.py +++ /dev/null @@ -1,32 +0,0 @@ -"Test autocomplete_w, coverage 11%." - -import unittest -from test.support import requires -from tkinter import Tk, Text - -import idlelib.autocomplete_w as acw - - -class AutoCompleteWindowTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - cls.text = Text(cls.root) - cls.acw = acw.AutoCompleteWindow(cls.text) - - @classmethod - def tearDownClass(cls): - del cls.text, cls.acw - cls.root.update_idletasks() - cls.root.destroy() - del cls.root - - def test_init(self): - self.assertEqual(self.acw.widget, self.text) - - -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 index e734a8b..6be4fbf 100644 --- a/Lib/idlelib/idle_test/test_autoexpand.py +++ b/Lib/idlelib/idle_test/test_autoexpand.py @@ -1,12 +1,12 @@ -"Test autoexpand, coverage 100%." - -from idlelib.autoexpand import AutoExpand +"""Unit tests for idlelib.AutoExpand""" import unittest -from test.support import requires -from tkinter import Text, Tk +from test.test_support import requires +from Tkinter import Text, Tk +#from idlelib.idle_test.mock_tk import Text +from idlelib.AutoExpand import AutoExpand -class DummyEditwin: +class Dummy_Editwin: # AutoExpand.__init__ only needs .text def __init__(self, text): self.text = text @@ -15,26 +15,13 @@ class AutoExpandTest(unittest.TestCase): @classmethod def setUpClass(cls): - requires('gui') - cls.tk = Tk() - cls.text = Text(cls.tk) - cls.auto_expand = AutoExpand(DummyEditwin(cls.text)) - cls.auto_expand.bell = lambda: None - -# If mock_tk.Text._decode understood indexes 'insert' with suffixed 'linestart', -# 'wordstart', and 'lineend', used by autoexpand, we could use the following -# to run these test on non-gui machines (but check bell). -## try: -## requires('gui') -## #raise ResourceDenied() # Uncomment to test mock. -## except ResourceDenied: -## from idlelib.idle_test.mock_tk import Text -## cls.text = Text() -## cls.text.bell = lambda: None -## else: -## from tkinter import Tk, Text -## cls.tk = Tk() -## cls.text = Text(cls.tk) + 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): @@ -91,7 +78,7 @@ class AutoExpandTest(unittest.TestCase): equal(previous(), 'a') def test_after_only(self): - # Also add punctuation 'noise' that should be ignored. + # Also add punctuation 'noise' that shoud be ignored. text = self.text previous = self.auto_expand.getprevword expand = self.auto_expand.expand_word_event @@ -150,6 +137,5 @@ class AutoExpandTest(unittest.TestCase): 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_browser.py b/Lib/idlelib/idle_test/test_browser.py deleted file mode 100644 index 25d6dc6..0000000 --- a/Lib/idlelib/idle_test/test_browser.py +++ /dev/null @@ -1,248 +0,0 @@ -"Test browser, coverage 90%." - -from idlelib import browser -from test.support import requires -import unittest -from unittest import mock -from idlelib.idle_test.mock_idle import Func - -from collections import deque -import os.path -import pyclbr -from tkinter import Tk - -from idlelib.tree import TreeNode - - -class ModuleBrowserTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - cls.mb = browser.ModuleBrowser(cls.root, __file__, _utest=True) - - @classmethod - def tearDownClass(cls): - cls.mb.close() - cls.root.update_idletasks() - cls.root.destroy() - del cls.root, cls.mb - - def test_init(self): - mb = self.mb - eq = self.assertEqual - eq(mb.path, __file__) - eq(pyclbr._modules, {}) - self.assertIsInstance(mb.node, TreeNode) - self.assertIsNotNone(browser.file_open) - - def test_settitle(self): - mb = self.mb - self.assertIn(os.path.basename(__file__), mb.top.title()) - self.assertEqual(mb.top.iconname(), 'Module Browser') - - def test_rootnode(self): - mb = self.mb - rn = mb.rootnode() - self.assertIsInstance(rn, browser.ModuleBrowserTreeItem) - - def test_close(self): - mb = self.mb - mb.top.destroy = Func() - mb.node.destroy = Func() - mb.close() - self.assertTrue(mb.top.destroy.called) - self.assertTrue(mb.node.destroy.called) - del mb.top.destroy, mb.node.destroy - - -# Nested tree same as in test_pyclbr.py except for supers on C0. C1. -mb = pyclbr -module, fname = 'test', 'test.py' -C0 = mb.Class(module, 'C0', ['base'], fname, 1) -F1 = mb._nest_function(C0, 'F1', 3) -C1 = mb._nest_class(C0, 'C1', 6, ['']) -C2 = mb._nest_class(C1, 'C2', 7) -F3 = mb._nest_function(C2, 'F3', 9) -f0 = mb.Function(module, 'f0', fname, 11) -f1 = mb._nest_function(f0, 'f1', 12) -f2 = mb._nest_function(f1, 'f2', 13) -c1 = mb._nest_class(f0, 'c1', 15) -mock_pyclbr_tree = {'C0': C0, 'f0': f0} - -# Adjust C0.name, C1.name so tests do not depend on order. -browser.transform_children(mock_pyclbr_tree, 'test') # C0(base) -browser.transform_children(C0.children) # C1() - -# The class below checks that the calls above are correct -# and that duplicate calls have no effect. - - -class TransformChildrenTest(unittest.TestCase): - - def test_transform_module_children(self): - eq = self.assertEqual - transform = browser.transform_children - # Parameter matches tree module. - tcl = list(transform(mock_pyclbr_tree, 'test')) - eq(tcl, [C0, f0]) - eq(tcl[0].name, 'C0(base)') - eq(tcl[1].name, 'f0') - # Check that second call does not change suffix. - tcl = list(transform(mock_pyclbr_tree, 'test')) - eq(tcl[0].name, 'C0(base)') - # Nothing to traverse if parameter name isn't same as tree module. - tcl = list(transform(mock_pyclbr_tree, 'different name')) - eq(tcl, []) - - def test_transform_node_children(self): - eq = self.assertEqual - transform = browser.transform_children - # Class with two children, one name altered. - tcl = list(transform(C0.children)) - eq(tcl, [F1, C1]) - eq(tcl[0].name, 'F1') - eq(tcl[1].name, 'C1()') - tcl = list(transform(C0.children)) - eq(tcl[1].name, 'C1()') - # Function with two children. - eq(list(transform(f0.children)), [f1, c1]) - - -class ModuleBrowserTreeItemTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.mbt = browser.ModuleBrowserTreeItem(fname) - - def test_init(self): - self.assertEqual(self.mbt.file, fname) - - def test_gettext(self): - self.assertEqual(self.mbt.GetText(), fname) - - def test_geticonname(self): - self.assertEqual(self.mbt.GetIconName(), 'python') - - def test_isexpandable(self): - self.assertTrue(self.mbt.IsExpandable()) - - def test_listchildren(self): - save_rex = browser.pyclbr.readmodule_ex - save_tc = browser.transform_children - browser.pyclbr.readmodule_ex = Func(result=mock_pyclbr_tree) - browser.transform_children = Func(result=[f0, C0]) - try: - self.assertEqual(self.mbt.listchildren(), [f0, C0]) - finally: - browser.pyclbr.readmodule_ex = save_rex - browser.transform_children = save_tc - - def test_getsublist(self): - mbt = self.mbt - mbt.listchildren = Func(result=[f0, C0]) - sub0, sub1 = mbt.GetSubList() - del mbt.listchildren - self.assertIsInstance(sub0, browser.ChildBrowserTreeItem) - self.assertIsInstance(sub1, browser.ChildBrowserTreeItem) - self.assertEqual(sub0.name, 'f0') - self.assertEqual(sub1.name, 'C0(base)') - - @mock.patch('idlelib.browser.file_open') - def test_ondoubleclick(self, fopen): - mbt = self.mbt - - with mock.patch('os.path.exists', return_value=False): - mbt.OnDoubleClick() - fopen.assert_not_called() - - with mock.patch('os.path.exists', return_value=True): - mbt.OnDoubleClick() - fopen.assert_called() - fopen.called_with(fname) - - -class ChildBrowserTreeItemTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - CBT = browser.ChildBrowserTreeItem - cls.cbt_f1 = CBT(f1) - cls.cbt_C1 = CBT(C1) - cls.cbt_F1 = CBT(F1) - - @classmethod - def tearDownClass(cls): - del cls.cbt_C1, cls.cbt_f1, cls.cbt_F1 - - def test_init(self): - eq = self.assertEqual - eq(self.cbt_C1.name, 'C1()') - self.assertFalse(self.cbt_C1.isfunction) - eq(self.cbt_f1.name, 'f1') - self.assertTrue(self.cbt_f1.isfunction) - - def test_gettext(self): - self.assertEqual(self.cbt_C1.GetText(), 'class C1()') - self.assertEqual(self.cbt_f1.GetText(), 'def f1(...)') - - def test_geticonname(self): - self.assertEqual(self.cbt_C1.GetIconName(), 'folder') - self.assertEqual(self.cbt_f1.GetIconName(), 'python') - - def test_isexpandable(self): - self.assertTrue(self.cbt_C1.IsExpandable()) - self.assertTrue(self.cbt_f1.IsExpandable()) - self.assertFalse(self.cbt_F1.IsExpandable()) - - def test_getsublist(self): - eq = self.assertEqual - CBT = browser.ChildBrowserTreeItem - - f1sublist = self.cbt_f1.GetSubList() - self.assertIsInstance(f1sublist[0], CBT) - eq(len(f1sublist), 1) - eq(f1sublist[0].name, 'f2') - - eq(self.cbt_F1.GetSubList(), []) - - @mock.patch('idlelib.browser.file_open') - def test_ondoubleclick(self, fopen): - goto = fopen.return_value.gotoline = mock.Mock() - self.cbt_F1.OnDoubleClick() - fopen.assert_called() - goto.assert_called() - goto.assert_called_with(self.cbt_F1.obj.lineno) - # Failure test would have to raise OSError or AttributeError. - - -class NestedChildrenTest(unittest.TestCase): - "Test that all the nodes in a nested tree are added to the BrowserTree." - - def test_nested(self): - queue = deque() - actual_names = [] - # The tree items are processed in breadth first order. - # Verify that processing each sublist hits every node and - # in the right order. - expected_names = ['f0', 'C0(base)', - 'f1', 'c1', 'F1', 'C1()', - 'f2', 'C2', - 'F3'] - CBT = browser.ChildBrowserTreeItem - queue.extend((CBT(f0), CBT(C0))) - while queue: - cb = queue.popleft() - sublist = cb.GetSubList() - queue.extend(sublist) - self.assertIn(cb.name, cb.GetText()) - self.assertIn(cb.GetIconName(), ('python', 'folder')) - self.assertIs(cb.IsExpandable(), sublist != []) - actual_names.append(cb.name) - self.assertEqual(actual_names, expected_names) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_calltip.py b/Lib/idlelib/idle_test/test_calltip.py deleted file mode 100644 index 886959b..0000000 --- a/Lib/idlelib/idle_test/test_calltip.py +++ /dev/null @@ -1,253 +0,0 @@ -"Test calltip, coverage 60%" - -from idlelib import calltip -import unittest -import textwrap -import types -import re - - -# Test Class TC is used in multiple get_argspec test methods -class TC(): - 'doc' - tip = "(ai=None, *b)" - def __init__(self, ai=None, *b): 'doc' - __init__.tip = "(self, ai=None, *b)" - def t1(self): 'doc' - t1.tip = "(self)" - def t2(self, ai, b=None): 'doc' - t2.tip = "(self, ai, b=None)" - def t3(self, ai, *args): 'doc' - t3.tip = "(self, ai, *args)" - def t4(self, *args): 'doc' - t4.tip = "(self, *args)" - def t5(self, ai, b=None, *args, **kw): 'doc' - t5.tip = "(self, ai, b=None, *args, **kw)" - def t6(no, self): 'doc' - t6.tip = "(no, self)" - def __call__(self, ci): 'doc' - __call__.tip = "(self, ci)" - def nd(self): pass # No doc. - # attaching .tip to wrapped methods does not work - @classmethod - def cm(cls, a): 'doc' - @staticmethod - def sm(b): 'doc' - - -tc = TC() -default_tip = calltip._default_callable_argspec -get_spec = calltip.get_argspec - - -class Get_argspecTest(unittest.TestCase): - # The get_spec function must return a string, even if blank. - # Test a variety of objects to be sure that none cause it to raise - # (quite aside from getting as correct an answer as possible). - # The tests of builtins may break if inspect or the docstrings change, - # but a red buildbot is better than a user crash (as has happened). - # For a simple mismatch, change the expected output to the actual. - - def test_builtins(self): - - def tiptest(obj, out): - self.assertEqual(get_spec(obj), out) - - # Python class that inherits builtin methods - class List(list): "List() doc" - - # Simulate builtin with no docstring for default tip test - class SB: __call__ = None - - if List.__doc__ is not None: - tiptest(List, - f'(iterable=(), /){calltip._argument_positional}' - f'\n{List.__doc__}') - tiptest(list.__new__, - '(*args, **kwargs)\n' - 'Create and return a new object. ' - 'See help(type) for accurate signature.') - tiptest(list.__init__, - '(self, /, *args, **kwargs)' - + calltip._argument_positional + '\n' + - 'Initialize self. See help(type(self)) for accurate signature.') - append_doc = (calltip._argument_positional - + "\nAppend object to the end of the list.") - tiptest(list.append, '(self, object, /)' + append_doc) - tiptest(List.append, '(self, object, /)' + append_doc) - tiptest([].append, '(object, /)' + append_doc) - - tiptest(types.MethodType, "method(function, instance)") - tiptest(SB(), default_tip) - - p = re.compile('') - tiptest(re.sub, '''\ -(pattern, repl, string, count=0, flags=0) -Return the string obtained by replacing the leftmost -non-overlapping occurrences of the pattern in string by the -replacement repl. repl can be either a string or a callable; -if a string, backslash escapes in it are processed. If it is -a callable, it's passed the Match object and must return''') - tiptest(p.sub, '''\ -(repl, string, count=0) -Return the string obtained by replacing the leftmost \ -non-overlapping occurrences o...''') - - def test_signature_wrap(self): - if textwrap.TextWrapper.__doc__ is not None: - self.assertEqual(get_spec(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, *, max_lines=None, - placeholder=' [...]')''') - - def test_properly_formated(self): - - def foo(s='a'*100): - pass - - def bar(s='a'*100): - """Hello Guido""" - pass - - def baz(s='a'*100, z='b'*100): - pass - - indent = calltip._INDENT - - sfoo = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\ - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\ - "aaaaaaaaaa')" - sbar = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\ - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\ - "aaaaaaaaaa')\nHello Guido" - sbaz = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\ - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\ - "aaaaaaaaaa', z='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"\ - "bbbbbbbbbbbbbbbbb\n" + indent + "bbbbbbbbbbbbbbbbbbbbbb"\ - "bbbbbbbbbbbbbbbbbbbbbb')" - - for func,doc in [(foo, sfoo), (bar, sbar), (baz, sbaz)]: - with self.subTest(func=func, doc=doc): - self.assertEqual(get_spec(func), doc) - - def test_docline_truncation(self): - def f(): pass - f.__doc__ = 'a'*300 - self.assertEqual(get_spec(f), f"()\n{'a'*(calltip._MAX_COLS-3) + '...'}") - - def test_multiline_docstring(self): - # Test fewer lines than max. - self.assertEqual(get_spec(range), - "range(stop) -> range object\n" - "range(start, stop[, step]) -> range object") - - # Test max lines - self.assertEqual(get_spec(bytes), '''\ -bytes(iterable_of_ints) -> bytes -bytes(string, encoding[, errors]) -> bytes -bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer -bytes(int) -> bytes object of size given by the parameter initialized with null bytes -bytes() -> empty bytes object''') - - # Test more than max lines - def f(): pass - f.__doc__ = 'a\n' * 15 - self.assertEqual(get_spec(f), '()' + '\na' * calltip._MAX_LINES) - - def test_functions(self): - def t1(): 'doc' - t1.tip = "()" - def t2(a, b=None): 'doc' - t2.tip = "(a, b=None)" - def t3(a, *args): 'doc' - t3.tip = "(a, *args)" - def t4(*args): 'doc' - t4.tip = "(*args)" - 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): - with self.subTest(func=func): - self.assertEqual(get_spec(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__): - with self.subTest(meth=meth): - self.assertEqual(get_spec(meth), meth.tip + doc) - self.assertEqual(get_spec(TC.cm), "(a)" + doc) - self.assertEqual(get_spec(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)"),): - with self.subTest(meth=meth, mtip=mtip): - self.assertEqual(get_spec(meth), mtip + doc) - - def test_starred_parameter(self): - # test that starred first parameter is *not* removed from argspec - class C: - def m1(*args): pass - c = C() - for meth, mtip in ((C.m1, '(*args)'), (c.m1, "(*args)"),): - with self.subTest(meth=meth, mtip=mtip): - self.assertEqual(get_spec(meth), mtip) - - def test_invalid_method_get_spec(self): - class C: - def m2(**kwargs): pass - class Test: - def __call__(*, a): pass - - mtip = calltip._invalid_method - self.assertEqual(get_spec(C().m2), mtip) - self.assertEqual(get_spec(Test()), mtip) - - def test_non_ascii_name(self): - # test that re works to delete a first parameter name that - # includes non-ascii chars, such as various forms of A. - uni = "(A\u0391\u0410\u05d0\u0627\u0905\u1e00\u3042, a)" - assert calltip._first_param.sub('', uni) == '(a)' - - def test_no_docstring(self): - for meth, mtip in ((TC.nd, "(self)"), (tc.nd, "()")): - with self.subTest(meth=meth, mtip=mtip): - self.assertEqual(get_spec(meth), mtip) - - def test_attribute_exception(self): - class NoCall: - def __getattr__(self, name): - raise BaseException - class CallA(NoCall): - def __call__(oui, a, b, c): - pass - class CallB(NoCall): - def __call__(self, ci): - pass - - for meth, mtip in ((NoCall, default_tip), (CallA, default_tip), - (NoCall(), ''), (CallA(), '(a, b, c)'), - (CallB(), '(ci)')): - with self.subTest(meth=meth, mtip=mtip): - self.assertEqual(get_spec(meth), mtip) - - def test_non_callables(self): - for obj in (0, 0.0, '0', b'0', [], {}): - with self.subTest(obj=obj): - self.assertEqual(get_spec(obj), '') - - -class Get_entityTest(unittest.TestCase): - def test_bad_entity(self): - self.assertIsNone(calltip.get_entity('1/0')) - def test_good_entity(self): - self.assertIs(calltip.get_entity('int'), int) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_calltip_w.py b/Lib/idlelib/idle_test/test_calltip_w.py deleted file mode 100644 index a5ec76e..0000000 --- a/Lib/idlelib/idle_test/test_calltip_w.py +++ /dev/null @@ -1,29 +0,0 @@ -"Test calltip_w, coverage 18%." - -from idlelib import calltip_w -import unittest -from test.support import requires -from tkinter import Tk, Text - - -class CallTipWindowTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - cls.text = Text(cls.root) - cls.calltip = calltip_w.CalltipWindow(cls.text) - - @classmethod - def tearDownClass(cls): - cls.root.update_idletasks() - cls.root.destroy() - del cls.text, cls.root - - def test_init(self): - self.assertEqual(self.calltip.anchor_widget, self.text) - -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 new file mode 100644 index 0000000..147119c --- /dev/null +++ b/Lib/idlelib/idle_test/test_calltips.py @@ -0,0 +1,185 @@ +import unittest +import idlelib.CallTips as ct +CTi = ct.CallTips() # needed for get_entity test in 2.7 +import textwrap +import types +import warnings + +default_tip = '' + +# Test Class TC is used in multiple get_argspec test methods +class TC(object): + 'doc' + tip = "(ai=None, *args)" + def __init__(self, ai=None, *b): 'doc' + __init__.tip = "(self, ai=None, *args)" + def t1(self): 'doc' + t1.tip = "(self)" + def t2(self, ai, b=None): 'doc' + t2.tip = "(self, ai, b=None)" + def t3(self, ai, *args): 'doc' + t3.tip = "(self, ai, *args)" + def t4(self, *args): 'doc' + t4.tip = "(self, *args)" + def t5(self, ai, b=None, *args, **kw): 'doc' + t5.tip = "(self, ai, b=None, *args, **kwargs)" + def t6(no, self): 'doc' + t6.tip = "(no, self)" + def __call__(self, ci): 'doc' + __call__.tip = "(self, ci)" + # attaching .tip to wrapped methods does not work + @classmethod + def cm(cls, a): 'doc' + @staticmethod + def sm(b): 'doc' + +tc = TC() + +signature = ct.get_arg_text # 2.7 and 3.x use different functions +class Get_signatureTest(unittest.TestCase): + # The signature function must return a string, even if blank. + # Test a variety of objects to be sure that none cause it to raise + # (quite aside from getting as correct an answer as possible). + # The tests of builtins may break if the docstrings change, + # but a red buildbot is better than a user crash (as has happened). + # For a simple mismatch, change the expected output to the actual. + + def test_builtins(self): + # 2.7 puts '()\n' where 3.x does not, other minor differences + + # Python class that inherits builtin methods + class List(list): "List() doc" + # Simulate builtin with no docstring for default argspec test + class SB: __call__ = None + + def gtest(obj, out): + self.assertEqual(signature(obj), out) + + if List.__doc__ is not None: + gtest(List, '()\n' + List.__doc__) + gtest(list.__new__, + 'T.__new__(S, ...) -> a new object with type S, a subtype of T') + gtest(list.__init__, + 'x.__init__(...) initializes x; see help(type(x)) for signature') + append_doc = "L.append(object) -- append object to end" + gtest(list.append, append_doc) + gtest([].append, append_doc) + gtest(List.append, append_doc) + + gtest(types.MethodType, '()\ninstancemethod(function, instance, class)') + gtest(SB(), default_tip) + + def test_signature_wrap(self): + # This is also a test of an old-style class + 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)''') + + def test_docline_truncation(self): + def f(): pass + f.__doc__ = 'a'*300 + self.assertEqual(signature(f), '()\n' + 'a' * (ct._MAX_COLS-3) + '...') + + def test_multiline_docstring(self): + # Test fewer lines than max. + self.assertEqual(signature(list), + "()\nlist() -> new empty list\n" + "list(iterable) -> new list initialized from iterable's items") + + # Test max lines and line (currently) too long. + def f(): + pass + s = 'a\nb\nc\nd\n' + f.__doc__ = s + 300 * 'e' + 'f' + self.assertEqual(signature(f), + '()\n' + s + (ct._MAX_COLS - 3) * 'e' + '...') + + def test_functions(self): + def t1(): 'doc' + t1.tip = "()" + def t2(a, b=None): 'doc' + t2.tip = "(a, b=None)" + def t3(a, *args): 'doc' + t3.tip = "(a, *args)" + def t4(*args): 'doc' + t4.tip = "(*args)" + def t5(a, b=None, *args, **kwds): 'doc' + t5.tip = "(a, b=None, *args, **kwargs)" + + doc = '\ndoc' if t1.__doc__ is not None else '' + for func in (t1, t2, t3, t4, t5, TC): + 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 + 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 + doc) + + def test_starred_parameter(self): + # test that starred first parameter is *not* removed from argspec + class C: + def m1(*args): pass + def m2(**kwds): pass + def f1(args, kwargs, *a, **k): pass + def f2(args, kwargs, args1, kwargs1, *a, **k): pass + c = C() + self.assertEqual(signature(C.m1), '(*args)') + self.assertEqual(signature(c.m1), '(*args)') + self.assertEqual(signature(C.m2), '(**kwargs)') + self.assertEqual(signature(c.m2), '(**kwargs)') + self.assertEqual(signature(f1), '(args, kwargs, *args1, **kwargs1)') + self.assertEqual(signature(f2), + '(args, kwargs, args1, kwargs1, *args2, **kwargs2)') + + def test_no_docstring(self): + def nd(s): pass + TC.nd = nd + self.assertEqual(signature(nd), "(s)") + self.assertEqual(signature(TC.nd), "(s)") + self.assertEqual(signature(tc.nd), "()") + + def test_attribute_exception(self): + class NoCall(object): + def __getattr__(self, name): + raise BaseException + class Call(NoCall): + def __call__(self, ci): + pass + for meth, mtip in ((NoCall, '()'), (Call, '()'), + (NoCall(), ''), (Call(), '(ci)')): + self.assertEqual(signature(meth), mtip) + + def test_non_callables(self): + for obj in (0, 0.0, '0', b'0', [], {}): + self.assertEqual(signature(obj), '') + +class Get_entityTest(unittest.TestCase): + # In 3.x, get_entity changed from 'instance method' to module function + # since 'self' not used. Use dummy instance until change 2.7 also. + def test_bad_entity(self): + self.assertIsNone(CTi.get_entity('1//0')) + def test_good_entity(self): + self.assertIs(CTi.get_entity('int'), int) + +class Py2Test(unittest.TestCase): + def test_paramtuple_float(self): + # 18539: (a,b) becomes '.0' in code object; change that but not 0.0 + with warnings.catch_warnings(): + # Suppess message of py3 deprecation of parameter unpacking + warnings.simplefilter("ignore") + exec "def f((a,b), c=0.0): pass" + self.assertEqual(signature(f), '(<tuple>, c=0.0)') + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=False) diff --git a/Lib/idlelib/idle_test/test_codecontext.py b/Lib/idlelib/idle_test/test_codecontext.py deleted file mode 100644 index 3ec49e9..0000000 --- a/Lib/idlelib/idle_test/test_codecontext.py +++ /dev/null @@ -1,447 +0,0 @@ -"Test codecontext, coverage 100%" - -from idlelib import codecontext -import unittest -import unittest.mock -from test.support import requires -from tkinter import NSEW, Tk, Frame, Text, TclError - -from unittest import mock -import re -from idlelib import config - - -usercfg = codecontext.idleConf.userCfg -testcfg = { - 'main': config.IdleUserConfParser(''), - 'highlight': config.IdleUserConfParser(''), - 'keys': config.IdleUserConfParser(''), - 'extensions': config.IdleUserConfParser(''), -} -code_sample = """\ - -class C1(): - # Class comment. - def __init__(self, a, b): - self.a = a - self.b = b - def compare(self): - if a > b: - return a - elif a < b: - return b - else: - return None -""" - - -class DummyEditwin: - def __init__(self, root, frame, text): - self.root = root - self.top = root - self.text_frame = frame - self.text = text - self.label = '' - - def getlineno(self, index): - return int(float(self.text.index(index))) - - def update_menu_label(self, **kwargs): - self.label = kwargs['label'] - - -class CodeContextTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - root = cls.root = Tk() - root.withdraw() - frame = cls.frame = Frame(root) - text = cls.text = Text(frame) - text.insert('1.0', code_sample) - # Need to pack for creation of code context text widget. - frame.pack(side='left', fill='both', expand=1) - text.grid(row=1, column=1, sticky=NSEW) - cls.editor = DummyEditwin(root, frame, text) - codecontext.idleConf.userCfg = testcfg - - @classmethod - def tearDownClass(cls): - codecontext.idleConf.userCfg = usercfg - cls.editor.text.delete('1.0', 'end') - del cls.editor, cls.frame, cls.text - cls.root.update_idletasks() - cls.root.destroy() - del cls.root - - def setUp(self): - self.text.yview(0) - self.text['font'] = 'TkFixedFont' - self.cc = codecontext.CodeContext(self.editor) - - self.highlight_cfg = {"background": '#abcdef', - "foreground": '#123456'} - orig_idleConf_GetHighlight = codecontext.idleConf.GetHighlight - def mock_idleconf_GetHighlight(theme, element): - if element == 'context': - return self.highlight_cfg - return orig_idleConf_GetHighlight(theme, element) - GetHighlight_patcher = unittest.mock.patch.object( - codecontext.idleConf, 'GetHighlight', mock_idleconf_GetHighlight) - GetHighlight_patcher.start() - self.addCleanup(GetHighlight_patcher.stop) - - self.font_override = 'TkFixedFont' - def mock_idleconf_GetFont(root, configType, section): - return self.font_override - GetFont_patcher = unittest.mock.patch.object( - codecontext.idleConf, 'GetFont', mock_idleconf_GetFont) - GetFont_patcher.start() - self.addCleanup(GetFont_patcher.stop) - - def tearDown(self): - if self.cc.context: - self.cc.context.destroy() - # Explicitly call __del__ to remove scheduled scripts. - self.cc.__del__() - del self.cc.context, self.cc - - def test_init(self): - eq = self.assertEqual - ed = self.editor - cc = self.cc - - eq(cc.editwin, ed) - eq(cc.text, ed.text) - eq(cc.text['font'], ed.text['font']) - self.assertIsNone(cc.context) - eq(cc.info, [(0, -1, '', False)]) - eq(cc.topvisible, 1) - self.assertIsNone(self.cc.t1) - - def test_del(self): - self.cc.__del__() - - def test_del_with_timer(self): - timer = self.cc.t1 = self.text.after(10000, lambda: None) - self.cc.__del__() - with self.assertRaises(TclError) as cm: - self.root.tk.call('after', 'info', timer) - self.assertIn("doesn't exist", str(cm.exception)) - - def test_reload(self): - codecontext.CodeContext.reload() - self.assertEqual(self.cc.context_depth, 15) - - def test_toggle_code_context_event(self): - eq = self.assertEqual - cc = self.cc - toggle = cc.toggle_code_context_event - - # Make sure code context is off. - if cc.context: - toggle() - - # Toggle on. - toggle() - self.assertIsNotNone(cc.context) - eq(cc.context['font'], self.text['font']) - eq(cc.context['fg'], self.highlight_cfg['foreground']) - eq(cc.context['bg'], self.highlight_cfg['background']) - eq(cc.context.get('1.0', 'end-1c'), '') - eq(cc.editwin.label, 'Hide Code Context') - eq(self.root.tk.call('after', 'info', self.cc.t1)[1], 'timer') - - # Toggle off. - toggle() - self.assertIsNone(cc.context) - eq(cc.editwin.label, 'Show Code Context') - self.assertIsNone(self.cc.t1) - - # Scroll down and toggle back on. - line11_context = '\n'.join(x[2] for x in cc.get_context(11)[0]) - cc.text.yview(11) - toggle() - eq(cc.context.get('1.0', 'end-1c'), line11_context) - - # Toggle off and on again. - toggle() - toggle() - eq(cc.context.get('1.0', 'end-1c'), line11_context) - - def test_get_context(self): - eq = self.assertEqual - gc = self.cc.get_context - - # stopline must be greater than 0. - with self.assertRaises(AssertionError): - gc(1, stopline=0) - - eq(gc(3), ([(2, 0, 'class C1():', 'class')], 0)) - - # Don't return comment. - eq(gc(4), ([(2, 0, 'class C1():', 'class')], 0)) - - # Two indentation levels and no comment. - eq(gc(5), ([(2, 0, 'class C1():', 'class'), - (4, 4, ' def __init__(self, a, b):', 'def')], 0)) - - # Only one 'def' is returned, not both at the same indent level. - eq(gc(10), ([(2, 0, 'class C1():', 'class'), - (7, 4, ' def compare(self):', 'def'), - (8, 8, ' if a > b:', 'if')], 0)) - - # With 'elif', also show the 'if' even though it's at the same level. - eq(gc(11), ([(2, 0, 'class C1():', 'class'), - (7, 4, ' def compare(self):', 'def'), - (8, 8, ' if a > b:', 'if'), - (10, 8, ' elif a < b:', 'elif')], 0)) - - # Set stop_line to not go back to first line in source code. - # Return includes stop_line. - eq(gc(11, stopline=2), ([(2, 0, 'class C1():', 'class'), - (7, 4, ' def compare(self):', 'def'), - (8, 8, ' if a > b:', 'if'), - (10, 8, ' elif a < b:', 'elif')], 0)) - eq(gc(11, stopline=3), ([(7, 4, ' def compare(self):', 'def'), - (8, 8, ' if a > b:', 'if'), - (10, 8, ' elif a < b:', 'elif')], 4)) - eq(gc(11, stopline=8), ([(8, 8, ' if a > b:', 'if'), - (10, 8, ' elif a < b:', 'elif')], 8)) - - # Set stop_indent to test indent level to stop at. - eq(gc(11, stopindent=4), ([(7, 4, ' def compare(self):', 'def'), - (8, 8, ' if a > b:', 'if'), - (10, 8, ' elif a < b:', 'elif')], 4)) - # Check that the 'if' is included. - eq(gc(11, stopindent=8), ([(8, 8, ' if a > b:', 'if'), - (10, 8, ' elif a < b:', 'elif')], 8)) - - def test_update_code_context(self): - eq = self.assertEqual - cc = self.cc - # Ensure code context is active. - if not cc.context: - cc.toggle_code_context_event() - - # Invoke update_code_context without scrolling - nothing happens. - self.assertIsNone(cc.update_code_context()) - eq(cc.info, [(0, -1, '', False)]) - eq(cc.topvisible, 1) - - # Scroll down to line 1. - cc.text.yview(1) - cc.update_code_context() - eq(cc.info, [(0, -1, '', False)]) - eq(cc.topvisible, 2) - eq(cc.context.get('1.0', 'end-1c'), '') - - # Scroll down to line 2. - cc.text.yview(2) - cc.update_code_context() - eq(cc.info, [(0, -1, '', False), (2, 0, 'class C1():', 'class')]) - eq(cc.topvisible, 3) - eq(cc.context.get('1.0', 'end-1c'), 'class C1():') - - # Scroll down to line 3. Since it's a comment, nothing changes. - cc.text.yview(3) - cc.update_code_context() - eq(cc.info, [(0, -1, '', False), (2, 0, 'class C1():', 'class')]) - eq(cc.topvisible, 4) - eq(cc.context.get('1.0', 'end-1c'), 'class C1():') - - # Scroll down to line 4. - cc.text.yview(4) - cc.update_code_context() - eq(cc.info, [(0, -1, '', False), - (2, 0, 'class C1():', 'class'), - (4, 4, ' def __init__(self, a, b):', 'def')]) - eq(cc.topvisible, 5) - eq(cc.context.get('1.0', 'end-1c'), 'class C1():\n' - ' def __init__(self, a, b):') - - # Scroll down to line 11. Last 'def' is removed. - cc.text.yview(11) - cc.update_code_context() - eq(cc.info, [(0, -1, '', False), - (2, 0, 'class C1():', 'class'), - (7, 4, ' def compare(self):', 'def'), - (8, 8, ' if a > b:', 'if'), - (10, 8, ' elif a < b:', 'elif')]) - eq(cc.topvisible, 12) - eq(cc.context.get('1.0', 'end-1c'), 'class C1():\n' - ' def compare(self):\n' - ' if a > b:\n' - ' elif a < b:') - - # No scroll. No update, even though context_depth changed. - cc.update_code_context() - cc.context_depth = 1 - eq(cc.info, [(0, -1, '', False), - (2, 0, 'class C1():', 'class'), - (7, 4, ' def compare(self):', 'def'), - (8, 8, ' if a > b:', 'if'), - (10, 8, ' elif a < b:', 'elif')]) - eq(cc.topvisible, 12) - eq(cc.context.get('1.0', 'end-1c'), 'class C1():\n' - ' def compare(self):\n' - ' if a > b:\n' - ' elif a < b:') - - # Scroll up. - cc.text.yview(5) - cc.update_code_context() - eq(cc.info, [(0, -1, '', False), - (2, 0, 'class C1():', 'class'), - (4, 4, ' def __init__(self, a, b):', 'def')]) - eq(cc.topvisible, 6) - # context_depth is 1. - eq(cc.context.get('1.0', 'end-1c'), ' def __init__(self, a, b):') - - def test_jumptoline(self): - eq = self.assertEqual - cc = self.cc - jump = cc.jumptoline - - if not cc.context: - cc.toggle_code_context_event() - - # Empty context. - cc.text.yview('2.0') - cc.update_code_context() - eq(cc.topvisible, 2) - cc.context.mark_set('insert', '1.5') - jump() - eq(cc.topvisible, 1) - - # 4 lines of context showing. - cc.text.yview('12.0') - cc.update_code_context() - eq(cc.topvisible, 12) - cc.context.mark_set('insert', '3.0') - jump() - eq(cc.topvisible, 8) - - # More context lines than limit. - cc.context_depth = 2 - cc.text.yview('12.0') - cc.update_code_context() - eq(cc.topvisible, 12) - cc.context.mark_set('insert', '1.0') - jump() - eq(cc.topvisible, 8) - - @mock.patch.object(codecontext.CodeContext, 'update_code_context') - def test_timer_event(self, mock_update): - # Ensure code context is not active. - if self.cc.context: - self.cc.toggle_code_context_event() - self.cc.timer_event() - mock_update.assert_not_called() - - # Activate code context. - self.cc.toggle_code_context_event() - self.cc.timer_event() - mock_update.assert_called() - - def test_font(self): - eq = self.assertEqual - cc = self.cc - - orig_font = cc.text['font'] - test_font = 'TkTextFont' - self.assertNotEqual(orig_font, test_font) - - # Ensure code context is not active. - if cc.context is not None: - cc.toggle_code_context_event() - - self.font_override = test_font - # Nothing breaks or changes with inactive code context. - cc.update_font() - - # Activate code context, previous font change is immediately effective. - cc.toggle_code_context_event() - eq(cc.context['font'], test_font) - - # Call the font update, change is picked up. - self.font_override = orig_font - cc.update_font() - eq(cc.context['font'], orig_font) - - def test_highlight_colors(self): - eq = self.assertEqual - cc = self.cc - - orig_colors = dict(self.highlight_cfg) - test_colors = {'background': '#222222', 'foreground': '#ffff00'} - - def assert_colors_are_equal(colors): - eq(cc.context['background'], colors['background']) - eq(cc.context['foreground'], colors['foreground']) - - # Ensure code context is not active. - if cc.context: - cc.toggle_code_context_event() - - self.highlight_cfg = test_colors - # Nothing breaks with inactive code context. - cc.update_highlight_colors() - - # Activate code context, previous colors change is immediately effective. - cc.toggle_code_context_event() - assert_colors_are_equal(test_colors) - - # Call colors update with no change to the configured colors. - cc.update_highlight_colors() - assert_colors_are_equal(test_colors) - - # Call the colors update with code context active, change is picked up. - self.highlight_cfg = orig_colors - cc.update_highlight_colors() - assert_colors_are_equal(orig_colors) - - -class HelperFunctionText(unittest.TestCase): - - def test_get_spaces_firstword(self): - get = codecontext.get_spaces_firstword - test_lines = ( - (' first word', (' ', 'first')), - ('\tfirst word', ('\t', 'first')), - (' \u19D4\u19D2: ', (' ', '\u19D4\u19D2')), - ('no spaces', ('', 'no')), - ('', ('', '')), - ('# TEST COMMENT', ('', '')), - (' (continuation)', (' ', '')) - ) - for line, expected_output in test_lines: - self.assertEqual(get(line), expected_output) - - # Send the pattern in the call. - self.assertEqual(get(' (continuation)', - c=re.compile(r'^(\s*)([^\s]*)')), - (' ', '(continuation)')) - - def test_get_line_info(self): - eq = self.assertEqual - gli = codecontext.get_line_info - lines = code_sample.splitlines() - - # Line 1 is not a BLOCKOPENER. - eq(gli(lines[0]), (codecontext.INFINITY, '', False)) - # Line 2 is a BLOCKOPENER without an indent. - eq(gli(lines[1]), (0, 'class C1():', 'class')) - # Line 3 is not a BLOCKOPENER and does not return the indent level. - eq(gli(lines[2]), (codecontext.INFINITY, ' # Class comment.', False)) - # Line 4 is a BLOCKOPENER and is indented. - eq(gli(lines[3]), (4, ' def __init__(self, a, b):', 'def')) - # Line 8 is a different BLOCKOPENER and is indented. - eq(gli(lines[7]), (8, ' if a > b:', 'if')) - # Test tab. - eq(gli('\tif a == b:'), (1, '\tif a == b:', 'if')) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_colorizer.py b/Lib/idlelib/idle_test/test_colorizer.py deleted file mode 100644 index c31c492..0000000 --- a/Lib/idlelib/idle_test/test_colorizer.py +++ /dev/null @@ -1,423 +0,0 @@ -"Test colorizer, coverage 93%." - -from idlelib import colorizer -from test.support import requires -import unittest -from unittest import mock - -from functools import partial -from tkinter import Tk, Text -from idlelib import config -from idlelib.percolator import Percolator - - -usercfg = colorizer.idleConf.userCfg -testcfg = { - 'main': config.IdleUserConfParser(''), - 'highlight': config.IdleUserConfParser(''), - 'keys': config.IdleUserConfParser(''), - 'extensions': config.IdleUserConfParser(''), -} - -source = ( - "if True: int ('1') # keyword, builtin, string, comment\n" - "elif False: print(0) # 'string' in comment\n" - "else: float(None) # if in comment\n" - "if iF + If + IF: 'keyword matching must respect case'\n" - "if'': x or'' # valid string-keyword no-space combinations\n" - "async def f(): await g()\n" - "'x', '''x''', \"x\", \"\"\"x\"\"\"\n" - ) - - -def setUpModule(): - colorizer.idleConf.userCfg = testcfg - - -def tearDownModule(): - colorizer.idleConf.userCfg = usercfg - - -class FunctionTest(unittest.TestCase): - - def test_any(self): - self.assertEqual(colorizer.any('test', ('a', 'b', 'cd')), - '(?P<test>a|b|cd)') - - def test_make_pat(self): - # Tested in more detail by testing prog. - self.assertTrue(colorizer.make_pat()) - - def test_prog(self): - prog = colorizer.prog - eq = self.assertEqual - line = 'def f():\n print("hello")\n' - m = prog.search(line) - eq(m.groupdict()['KEYWORD'], 'def') - m = prog.search(line, m.end()) - eq(m.groupdict()['SYNC'], '\n') - m = prog.search(line, m.end()) - eq(m.groupdict()['BUILTIN'], 'print') - m = prog.search(line, m.end()) - eq(m.groupdict()['STRING'], '"hello"') - m = prog.search(line, m.end()) - eq(m.groupdict()['SYNC'], '\n') - - def test_idprog(self): - idprog = colorizer.idprog - m = idprog.match('nospace') - self.assertIsNone(m) - m = idprog.match(' space') - self.assertEqual(m.group(0), ' space') - - -class ColorConfigTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - root = cls.root = Tk() - root.withdraw() - cls.text = Text(root) - - @classmethod - def tearDownClass(cls): - del cls.text - cls.root.update_idletasks() - cls.root.destroy() - del cls.root - - def test_color_config(self): - text = self.text - eq = self.assertEqual - colorizer.color_config(text) - # Uses IDLE Classic theme as default. - eq(text['background'], '#ffffff') - eq(text['foreground'], '#000000') - eq(text['selectbackground'], 'gray') - eq(text['selectforeground'], '#000000') - eq(text['insertbackground'], 'black') - eq(text['inactiveselectbackground'], 'gray') - - -class ColorDelegatorInstantiationTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - root = cls.root = Tk() - root.withdraw() - text = cls.text = Text(root) - - @classmethod - def tearDownClass(cls): - del cls.text - cls.root.update_idletasks() - cls.root.destroy() - del cls.root - - def setUp(self): - self.color = colorizer.ColorDelegator() - - def tearDown(self): - self.color.close() - self.text.delete('1.0', 'end') - self.color.resetcache() - del self.color - - def test_init(self): - color = self.color - self.assertIsInstance(color, colorizer.ColorDelegator) - - def test_init_state(self): - # init_state() is called during the instantiation of - # ColorDelegator in setUp(). - color = self.color - self.assertIsNone(color.after_id) - self.assertTrue(color.allow_colorizing) - self.assertFalse(color.colorizing) - self.assertFalse(color.stop_colorizing) - - -class ColorDelegatorTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - root = cls.root = Tk() - root.withdraw() - text = cls.text = Text(root) - cls.percolator = Percolator(text) - # Delegator stack = [Delegator(text)] - - @classmethod - def tearDownClass(cls): - cls.percolator.redir.close() - del cls.percolator, cls.text - cls.root.update_idletasks() - cls.root.destroy() - del cls.root - - def setUp(self): - self.color = colorizer.ColorDelegator() - self.percolator.insertfilter(self.color) - # Calls color.setdelegate(Delegator(text)). - - def tearDown(self): - self.color.close() - self.percolator.removefilter(self.color) - self.text.delete('1.0', 'end') - self.color.resetcache() - del self.color - - def test_setdelegate(self): - # Called in setUp when filter is attached to percolator. - color = self.color - self.assertIsInstance(color.delegate, colorizer.Delegator) - # It is too late to mock notify_range, so test side effect. - self.assertEqual(self.root.tk.call( - 'after', 'info', color.after_id)[1], 'timer') - - def test_LoadTagDefs(self): - highlight = partial(config.idleConf.GetHighlight, theme='IDLE Classic') - for tag, colors in self.color.tagdefs.items(): - with self.subTest(tag=tag): - self.assertIn('background', colors) - self.assertIn('foreground', colors) - if tag not in ('SYNC', 'TODO'): - self.assertEqual(colors, highlight(element=tag.lower())) - - def test_config_colors(self): - text = self.text - highlight = partial(config.idleConf.GetHighlight, theme='IDLE Classic') - for tag in self.color.tagdefs: - for plane in ('background', 'foreground'): - with self.subTest(tag=tag, plane=plane): - if tag in ('SYNC', 'TODO'): - self.assertEqual(text.tag_cget(tag, plane), '') - else: - self.assertEqual(text.tag_cget(tag, plane), - highlight(element=tag.lower())[plane]) - # 'sel' is marked as the highest priority. - self.assertEqual(text.tag_names()[-1], 'sel') - - @mock.patch.object(colorizer.ColorDelegator, 'notify_range') - def test_insert(self, mock_notify): - text = self.text - # Initial text. - text.insert('insert', 'foo') - self.assertEqual(text.get('1.0', 'end'), 'foo\n') - mock_notify.assert_called_with('1.0', '1.0+3c') - # Additional text. - text.insert('insert', 'barbaz') - self.assertEqual(text.get('1.0', 'end'), 'foobarbaz\n') - mock_notify.assert_called_with('1.3', '1.3+6c') - - @mock.patch.object(colorizer.ColorDelegator, 'notify_range') - def test_delete(self, mock_notify): - text = self.text - # Initialize text. - text.insert('insert', 'abcdefghi') - self.assertEqual(text.get('1.0', 'end'), 'abcdefghi\n') - # Delete single character. - text.delete('1.7') - self.assertEqual(text.get('1.0', 'end'), 'abcdefgi\n') - mock_notify.assert_called_with('1.7') - # Delete multiple characters. - text.delete('1.3', '1.6') - self.assertEqual(text.get('1.0', 'end'), 'abcgi\n') - mock_notify.assert_called_with('1.3') - - def test_notify_range(self): - text = self.text - color = self.color - eq = self.assertEqual - - # Colorizing already scheduled. - save_id = color.after_id - eq(self.root.tk.call('after', 'info', save_id)[1], 'timer') - self.assertFalse(color.colorizing) - self.assertFalse(color.stop_colorizing) - self.assertTrue(color.allow_colorizing) - - # Coloring scheduled and colorizing in progress. - color.colorizing = True - color.notify_range('1.0', 'end') - self.assertFalse(color.stop_colorizing) - eq(color.after_id, save_id) - - # No colorizing scheduled and colorizing in progress. - text.after_cancel(save_id) - color.after_id = None - color.notify_range('1.0', '1.0+3c') - self.assertTrue(color.stop_colorizing) - self.assertIsNotNone(color.after_id) - eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer') - # New event scheduled. - self.assertNotEqual(color.after_id, save_id) - - # No colorizing scheduled and colorizing off. - text.after_cancel(color.after_id) - color.after_id = None - color.allow_colorizing = False - color.notify_range('1.4', '1.4+10c') - # Nothing scheduled when colorizing is off. - self.assertIsNone(color.after_id) - - def test_toggle_colorize_event(self): - color = self.color - eq = self.assertEqual - - # Starts with colorizing allowed and scheduled. - self.assertFalse(color.colorizing) - self.assertFalse(color.stop_colorizing) - self.assertTrue(color.allow_colorizing) - eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer') - - # Toggle colorizing off. - color.toggle_colorize_event() - self.assertIsNone(color.after_id) - self.assertFalse(color.colorizing) - self.assertFalse(color.stop_colorizing) - self.assertFalse(color.allow_colorizing) - - # Toggle on while colorizing in progress (doesn't add timer). - color.colorizing = True - color.toggle_colorize_event() - self.assertIsNone(color.after_id) - self.assertTrue(color.colorizing) - self.assertFalse(color.stop_colorizing) - self.assertTrue(color.allow_colorizing) - - # Toggle off while colorizing in progress. - color.toggle_colorize_event() - self.assertIsNone(color.after_id) - self.assertTrue(color.colorizing) - self.assertTrue(color.stop_colorizing) - self.assertFalse(color.allow_colorizing) - - # Toggle on while colorizing not in progress. - color.colorizing = False - color.toggle_colorize_event() - eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer') - self.assertFalse(color.colorizing) - self.assertTrue(color.stop_colorizing) - self.assertTrue(color.allow_colorizing) - - @mock.patch.object(colorizer.ColorDelegator, 'recolorize_main') - def test_recolorize(self, mock_recmain): - text = self.text - color = self.color - eq = self.assertEqual - # Call recolorize manually and not scheduled. - text.after_cancel(color.after_id) - - # No delegate. - save_delegate = color.delegate - color.delegate = None - color.recolorize() - mock_recmain.assert_not_called() - color.delegate = save_delegate - - # Toggle off colorizing. - color.allow_colorizing = False - color.recolorize() - mock_recmain.assert_not_called() - color.allow_colorizing = True - - # Colorizing in progress. - color.colorizing = True - color.recolorize() - mock_recmain.assert_not_called() - color.colorizing = False - - # Colorizing is done, but not completed, so rescheduled. - color.recolorize() - self.assertFalse(color.stop_colorizing) - self.assertFalse(color.colorizing) - mock_recmain.assert_called() - eq(mock_recmain.call_count, 1) - # Rescheduled when TODO tag still exists. - eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer') - - # No changes to text, so no scheduling added. - text.tag_remove('TODO', '1.0', 'end') - color.recolorize() - self.assertFalse(color.stop_colorizing) - self.assertFalse(color.colorizing) - mock_recmain.assert_called() - eq(mock_recmain.call_count, 2) - self.assertIsNone(color.after_id) - - @mock.patch.object(colorizer.ColorDelegator, 'notify_range') - def test_recolorize_main(self, mock_notify): - text = self.text - color = self.color - eq = self.assertEqual - - text.insert('insert', source) - expected = (('1.0', ('KEYWORD',)), ('1.2', ()), ('1.3', ('KEYWORD',)), - ('1.7', ()), ('1.9', ('BUILTIN',)), ('1.14', ('STRING',)), - ('1.19', ('COMMENT',)), - ('2.1', ('KEYWORD',)), ('2.18', ()), ('2.25', ('COMMENT',)), - ('3.6', ('BUILTIN',)), ('3.12', ('KEYWORD',)), ('3.21', ('COMMENT',)), - ('4.0', ('KEYWORD',)), ('4.3', ()), ('4.6', ()), - ('5.2', ('STRING',)), ('5.8', ('KEYWORD',)), ('5.10', ('STRING',)), - ('6.0', ('KEYWORD',)), ('6.10', ('DEFINITION',)), ('6.11', ()), - ('7.0', ('STRING',)), ('7.4', ()), ('7.5', ('STRING',)), - ('7.12', ()), ('7.14', ('STRING',)), - # SYNC at the end of every line. - ('1.55', ('SYNC',)), ('2.50', ('SYNC',)), ('3.34', ('SYNC',)), - ) - - # Nothing marked to do therefore no tags in text. - text.tag_remove('TODO', '1.0', 'end') - color.recolorize_main() - for tag in text.tag_names(): - with self.subTest(tag=tag): - eq(text.tag_ranges(tag), ()) - - # Source marked for processing. - text.tag_add('TODO', '1.0', 'end') - # Check some indexes. - color.recolorize_main() - for index, expected_tags in expected: - with self.subTest(index=index): - eq(text.tag_names(index), expected_tags) - - # Check for some tags for ranges. - eq(text.tag_nextrange('TODO', '1.0'), ()) - eq(text.tag_nextrange('KEYWORD', '1.0'), ('1.0', '1.2')) - eq(text.tag_nextrange('COMMENT', '2.0'), ('2.22', '2.43')) - eq(text.tag_nextrange('SYNC', '2.0'), ('2.43', '3.0')) - eq(text.tag_nextrange('STRING', '2.0'), ('4.17', '4.53')) - eq(text.tag_nextrange('STRING', '7.0'), ('7.0', '7.3')) - eq(text.tag_nextrange('STRING', '7.3'), ('7.5', '7.12')) - eq(text.tag_nextrange('STRING', '7.12'), ('7.14', '7.17')) - eq(text.tag_nextrange('STRING', '7.17'), ('7.19', '7.26')) - eq(text.tag_nextrange('SYNC', '7.0'), ('7.26', '9.0')) - - @mock.patch.object(colorizer.ColorDelegator, 'recolorize') - @mock.patch.object(colorizer.ColorDelegator, 'notify_range') - def test_removecolors(self, mock_notify, mock_recolorize): - text = self.text - color = self.color - text.insert('insert', source) - - color.recolorize_main() - # recolorize_main doesn't add these tags. - text.tag_add("ERROR", "1.0") - text.tag_add("TODO", "1.0") - text.tag_add("hit", "1.0") - for tag in color.tagdefs: - with self.subTest(tag=tag): - self.assertNotEqual(text.tag_ranges(tag), ()) - - color.removecolors() - for tag in color.tagdefs: - with self.subTest(tag=tag): - self.assertEqual(text.tag_ranges(tag), ()) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_config.py b/Lib/idlelib/idle_test/test_config.py deleted file mode 100644 index 697fda5..0000000 --- a/Lib/idlelib/idle_test/test_config.py +++ /dev/null @@ -1,805 +0,0 @@ -"""Test config, coverage 93%. -(100% for IdleConfParser, IdleUserConfParser*, ConfigChanges). -* Exception is OSError clause in Save method. -Much of IdleConf is also exercised by ConfigDialog and test_configdialog. -""" -from idlelib import config -import sys -import os -import tempfile -from test.support import captured_stderr, findfile -import unittest -from unittest import mock -import idlelib -from idlelib.idle_test.mock_idle import Func - -# Tests should not depend on fortuitous user configurations. -# They must not affect actual user .cfg files. -# Replace user parsers with empty parsers that cannot be saved -# due to getting '' as the filename when created. - -idleConf = config.idleConf -usercfg = idleConf.userCfg -testcfg = {} -usermain = testcfg['main'] = config.IdleUserConfParser('') -userhigh = testcfg['highlight'] = config.IdleUserConfParser('') -userkeys = testcfg['keys'] = config.IdleUserConfParser('') -userextn = testcfg['extensions'] = config.IdleUserConfParser('') - -def setUpModule(): - idleConf.userCfg = testcfg - idlelib.testing = True - -def tearDownModule(): - idleConf.userCfg = usercfg - idlelib.testing = False - - -class IdleConfParserTest(unittest.TestCase): - """Test that IdleConfParser works""" - - config = """ - [one] - one = false - two = true - three = 10 - - [two] - one = a string - two = true - three = false - """ - - def test_get(self): - parser = config.IdleConfParser('') - parser.read_string(self.config) - eq = self.assertEqual - - # Test with type argument. - self.assertIs(parser.Get('one', 'one', type='bool'), False) - self.assertIs(parser.Get('one', 'two', type='bool'), True) - eq(parser.Get('one', 'three', type='int'), 10) - eq(parser.Get('two', 'one'), 'a string') - self.assertIs(parser.Get('two', 'two', type='bool'), True) - self.assertIs(parser.Get('two', 'three', type='bool'), False) - - # Test without type should fallback to string. - eq(parser.Get('two', 'two'), 'true') - eq(parser.Get('two', 'three'), 'false') - - # If option not exist, should return None, or default. - self.assertIsNone(parser.Get('not', 'exist')) - eq(parser.Get('not', 'exist', default='DEFAULT'), 'DEFAULT') - - def test_get_option_list(self): - parser = config.IdleConfParser('') - parser.read_string(self.config) - get_list = parser.GetOptionList - self.assertCountEqual(get_list('one'), ['one', 'two', 'three']) - self.assertCountEqual(get_list('two'), ['one', 'two', 'three']) - self.assertEqual(get_list('not exist'), []) - - def test_load_nothing(self): - parser = config.IdleConfParser('') - parser.Load() - self.assertEqual(parser.sections(), []) - - def test_load_file(self): - # Borrow test/cfgparser.1 from test_configparser. - config_path = findfile('cfgparser.1') - parser = config.IdleConfParser(config_path) - parser.Load() - - self.assertEqual(parser.Get('Foo Bar', 'foo'), 'newbar') - self.assertEqual(parser.GetOptionList('Foo Bar'), ['foo']) - - -class IdleUserConfParserTest(unittest.TestCase): - """Test that IdleUserConfParser works""" - - def new_parser(self, path=''): - return config.IdleUserConfParser(path) - - def test_set_option(self): - parser = self.new_parser() - parser.add_section('Foo') - # Setting new option in existing section should return True. - self.assertTrue(parser.SetOption('Foo', 'bar', 'true')) - # Setting existing option with same value should return False. - self.assertFalse(parser.SetOption('Foo', 'bar', 'true')) - # Setting exiting option with new value should return True. - self.assertTrue(parser.SetOption('Foo', 'bar', 'false')) - self.assertEqual(parser.Get('Foo', 'bar'), 'false') - - # Setting option in new section should create section and return True. - self.assertTrue(parser.SetOption('Bar', 'bar', 'true')) - self.assertCountEqual(parser.sections(), ['Bar', 'Foo']) - self.assertEqual(parser.Get('Bar', 'bar'), 'true') - - def test_remove_option(self): - parser = self.new_parser() - parser.AddSection('Foo') - parser.SetOption('Foo', 'bar', 'true') - - self.assertTrue(parser.RemoveOption('Foo', 'bar')) - self.assertFalse(parser.RemoveOption('Foo', 'bar')) - self.assertFalse(parser.RemoveOption('Not', 'Exist')) - - def test_add_section(self): - parser = self.new_parser() - self.assertEqual(parser.sections(), []) - - # Should not add duplicate section. - # Configparser raises DuplicateError, IdleParser not. - parser.AddSection('Foo') - parser.AddSection('Foo') - parser.AddSection('Bar') - self.assertCountEqual(parser.sections(), ['Bar', 'Foo']) - - def test_remove_empty_sections(self): - parser = self.new_parser() - - parser.AddSection('Foo') - parser.AddSection('Bar') - parser.SetOption('Idle', 'name', 'val') - self.assertCountEqual(parser.sections(), ['Bar', 'Foo', 'Idle']) - parser.RemoveEmptySections() - self.assertEqual(parser.sections(), ['Idle']) - - def test_is_empty(self): - parser = self.new_parser() - - parser.AddSection('Foo') - parser.AddSection('Bar') - self.assertTrue(parser.IsEmpty()) - self.assertEqual(parser.sections(), []) - - parser.SetOption('Foo', 'bar', 'false') - parser.AddSection('Bar') - self.assertFalse(parser.IsEmpty()) - self.assertCountEqual(parser.sections(), ['Foo']) - - def test_save(self): - with tempfile.TemporaryDirectory() as tdir: - path = os.path.join(tdir, 'test.cfg') - parser = self.new_parser(path) - parser.AddSection('Foo') - parser.SetOption('Foo', 'bar', 'true') - - # Should save to path when config is not empty. - self.assertFalse(os.path.exists(path)) - parser.Save() - self.assertTrue(os.path.exists(path)) - - # Should remove the file from disk when config is empty. - parser.remove_section('Foo') - parser.Save() - self.assertFalse(os.path.exists(path)) - - -class IdleConfTest(unittest.TestCase): - """Test for idleConf""" - - @classmethod - def setUpClass(cls): - cls.config_string = {} - - conf = config.IdleConf(_utest=True) - if __name__ != '__main__': - idle_dir = os.path.dirname(__file__) - else: - idle_dir = os.path.abspath(sys.path[0]) - for ctype in conf.config_types: - config_path = os.path.join(idle_dir, '../config-%s.def' % ctype) - with open(config_path, 'r') as f: - cls.config_string[ctype] = f.read() - - cls.orig_warn = config._warn - config._warn = Func() - - @classmethod - def tearDownClass(cls): - config._warn = cls.orig_warn - - def new_config(self, _utest=False): - return config.IdleConf(_utest=_utest) - - def mock_config(self): - """Return a mocked idleConf - - Both default and user config used the same config-*.def - """ - conf = config.IdleConf(_utest=True) - for ctype in conf.config_types: - conf.defaultCfg[ctype] = config.IdleConfParser('') - conf.defaultCfg[ctype].read_string(self.config_string[ctype]) - conf.userCfg[ctype] = config.IdleUserConfParser('') - conf.userCfg[ctype].read_string(self.config_string[ctype]) - - return conf - - @unittest.skipIf(sys.platform.startswith('win'), 'this is test for unix system') - def test_get_user_cfg_dir_unix(self): - # Test to get user config directory under unix. - conf = self.new_config(_utest=True) - - # Check normal way should success - with mock.patch('os.path.expanduser', return_value='/home/foo'): - with mock.patch('os.path.exists', return_value=True): - self.assertEqual(conf.GetUserCfgDir(), '/home/foo/.idlerc') - - # Check os.getcwd should success - with mock.patch('os.path.expanduser', return_value='~'): - with mock.patch('os.getcwd', return_value='/home/foo/cpython'): - with mock.patch('os.mkdir'): - self.assertEqual(conf.GetUserCfgDir(), - '/home/foo/cpython/.idlerc') - - # Check user dir not exists and created failed should raise SystemExit - with mock.patch('os.path.join', return_value='/path/not/exists'): - with self.assertRaises(SystemExit): - with self.assertRaises(FileNotFoundError): - conf.GetUserCfgDir() - - @unittest.skipIf(not sys.platform.startswith('win'), 'this is test for Windows system') - def test_get_user_cfg_dir_windows(self): - # Test to get user config directory under Windows. - conf = self.new_config(_utest=True) - - # Check normal way should success - with mock.patch('os.path.expanduser', return_value='C:\\foo'): - with mock.patch('os.path.exists', return_value=True): - self.assertEqual(conf.GetUserCfgDir(), 'C:\\foo\\.idlerc') - - # Check os.getcwd should success - with mock.patch('os.path.expanduser', return_value='~'): - with mock.patch('os.getcwd', return_value='C:\\foo\\cpython'): - with mock.patch('os.mkdir'): - self.assertEqual(conf.GetUserCfgDir(), - 'C:\\foo\\cpython\\.idlerc') - - # Check user dir not exists and created failed should raise SystemExit - with mock.patch('os.path.join', return_value='/path/not/exists'): - with self.assertRaises(SystemExit): - with self.assertRaises(FileNotFoundError): - conf.GetUserCfgDir() - - def test_create_config_handlers(self): - conf = self.new_config(_utest=True) - - # Mock out idle_dir - idle_dir = '/home/foo' - with mock.patch.dict({'__name__': '__foo__'}): - with mock.patch('os.path.dirname', return_value=idle_dir): - conf.CreateConfigHandlers() - - # Check keys are equal - self.assertCountEqual(conf.defaultCfg.keys(), conf.config_types) - self.assertCountEqual(conf.userCfg.keys(), conf.config_types) - - # Check conf parser are correct type - for default_parser in conf.defaultCfg.values(): - self.assertIsInstance(default_parser, config.IdleConfParser) - for user_parser in conf.userCfg.values(): - self.assertIsInstance(user_parser, config.IdleUserConfParser) - - # Check config path are correct - for cfg_type, parser in conf.defaultCfg.items(): - self.assertEqual(parser.file, - os.path.join(idle_dir, f'config-{cfg_type}.def')) - for cfg_type, parser in conf.userCfg.items(): - self.assertEqual(parser.file, - os.path.join(conf.userdir or '#', f'config-{cfg_type}.cfg')) - - def test_load_cfg_files(self): - conf = self.new_config(_utest=True) - - # Borrow test/cfgparser.1 from test_configparser. - config_path = findfile('cfgparser.1') - conf.defaultCfg['foo'] = config.IdleConfParser(config_path) - conf.userCfg['foo'] = config.IdleUserConfParser(config_path) - - # Load all config from path - conf.LoadCfgFiles() - - eq = self.assertEqual - - # Check defaultCfg is loaded - eq(conf.defaultCfg['foo'].Get('Foo Bar', 'foo'), 'newbar') - eq(conf.defaultCfg['foo'].GetOptionList('Foo Bar'), ['foo']) - - # Check userCfg is loaded - eq(conf.userCfg['foo'].Get('Foo Bar', 'foo'), 'newbar') - eq(conf.userCfg['foo'].GetOptionList('Foo Bar'), ['foo']) - - def test_save_user_cfg_files(self): - conf = self.mock_config() - - with mock.patch('idlelib.config.IdleUserConfParser.Save') as m: - conf.SaveUserCfgFiles() - self.assertEqual(m.call_count, len(conf.userCfg)) - - def test_get_option(self): - conf = self.mock_config() - - eq = self.assertEqual - eq(conf.GetOption('main', 'EditorWindow', 'width'), '80') - eq(conf.GetOption('main', 'EditorWindow', 'width', type='int'), 80) - with mock.patch('idlelib.config._warn') as _warn: - eq(conf.GetOption('main', 'EditorWindow', 'font', type='int'), None) - eq(conf.GetOption('main', 'EditorWindow', 'NotExists'), None) - eq(conf.GetOption('main', 'EditorWindow', 'NotExists', default='NE'), 'NE') - eq(_warn.call_count, 4) - - def test_set_option(self): - conf = self.mock_config() - - conf.SetOption('main', 'Foo', 'bar', 'newbar') - self.assertEqual(conf.GetOption('main', 'Foo', 'bar'), 'newbar') - - def test_get_section_list(self): - conf = self.mock_config() - - self.assertCountEqual( - conf.GetSectionList('default', 'main'), - ['General', 'EditorWindow', 'PyShell', 'Indent', 'Theme', - 'Keys', 'History', 'HelpFiles']) - self.assertCountEqual( - conf.GetSectionList('user', 'main'), - ['General', 'EditorWindow', 'PyShell', 'Indent', 'Theme', - 'Keys', 'History', 'HelpFiles']) - - with self.assertRaises(config.InvalidConfigSet): - conf.GetSectionList('foobar', 'main') - with self.assertRaises(config.InvalidConfigType): - conf.GetSectionList('default', 'notexists') - - def test_get_highlight(self): - conf = self.mock_config() - - eq = self.assertEqual - eq(conf.GetHighlight('IDLE Classic', 'normal'), {'foreground': '#000000', - 'background': '#ffffff'}) - - # Test cursor (this background should be normal-background) - eq(conf.GetHighlight('IDLE Classic', 'cursor'), {'foreground': 'black', - 'background': '#ffffff'}) - - # Test get user themes - conf.SetOption('highlight', 'Foobar', 'normal-foreground', '#747474') - conf.SetOption('highlight', 'Foobar', 'normal-background', '#171717') - with mock.patch('idlelib.config._warn'): - eq(conf.GetHighlight('Foobar', 'normal'), {'foreground': '#747474', - 'background': '#171717'}) - - def test_get_theme_dict(self): - # TODO: finish. - conf = self.mock_config() - - # These two should be the same - self.assertEqual( - conf.GetThemeDict('default', 'IDLE Classic'), - conf.GetThemeDict('user', 'IDLE Classic')) - - with self.assertRaises(config.InvalidTheme): - conf.GetThemeDict('bad', 'IDLE Classic') - - def test_get_current_theme_and_keys(self): - conf = self.mock_config() - - self.assertEqual(conf.CurrentTheme(), conf.current_colors_and_keys('Theme')) - self.assertEqual(conf.CurrentKeys(), conf.current_colors_and_keys('Keys')) - - def test_current_colors_and_keys(self): - conf = self.mock_config() - - self.assertEqual(conf.current_colors_and_keys('Theme'), 'IDLE Classic') - - def test_default_keys(self): - current_platform = sys.platform - conf = self.new_config(_utest=True) - - sys.platform = 'win32' - self.assertEqual(conf.default_keys(), 'IDLE Classic Windows') - - sys.platform = 'darwin' - self.assertEqual(conf.default_keys(), 'IDLE Classic OSX') - - sys.platform = 'some-linux' - self.assertEqual(conf.default_keys(), 'IDLE Modern Unix') - - # Restore platform - sys.platform = current_platform - - def test_get_extensions(self): - userextn.read_string(''' - [ZzDummy] - enable = True - [DISABLE] - enable = False - ''') - eq = self.assertEqual - iGE = idleConf.GetExtensions - eq(iGE(shell_only=True), []) - eq(iGE(), ['ZzDummy']) - eq(iGE(editor_only=True), ['ZzDummy']) - eq(iGE(active_only=False), ['ZzDummy', 'DISABLE']) - eq(iGE(active_only=False, editor_only=True), ['ZzDummy', 'DISABLE']) - userextn.remove_section('ZzDummy') - userextn.remove_section('DISABLE') - - - def test_remove_key_bind_names(self): - conf = self.mock_config() - - self.assertCountEqual( - conf.RemoveKeyBindNames(conf.GetSectionList('default', 'extensions')), - ['AutoComplete', 'CodeContext', 'FormatParagraph', 'ParenMatch', 'ZzDummy']) - - def test_get_extn_name_for_event(self): - userextn.read_string(''' - [ZzDummy] - enable = True - ''') - eq = self.assertEqual - eq(idleConf.GetExtnNameForEvent('z-in'), 'ZzDummy') - eq(idleConf.GetExtnNameForEvent('z-out'), None) - userextn.remove_section('ZzDummy') - - def test_get_extension_keys(self): - userextn.read_string(''' - [ZzDummy] - enable = True - ''') - self.assertEqual(idleConf.GetExtensionKeys('ZzDummy'), - {'<<z-in>>': ['<Control-Shift-KeyRelease-Insert>']}) - userextn.remove_section('ZzDummy') -# need option key test -## key = ['<Option-Key-2>'] if sys.platform == 'darwin' else ['<Alt-Key-2>'] -## eq(conf.GetExtensionKeys('ZoomHeight'), {'<<zoom-height>>': key}) - - def test_get_extension_bindings(self): - userextn.read_string(''' - [ZzDummy] - enable = True - ''') - eq = self.assertEqual - iGEB = idleConf.GetExtensionBindings - eq(iGEB('NotExists'), {}) - expect = {'<<z-in>>': ['<Control-Shift-KeyRelease-Insert>'], - '<<z-out>>': ['<Control-Shift-KeyRelease-Delete>']} - eq(iGEB('ZzDummy'), expect) - userextn.remove_section('ZzDummy') - - def test_get_keybinding(self): - conf = self.mock_config() - - eq = self.assertEqual - eq(conf.GetKeyBinding('IDLE Modern Unix', '<<copy>>'), - ['<Control-Shift-Key-C>', '<Control-Key-Insert>']) - eq(conf.GetKeyBinding('IDLE Classic Unix', '<<copy>>'), - ['<Alt-Key-w>', '<Meta-Key-w>']) - eq(conf.GetKeyBinding('IDLE Classic Windows', '<<copy>>'), - ['<Control-Key-c>', '<Control-Key-C>']) - eq(conf.GetKeyBinding('IDLE Classic Mac', '<<copy>>'), ['<Command-Key-c>']) - eq(conf.GetKeyBinding('IDLE Classic OSX', '<<copy>>'), ['<Command-Key-c>']) - - # Test keybinding not exists - eq(conf.GetKeyBinding('NOT EXISTS', '<<copy>>'), []) - eq(conf.GetKeyBinding('IDLE Modern Unix', 'NOT EXISTS'), []) - - def test_get_current_keyset(self): - current_platform = sys.platform - conf = self.mock_config() - - # Ensure that platform isn't darwin - sys.platform = 'some-linux' - self.assertEqual(conf.GetCurrentKeySet(), conf.GetKeySet(conf.CurrentKeys())) - - # This should not be the same, since replace <Alt- to <Option-. - # Above depended on config-extensions.def having Alt keys, - # which is no longer true. - # sys.platform = 'darwin' - # self.assertNotEqual(conf.GetCurrentKeySet(), conf.GetKeySet(conf.CurrentKeys())) - - # Restore platform - sys.platform = current_platform - - def test_get_keyset(self): - conf = self.mock_config() - - # Conflict with key set, should be disable to '' - conf.defaultCfg['extensions'].add_section('Foobar') - conf.defaultCfg['extensions'].add_section('Foobar_cfgBindings') - conf.defaultCfg['extensions'].set('Foobar', 'enable', 'True') - conf.defaultCfg['extensions'].set('Foobar_cfgBindings', 'newfoo', '<Key-F3>') - self.assertEqual(conf.GetKeySet('IDLE Modern Unix')['<<newfoo>>'], '') - - def test_is_core_binding(self): - # XXX: Should move out the core keys to config file or other place - conf = self.mock_config() - - self.assertTrue(conf.IsCoreBinding('copy')) - self.assertTrue(conf.IsCoreBinding('cut')) - self.assertTrue(conf.IsCoreBinding('del-word-right')) - self.assertFalse(conf.IsCoreBinding('not-exists')) - - def test_extra_help_source_list(self): - # Test GetExtraHelpSourceList and GetAllExtraHelpSourcesList in same - # place to prevent prepare input data twice. - conf = self.mock_config() - - # Test default with no extra help source - self.assertEqual(conf.GetExtraHelpSourceList('default'), []) - self.assertEqual(conf.GetExtraHelpSourceList('user'), []) - with self.assertRaises(config.InvalidConfigSet): - self.assertEqual(conf.GetExtraHelpSourceList('bad'), []) - self.assertCountEqual( - conf.GetAllExtraHelpSourcesList(), - conf.GetExtraHelpSourceList('default') + conf.GetExtraHelpSourceList('user')) - - # Add help source to user config - conf.userCfg['main'].SetOption('HelpFiles', '4', 'Python;https://python.org') # This is bad input - conf.userCfg['main'].SetOption('HelpFiles', '3', 'Python:https://python.org') # This is bad input - conf.userCfg['main'].SetOption('HelpFiles', '2', 'Pillow;https://pillow.readthedocs.io/en/latest/') - conf.userCfg['main'].SetOption('HelpFiles', '1', 'IDLE;C:/Programs/Python36/Lib/idlelib/help.html') - self.assertEqual(conf.GetExtraHelpSourceList('user'), - [('IDLE', 'C:/Programs/Python36/Lib/idlelib/help.html', '1'), - ('Pillow', 'https://pillow.readthedocs.io/en/latest/', '2'), - ('Python', 'https://python.org', '4')]) - self.assertCountEqual( - conf.GetAllExtraHelpSourcesList(), - conf.GetExtraHelpSourceList('default') + conf.GetExtraHelpSourceList('user')) - - def test_get_font(self): - from test.support import requires - from tkinter import Tk - from tkinter.font import Font - conf = self.mock_config() - - requires('gui') - root = Tk() - root.withdraw() - - f = Font.actual(Font(name='TkFixedFont', exists=True, root=root)) - self.assertEqual( - conf.GetFont(root, 'main', 'EditorWindow'), - (f['family'], 10 if f['size'] <= 0 else f['size'], f['weight'])) - - # Cleanup root - root.destroy() - del root - - def test_get_core_keys(self): - conf = self.mock_config() - - eq = self.assertEqual - eq(conf.GetCoreKeys()['<<center-insert>>'], ['<Control-l>']) - eq(conf.GetCoreKeys()['<<copy>>'], ['<Control-c>', '<Control-C>']) - eq(conf.GetCoreKeys()['<<history-next>>'], ['<Alt-n>']) - eq(conf.GetCoreKeys('IDLE Classic Windows')['<<center-insert>>'], - ['<Control-Key-l>', '<Control-Key-L>']) - eq(conf.GetCoreKeys('IDLE Classic OSX')['<<copy>>'], ['<Command-Key-c>']) - eq(conf.GetCoreKeys('IDLE Classic Unix')['<<history-next>>'], - ['<Alt-Key-n>', '<Meta-Key-n>']) - eq(conf.GetCoreKeys('IDLE Modern Unix')['<<history-next>>'], - ['<Alt-Key-n>', '<Meta-Key-n>']) - - -class CurrentColorKeysTest(unittest.TestCase): - """ Test colorkeys function with user config [Theme] and [Keys] patterns. - - colorkeys = config.IdleConf.current_colors_and_keys - Test all patterns written by IDLE and some errors - Item 'default' should really be 'builtin' (versus 'custom). - """ - colorkeys = idleConf.current_colors_and_keys - default_theme = 'IDLE Classic' - default_keys = idleConf.default_keys() - - def test_old_builtin_theme(self): - # On initial installation, user main is blank. - self.assertEqual(self.colorkeys('Theme'), self.default_theme) - # For old default, name2 must be blank. - usermain.read_string(''' - [Theme] - default = True - ''') - # IDLE omits 'name' for default old builtin theme. - self.assertEqual(self.colorkeys('Theme'), self.default_theme) - # IDLE adds 'name' for non-default old builtin theme. - usermain['Theme']['name'] = 'IDLE New' - self.assertEqual(self.colorkeys('Theme'), 'IDLE New') - # Erroneous non-default old builtin reverts to default. - usermain['Theme']['name'] = 'non-existent' - self.assertEqual(self.colorkeys('Theme'), self.default_theme) - usermain.remove_section('Theme') - - def test_new_builtin_theme(self): - # IDLE writes name2 for new builtins. - usermain.read_string(''' - [Theme] - default = True - name2 = IDLE Dark - ''') - self.assertEqual(self.colorkeys('Theme'), 'IDLE Dark') - # Leftover 'name', not removed, is ignored. - usermain['Theme']['name'] = 'IDLE New' - self.assertEqual(self.colorkeys('Theme'), 'IDLE Dark') - # Erroneous non-default new builtin reverts to default. - usermain['Theme']['name2'] = 'non-existent' - self.assertEqual(self.colorkeys('Theme'), self.default_theme) - usermain.remove_section('Theme') - - def test_user_override_theme(self): - # Erroneous custom name (no definition) reverts to default. - usermain.read_string(''' - [Theme] - default = False - name = Custom Dark - ''') - self.assertEqual(self.colorkeys('Theme'), self.default_theme) - # Custom name is valid with matching Section name. - userhigh.read_string('[Custom Dark]\na=b') - self.assertEqual(self.colorkeys('Theme'), 'Custom Dark') - # Name2 is ignored. - usermain['Theme']['name2'] = 'non-existent' - self.assertEqual(self.colorkeys('Theme'), 'Custom Dark') - usermain.remove_section('Theme') - userhigh.remove_section('Custom Dark') - - def test_old_builtin_keys(self): - # On initial installation, user main is blank. - self.assertEqual(self.colorkeys('Keys'), self.default_keys) - # For old default, name2 must be blank, name is always used. - usermain.read_string(''' - [Keys] - default = True - name = IDLE Classic Unix - ''') - self.assertEqual(self.colorkeys('Keys'), 'IDLE Classic Unix') - # Erroneous non-default old builtin reverts to default. - usermain['Keys']['name'] = 'non-existent' - self.assertEqual(self.colorkeys('Keys'), self.default_keys) - usermain.remove_section('Keys') - - def test_new_builtin_keys(self): - # IDLE writes name2 for new builtins. - usermain.read_string(''' - [Keys] - default = True - name2 = IDLE Modern Unix - ''') - self.assertEqual(self.colorkeys('Keys'), 'IDLE Modern Unix') - # Leftover 'name', not removed, is ignored. - usermain['Keys']['name'] = 'IDLE Classic Unix' - self.assertEqual(self.colorkeys('Keys'), 'IDLE Modern Unix') - # Erroneous non-default new builtin reverts to default. - usermain['Keys']['name2'] = 'non-existent' - self.assertEqual(self.colorkeys('Keys'), self.default_keys) - usermain.remove_section('Keys') - - def test_user_override_keys(self): - # Erroneous custom name (no definition) reverts to default. - usermain.read_string(''' - [Keys] - default = False - name = Custom Keys - ''') - self.assertEqual(self.colorkeys('Keys'), self.default_keys) - # Custom name is valid with matching Section name. - userkeys.read_string('[Custom Keys]\na=b') - self.assertEqual(self.colorkeys('Keys'), 'Custom Keys') - # Name2 is ignored. - usermain['Keys']['name2'] = 'non-existent' - self.assertEqual(self.colorkeys('Keys'), 'Custom Keys') - usermain.remove_section('Keys') - userkeys.remove_section('Custom Keys') - - -class ChangesTest(unittest.TestCase): - - empty = {'main':{}, 'highlight':{}, 'keys':{}, 'extensions':{}} - - def load(self): # Test_add_option verifies that this works. - changes = self.changes - changes.add_option('main', 'Msec', 'mitem', 'mval') - changes.add_option('highlight', 'Hsec', 'hitem', 'hval') - changes.add_option('keys', 'Ksec', 'kitem', 'kval') - return changes - - loaded = {'main': {'Msec': {'mitem': 'mval'}}, - 'highlight': {'Hsec': {'hitem': 'hval'}}, - 'keys': {'Ksec': {'kitem':'kval'}}, - 'extensions': {}} - - def setUp(self): - self.changes = config.ConfigChanges() - - def test_init(self): - self.assertEqual(self.changes, self.empty) - - def test_add_option(self): - changes = self.load() - self.assertEqual(changes, self.loaded) - changes.add_option('main', 'Msec', 'mitem', 'mval') - self.assertEqual(changes, self.loaded) - - def test_save_option(self): # Static function does not touch changes. - save_option = self.changes.save_option - self.assertTrue(save_option('main', 'Indent', 'what', '0')) - self.assertFalse(save_option('main', 'Indent', 'what', '0')) - self.assertEqual(usermain['Indent']['what'], '0') - - self.assertTrue(save_option('main', 'Indent', 'use-spaces', '0')) - self.assertEqual(usermain['Indent']['use-spaces'], '0') - self.assertTrue(save_option('main', 'Indent', 'use-spaces', '1')) - self.assertFalse(usermain.has_option('Indent', 'use-spaces')) - usermain.remove_section('Indent') - - def test_save_added(self): - changes = self.load() - self.assertTrue(changes.save_all()) - self.assertEqual(usermain['Msec']['mitem'], 'mval') - self.assertEqual(userhigh['Hsec']['hitem'], 'hval') - self.assertEqual(userkeys['Ksec']['kitem'], 'kval') - changes.add_option('main', 'Msec', 'mitem', 'mval') - self.assertFalse(changes.save_all()) - usermain.remove_section('Msec') - userhigh.remove_section('Hsec') - userkeys.remove_section('Ksec') - - def test_save_help(self): - # Any change to HelpFiles overwrites entire section. - changes = self.changes - changes.save_option('main', 'HelpFiles', 'IDLE', 'idledoc') - changes.add_option('main', 'HelpFiles', 'ELDI', 'codeldi') - changes.save_all() - self.assertFalse(usermain.has_option('HelpFiles', 'IDLE')) - self.assertTrue(usermain.has_option('HelpFiles', 'ELDI')) - - def test_save_default(self): # Cover 2nd and 3rd false branches. - changes = self.changes - changes.add_option('main', 'Indent', 'use-spaces', '1') - # save_option returns False; cfg_type_changed remains False. - - # TODO: test that save_all calls usercfg Saves. - - def test_delete_section(self): - changes = self.load() - changes.delete_section('main', 'fake') # Test no exception. - self.assertEqual(changes, self.loaded) # Test nothing deleted. - for cfgtype, section in (('main', 'Msec'), ('keys', 'Ksec')): - testcfg[cfgtype].SetOption(section, 'name', 'value') - changes.delete_section(cfgtype, section) - with self.assertRaises(KeyError): - changes[cfgtype][section] # Test section gone from changes - testcfg[cfgtype][section] # and from mock userCfg. - # TODO test for save call. - - def test_clear(self): - changes = self.load() - changes.clear() - self.assertEqual(changes, self.empty) - - -class WarningTest(unittest.TestCase): - - def test_warn(self): - Equal = self.assertEqual - config._warned = set() - with captured_stderr() as stderr: - config._warn('warning', 'key') - Equal(config._warned, {('warning','key')}) - Equal(stderr.getvalue(), 'warning'+'\n') - with captured_stderr() as stderr: - config._warn('warning', 'key') - Equal(stderr.getvalue(), '') - with captured_stderr() as stderr: - config._warn('warn2', 'yek') - Equal(config._warned, {('warning','key'), ('warn2','yek')}) - Equal(stderr.getvalue(), 'warn2'+'\n') - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_config_key.py b/Lib/idlelib/idle_test/test_config_key.py deleted file mode 100644 index b7fe7fd..0000000 --- a/Lib/idlelib/idle_test/test_config_key.py +++ /dev/null @@ -1,291 +0,0 @@ -"""Test config_key, coverage 98%. - -Coverage is effectively 100%. Tkinter dialog is mocked, Mac-only line -may be skipped, and dummy function in bind test should not be called. -Not tested: exit with 'self.advanced or self.keys_ok(keys)) ...' False. -""" - -from idlelib import config_key -from test.support import requires -import unittest -from unittest import mock -from tkinter import Tk, TclError -from idlelib.idle_test.mock_idle import Func -from idlelib.idle_test.mock_tk import Mbox_func - -gkd = config_key.GetKeysDialog - - -class ValidationTest(unittest.TestCase): - "Test validation methods: ok, keys_ok, bind_ok." - - class Validator(gkd): - def __init__(self, *args, **kwargs): - config_key.GetKeysDialog.__init__(self, *args, **kwargs) - class list_keys_final: - get = Func() - self.list_keys_final = list_keys_final - get_modifiers = Func() - showerror = Mbox_func() - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - keylist = [['<Key-F12>'], ['<Control-Key-x>', '<Control-Key-X>']] - cls.dialog = cls.Validator( - cls.root, 'Title', '<<Test>>', keylist, _utest=True) - - @classmethod - def tearDownClass(cls): - cls.dialog.cancel() - cls.root.update_idletasks() - cls.root.destroy() - del cls.dialog, cls.root - - def setUp(self): - self.dialog.showerror.message = '' - # A test that needs a particular final key value should set it. - # A test that sets a non-blank modifier list should reset it to []. - - def test_ok_empty(self): - self.dialog.key_string.set(' ') - self.dialog.ok() - self.assertEqual(self.dialog.result, '') - self.assertEqual(self.dialog.showerror.message, 'No key specified.') - - def test_ok_good(self): - self.dialog.key_string.set('<Key-F11>') - self.dialog.list_keys_final.get.result = 'F11' - self.dialog.ok() - self.assertEqual(self.dialog.result, '<Key-F11>') - self.assertEqual(self.dialog.showerror.message, '') - - def test_keys_no_ending(self): - self.assertFalse(self.dialog.keys_ok('<Control-Shift')) - self.assertIn('Missing the final', self.dialog.showerror.message) - - def test_keys_no_modifier_bad(self): - self.dialog.list_keys_final.get.result = 'A' - self.assertFalse(self.dialog.keys_ok('<Key-A>')) - self.assertIn('No modifier', self.dialog.showerror.message) - - def test_keys_no_modifier_ok(self): - self.dialog.list_keys_final.get.result = 'F11' - self.assertTrue(self.dialog.keys_ok('<Key-F11>')) - self.assertEqual(self.dialog.showerror.message, '') - - def test_keys_shift_bad(self): - self.dialog.list_keys_final.get.result = 'a' - self.dialog.get_modifiers.result = ['Shift'] - self.assertFalse(self.dialog.keys_ok('<a>')) - self.assertIn('shift modifier', self.dialog.showerror.message) - self.dialog.get_modifiers.result = [] - - def test_keys_dup(self): - for mods, final, seq in (([], 'F12', '<Key-F12>'), - (['Control'], 'x', '<Control-Key-x>'), - (['Control'], 'X', '<Control-Key-X>')): - with self.subTest(m=mods, f=final, s=seq): - self.dialog.list_keys_final.get.result = final - self.dialog.get_modifiers.result = mods - self.assertFalse(self.dialog.keys_ok(seq)) - self.assertIn('already in use', self.dialog.showerror.message) - self.dialog.get_modifiers.result = [] - - def test_bind_ok(self): - self.assertTrue(self.dialog.bind_ok('<Control-Shift-Key-a>')) - self.assertEqual(self.dialog.showerror.message, '') - - def test_bind_not_ok(self): - self.assertFalse(self.dialog.bind_ok('<Control-Shift>')) - self.assertIn('not accepted', self.dialog.showerror.message) - - -class ToggleLevelTest(unittest.TestCase): - "Test toggle between Basic and Advanced frames." - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - cls.dialog = gkd(cls.root, 'Title', '<<Test>>', [], _utest=True) - - @classmethod - def tearDownClass(cls): - cls.dialog.cancel() - cls.root.update_idletasks() - cls.root.destroy() - del cls.dialog, cls.root - - def test_toggle_level(self): - dialog = self.dialog - - def stackorder(): - """Get the stack order of the children of the frame. - - winfo_children() stores the children in stack order, so - this can be used to check whether a frame is above or - below another one. - """ - for index, child in enumerate(dialog.frame.winfo_children()): - if child._name == 'keyseq_basic': - basic = index - if child._name == 'keyseq_advanced': - advanced = index - return basic, advanced - - # New window starts at basic level. - self.assertFalse(dialog.advanced) - self.assertIn('Advanced', dialog.button_level['text']) - basic, advanced = stackorder() - self.assertGreater(basic, advanced) - - # Toggle to advanced. - dialog.toggle_level() - self.assertTrue(dialog.advanced) - self.assertIn('Basic', dialog.button_level['text']) - basic, advanced = stackorder() - self.assertGreater(advanced, basic) - - # Toggle to basic. - dialog.button_level.invoke() - self.assertFalse(dialog.advanced) - self.assertIn('Advanced', dialog.button_level['text']) - basic, advanced = stackorder() - self.assertGreater(basic, advanced) - - -class KeySelectionTest(unittest.TestCase): - "Test selecting key on Basic frames." - - class Basic(gkd): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - class list_keys_final: - get = Func() - select_clear = Func() - yview = Func() - self.list_keys_final = list_keys_final - def set_modifiers_for_platform(self): - self.modifiers = ['foo', 'bar', 'BAZ'] - self.modifier_label = {'BAZ': 'ZZZ'} - showerror = Mbox_func() - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - cls.dialog = cls.Basic(cls.root, 'Title', '<<Test>>', [], _utest=True) - - @classmethod - def tearDownClass(cls): - cls.dialog.cancel() - cls.root.update_idletasks() - cls.root.destroy() - del cls.dialog, cls.root - - def setUp(self): - self.dialog.clear_key_seq() - - def test_get_modifiers(self): - dialog = self.dialog - gm = dialog.get_modifiers - eq = self.assertEqual - - # Modifiers are set on/off by invoking the checkbutton. - dialog.modifier_checkbuttons['foo'].invoke() - eq(gm(), ['foo']) - - dialog.modifier_checkbuttons['BAZ'].invoke() - eq(gm(), ['foo', 'BAZ']) - - dialog.modifier_checkbuttons['foo'].invoke() - eq(gm(), ['BAZ']) - - @mock.patch.object(gkd, 'get_modifiers') - def test_build_key_string(self, mock_modifiers): - dialog = self.dialog - key = dialog.list_keys_final - string = dialog.key_string.get - eq = self.assertEqual - - key.get.result = 'a' - mock_modifiers.return_value = [] - dialog.build_key_string() - eq(string(), '<Key-a>') - - mock_modifiers.return_value = ['mymod'] - dialog.build_key_string() - eq(string(), '<mymod-Key-a>') - - key.get.result = '' - mock_modifiers.return_value = ['mymod', 'test'] - dialog.build_key_string() - eq(string(), '<mymod-test>') - - @mock.patch.object(gkd, 'get_modifiers') - def test_final_key_selected(self, mock_modifiers): - dialog = self.dialog - key = dialog.list_keys_final - string = dialog.key_string.get - eq = self.assertEqual - - mock_modifiers.return_value = ['Shift'] - key.get.result = '{' - dialog.final_key_selected() - eq(string(), '<Shift-Key-braceleft>') - - -class CancelTest(unittest.TestCase): - "Simulate user clicking [Cancel] button." - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - cls.dialog = gkd(cls.root, 'Title', '<<Test>>', [], _utest=True) - - @classmethod - def tearDownClass(cls): - cls.dialog.cancel() - cls.root.update_idletasks() - cls.root.destroy() - del cls.dialog, cls.root - - def test_cancel(self): - self.assertEqual(self.dialog.winfo_class(), 'Toplevel') - self.dialog.button_cancel.invoke() - with self.assertRaises(TclError): - self.dialog.winfo_class() - self.assertEqual(self.dialog.result, '') - - -class HelperTest(unittest.TestCase): - "Test module level helper functions." - - def test_translate_key(self): - tr = config_key.translate_key - eq = self.assertEqual - - # Letters return unchanged with no 'Shift'. - eq(tr('q', []), 'Key-q') - eq(tr('q', ['Control', 'Alt']), 'Key-q') - - # 'Shift' uppercases single lowercase letters. - eq(tr('q', ['Shift']), 'Key-Q') - eq(tr('q', ['Control', 'Shift']), 'Key-Q') - eq(tr('q', ['Control', 'Alt', 'Shift']), 'Key-Q') - - # Convert key name to keysym. - eq(tr('Page Up', []), 'Key-Prior') - # 'Shift' doesn't change case when it's not a single char. - eq(tr('*', ['Shift']), 'Key-asterisk') - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_config_name.py b/Lib/idlelib/idle_test/test_config_name.py new file mode 100644 index 0000000..2a4df6a --- /dev/null +++ b/Lib/idlelib/idle_test/test_config_name.py @@ -0,0 +1,77 @@ +"""Unit tests for idlelib.configSectionNameDialog""" +import unittest +from idlelib.idle_test.mock_tk import Var, Mbox +from idlelib import configSectionNameDialog as name_dialog_module + +name_dialog = name_dialog_module.GetCfgSectionNameDialog + +class Dummy_name_dialog(object): + # Mock for testing the following methods of name_dialog + name_ok = name_dialog.name_ok.im_func + Ok = name_dialog.Ok.im_func + Cancel = name_dialog.Cancel.im_func + # Attributes, constant or variable, needed for tests + used_names = ['used'] + name = Var() + result = None + destroyed = False + def grab_release(self): + pass + def destroy(self): + self.destroyed = True + +# name_ok calls Mbox.showerror if name is not ok +orig_mbox = name_dialog_module.tkMessageBox +showerror = Mbox.showerror + +class ConfigNameTest(unittest.TestCase): + dialog = Dummy_name_dialog() + + @classmethod + def setUpClass(cls): + name_dialog_module.tkMessageBox = Mbox + + @classmethod + def tearDownClass(cls): + name_dialog_module.tkMessageBox = orig_mbox + + def test_blank_name(self): + self.dialog.name.set(' ') + self.assertEqual(self.dialog.name_ok(), '') + self.assertEqual(showerror.title, 'Name Error') + self.assertIn('No', showerror.message) + + def test_used_name(self): + self.dialog.name.set('used') + self.assertEqual(self.dialog.name_ok(), '') + self.assertEqual(showerror.title, 'Name Error') + self.assertIn('use', showerror.message) + + def test_long_name(self): + self.dialog.name.set('good'*8) + self.assertEqual(self.dialog.name_ok(), '') + self.assertEqual(showerror.title, 'Name Error') + self.assertIn('too long', showerror.message) + + def test_good_name(self): + self.dialog.name.set(' good ') + showerror.title = 'No Error' # should not be called + self.assertEqual(self.dialog.name_ok(), 'good') + self.assertEqual(showerror.title, 'No Error') + + def test_ok(self): + self.dialog.destroyed = False + self.dialog.name.set('good') + self.dialog.Ok() + self.assertEqual(self.dialog.result, 'good') + self.assertTrue(self.dialog.destroyed) + + def test_cancel(self): + self.dialog.destroyed = False + self.dialog.Cancel() + self.assertEqual(self.dialog.result, '') + self.assertTrue(self.dialog.destroyed) + + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=False) diff --git a/Lib/idlelib/idle_test/test_configdialog.py b/Lib/idlelib/idle_test/test_configdialog.py index 1f14ed1..ba65100 100644 --- a/Lib/idlelib/idle_test/test_configdialog.py +++ b/Lib/idlelib/idle_test/test_configdialog.py @@ -1,1416 +1,32 @@ -"""Test configdialog, coverage 94%. +'''Unittests for idlelib/configHandler.py -Half the class creates dialog, half works with user customizations. -""" -from idlelib import configdialog -from test.support import requires -requires('gui') -import unittest -from unittest import mock -from idlelib.idle_test.mock_idle import Func -from tkinter import Tk, StringVar, IntVar, BooleanVar, DISABLED, NORMAL -from idlelib import config -from idlelib.configdialog import idleConf, changes, tracers - -# Tests should not depend on fortuitous user configurations. -# They must not affect actual user .cfg files. -# Use solution from test_config: empty parsers with no filename. -usercfg = idleConf.userCfg -testcfg = { - 'main': config.IdleUserConfParser(''), - 'highlight': config.IdleUserConfParser(''), - 'keys': config.IdleUserConfParser(''), - 'extensions': config.IdleUserConfParser(''), -} - -root = None -dialog = None -mainpage = changes['main'] -highpage = changes['highlight'] -keyspage = changes['keys'] -extpage = changes['extensions'] - -def setUpModule(): - global root, dialog - idleConf.userCfg = testcfg - root = Tk() - # root.withdraw() # Comment out, see issue 30870 - dialog = configdialog.ConfigDialog(root, 'Test', _utest=True) - -def tearDownModule(): - global root, dialog - idleConf.userCfg = usercfg - tracers.detach() - tracers.clear() - changes.clear() - root.update_idletasks() - root.destroy() - root = dialog = None - - -class FontPageTest(unittest.TestCase): - """Test that font widgets enable users to make font changes. - - Test that widget actions set vars, that var changes add three - options to changes and call set_samples, and that set_samples - changes the font of both sample boxes. - """ - @classmethod - def setUpClass(cls): - page = cls.page = dialog.fontpage - dialog.note.select(page) - page.set_samples = Func() # Mask instance method. - page.update() - - @classmethod - def tearDownClass(cls): - del cls.page.set_samples # Unmask instance method. - - def setUp(self): - changes.clear() - - def test_load_font_cfg(self): - # Leave widget load test to human visual check. - # TODO Improve checks when add IdleConf.get_font_values. - tracers.detach() - d = self.page - d.font_name.set('Fake') - d.font_size.set('1') - d.font_bold.set(True) - d.set_samples.called = 0 - d.load_font_cfg() - self.assertNotEqual(d.font_name.get(), 'Fake') - self.assertNotEqual(d.font_size.get(), '1') - self.assertFalse(d.font_bold.get()) - self.assertEqual(d.set_samples.called, 1) - tracers.attach() - - def test_fontlist_key(self): - # Up and Down keys should select a new font. - d = self.page - if d.fontlist.size() < 2: - self.skipTest('need at least 2 fonts') - fontlist = d.fontlist - fontlist.activate(0) - font = d.fontlist.get('active') - - # Test Down key. - fontlist.focus_force() - fontlist.update() - fontlist.event_generate('<Key-Down>') - fontlist.event_generate('<KeyRelease-Down>') - - down_font = fontlist.get('active') - self.assertNotEqual(down_font, font) - self.assertIn(d.font_name.get(), down_font.lower()) - - # Test Up key. - fontlist.focus_force() - fontlist.update() - fontlist.event_generate('<Key-Up>') - fontlist.event_generate('<KeyRelease-Up>') - - up_font = fontlist.get('active') - self.assertEqual(up_font, font) - self.assertIn(d.font_name.get(), up_font.lower()) - - def test_fontlist_mouse(self): - # Click on item should select that item. - d = self.page - if d.fontlist.size() < 2: - self.skipTest('need at least 2 fonts') - fontlist = d.fontlist - fontlist.activate(0) - - # Select next item in listbox - fontlist.focus_force() - fontlist.see(1) - fontlist.update() - x, y, dx, dy = fontlist.bbox(1) - x += dx // 2 - y += dy // 2 - fontlist.event_generate('<Button-1>', x=x, y=y) - fontlist.event_generate('<ButtonRelease-1>', x=x, y=y) - - font1 = fontlist.get(1) - select_font = fontlist.get('anchor') - self.assertEqual(select_font, font1) - self.assertIn(d.font_name.get(), font1.lower()) - - def test_sizelist(self): - # Click on number should select that number - d = self.page - d.sizelist.variable.set(40) - self.assertEqual(d.font_size.get(), '40') - - def test_bold_toggle(self): - # Click on checkbutton should invert it. - d = self.page - d.font_bold.set(False) - d.bold_toggle.invoke() - self.assertTrue(d.font_bold.get()) - d.bold_toggle.invoke() - self.assertFalse(d.font_bold.get()) - - def test_font_set(self): - # Test that setting a font Variable results in 3 provisional - # change entries and a call to set_samples. Use values sure to - # not be defaults. - - default_font = idleConf.GetFont(root, 'main', 'EditorWindow') - default_size = str(default_font[1]) - default_bold = default_font[2] == 'bold' - d = self.page - d.font_size.set(default_size) - d.font_bold.set(default_bold) - d.set_samples.called = 0 - - d.font_name.set('Test Font') - expected = {'EditorWindow': {'font': 'Test Font', - 'font-size': default_size, - 'font-bold': str(default_bold)}} - self.assertEqual(mainpage, expected) - self.assertEqual(d.set_samples.called, 1) - changes.clear() - - d.font_size.set('20') - expected = {'EditorWindow': {'font': 'Test Font', - 'font-size': '20', - 'font-bold': str(default_bold)}} - self.assertEqual(mainpage, expected) - self.assertEqual(d.set_samples.called, 2) - changes.clear() - - d.font_bold.set(not default_bold) - expected = {'EditorWindow': {'font': 'Test Font', - 'font-size': '20', - 'font-bold': str(not default_bold)}} - self.assertEqual(mainpage, expected) - self.assertEqual(d.set_samples.called, 3) - - def test_set_samples(self): - d = self.page - del d.set_samples # Unmask method for test - orig_samples = d.font_sample, d.highlight_sample - d.font_sample, d.highlight_sample = {}, {} - d.font_name.set('test') - d.font_size.set('5') - d.font_bold.set(1) - expected = {'font': ('test', '5', 'bold')} - - # Test set_samples. - d.set_samples() - self.assertTrue(d.font_sample == d.highlight_sample == expected) - - d.font_sample, d.highlight_sample = orig_samples - d.set_samples = Func() # Re-mask for other tests. - - -class IndentTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.page = dialog.fontpage - cls.page.update() - - def test_load_tab_cfg(self): - d = self.page - d.space_num.set(16) - d.load_tab_cfg() - self.assertEqual(d.space_num.get(), 4) - - def test_indent_scale(self): - d = self.page - changes.clear() - d.indent_scale.set(20) - self.assertEqual(d.space_num.get(), 16) - self.assertEqual(mainpage, {'Indent': {'num-spaces': '16'}}) - - -class HighPageTest(unittest.TestCase): - """Test that highlight tab widgets enable users to make changes. - - Test that widget actions set vars, that var changes add - options to changes and that themes work correctly. - """ - - @classmethod - def setUpClass(cls): - page = cls.page = dialog.highpage - dialog.note.select(page) - page.set_theme_type = Func() - page.paint_theme_sample = Func() - page.set_highlight_target = Func() - page.set_color_sample = Func() - page.update() - - @classmethod - def tearDownClass(cls): - d = cls.page - del d.set_theme_type, d.paint_theme_sample - del d.set_highlight_target, d.set_color_sample - - def setUp(self): - d = self.page - # The following is needed for test_load_key_cfg, _delete_custom_keys. - # This may indicate a defect in some test or function. - for section in idleConf.GetSectionList('user', 'highlight'): - idleConf.userCfg['highlight'].remove_section(section) - changes.clear() - d.set_theme_type.called = 0 - d.paint_theme_sample.called = 0 - d.set_highlight_target.called = 0 - d.set_color_sample.called = 0 - - def test_load_theme_cfg(self): - tracers.detach() - d = self.page - eq = self.assertEqual - - # Use builtin theme with no user themes created. - idleConf.CurrentTheme = mock.Mock(return_value='IDLE Classic') - d.load_theme_cfg() - self.assertTrue(d.theme_source.get()) - # builtinlist sets variable builtin_name to the CurrentTheme default. - eq(d.builtin_name.get(), 'IDLE Classic') - eq(d.custom_name.get(), '- no custom themes -') - eq(d.custom_theme_on.state(), ('disabled',)) - eq(d.set_theme_type.called, 1) - eq(d.paint_theme_sample.called, 1) - eq(d.set_highlight_target.called, 1) - - # Builtin theme with non-empty user theme list. - idleConf.SetOption('highlight', 'test1', 'option', 'value') - idleConf.SetOption('highlight', 'test2', 'option2', 'value2') - d.load_theme_cfg() - eq(d.builtin_name.get(), 'IDLE Classic') - eq(d.custom_name.get(), 'test1') - eq(d.set_theme_type.called, 2) - eq(d.paint_theme_sample.called, 2) - eq(d.set_highlight_target.called, 2) - - # Use custom theme. - idleConf.CurrentTheme = mock.Mock(return_value='test2') - idleConf.SetOption('main', 'Theme', 'default', '0') - d.load_theme_cfg() - self.assertFalse(d.theme_source.get()) - eq(d.builtin_name.get(), 'IDLE Classic') - eq(d.custom_name.get(), 'test2') - eq(d.set_theme_type.called, 3) - eq(d.paint_theme_sample.called, 3) - eq(d.set_highlight_target.called, 3) - - del idleConf.CurrentTheme - tracers.attach() - - def test_theme_source(self): - eq = self.assertEqual - d = self.page - # Test these separately. - d.var_changed_builtin_name = Func() - d.var_changed_custom_name = Func() - # Builtin selected. - d.builtin_theme_on.invoke() - eq(mainpage, {'Theme': {'default': 'True'}}) - eq(d.var_changed_builtin_name.called, 1) - eq(d.var_changed_custom_name.called, 0) - changes.clear() - - # Custom selected. - d.custom_theme_on.state(('!disabled',)) - d.custom_theme_on.invoke() - self.assertEqual(mainpage, {'Theme': {'default': 'False'}}) - eq(d.var_changed_builtin_name.called, 1) - eq(d.var_changed_custom_name.called, 1) - del d.var_changed_builtin_name, d.var_changed_custom_name - - def test_builtin_name(self): - eq = self.assertEqual - d = self.page - item_list = ['IDLE Classic', 'IDLE Dark', 'IDLE New'] - - # Not in old_themes, defaults name to first item. - idleConf.SetOption('main', 'Theme', 'name', 'spam') - d.builtinlist.SetMenu(item_list, 'IDLE Dark') - eq(mainpage, {'Theme': {'name': 'IDLE Classic', - 'name2': 'IDLE Dark'}}) - eq(d.theme_message['text'], 'New theme, see Help') - eq(d.paint_theme_sample.called, 1) - - # Not in old themes - uses name2. - changes.clear() - idleConf.SetOption('main', 'Theme', 'name', 'IDLE New') - d.builtinlist.SetMenu(item_list, 'IDLE Dark') - eq(mainpage, {'Theme': {'name2': 'IDLE Dark'}}) - eq(d.theme_message['text'], 'New theme, see Help') - eq(d.paint_theme_sample.called, 2) - - # Builtin name in old_themes. - changes.clear() - d.builtinlist.SetMenu(item_list, 'IDLE Classic') - eq(mainpage, {'Theme': {'name': 'IDLE Classic', 'name2': ''}}) - eq(d.theme_message['text'], '') - eq(d.paint_theme_sample.called, 3) - - def test_custom_name(self): - d = self.page - - # If no selections, doesn't get added. - d.customlist.SetMenu([], '- no custom themes -') - self.assertNotIn('Theme', mainpage) - self.assertEqual(d.paint_theme_sample.called, 0) - - # Custom name selected. - changes.clear() - d.customlist.SetMenu(['a', 'b', 'c'], 'c') - self.assertEqual(mainpage, {'Theme': {'name': 'c'}}) - self.assertEqual(d.paint_theme_sample.called, 1) - - def test_color(self): - d = self.page - d.on_new_color_set = Func() - # self.color is only set in get_color through ColorChooser. - d.color.set('green') - self.assertEqual(d.on_new_color_set.called, 1) - del d.on_new_color_set - - def test_highlight_target_list_mouse(self): - # Set highlight_target through targetlist. - eq = self.assertEqual - d = self.page - - d.targetlist.SetMenu(['a', 'b', 'c'], 'c') - eq(d.highlight_target.get(), 'c') - eq(d.set_highlight_target.called, 1) - - def test_highlight_target_text_mouse(self): - # Set highlight_target through clicking highlight_sample. - eq = self.assertEqual - d = self.page - - elem = {} - count = 0 - hs = d.highlight_sample - hs.focus_force() - hs.see(1.0) - hs.update_idletasks() - - def tag_to_element(elem): - for element, tag in d.theme_elements.items(): - elem[tag[0]] = element - - def click_it(start): - x, y, dx, dy = hs.bbox(start) - x += dx // 2 - y += dy // 2 - hs.event_generate('<Enter>', x=0, y=0) - hs.event_generate('<Motion>', x=x, y=y) - hs.event_generate('<ButtonPress-1>', x=x, y=y) - hs.event_generate('<ButtonRelease-1>', x=x, y=y) - - # Flip theme_elements to make the tag the key. - tag_to_element(elem) - - # If highlight_sample has a tag that isn't in theme_elements, there - # will be a KeyError in the test run. - for tag in hs.tag_names(): - for start_index in hs.tag_ranges(tag)[0::2]: - count += 1 - click_it(start_index) - eq(d.highlight_target.get(), elem[tag]) - eq(d.set_highlight_target.called, count) - - def test_set_theme_type(self): - eq = self.assertEqual - d = self.page - del d.set_theme_type - - # Builtin theme selected. - d.theme_source.set(True) - d.set_theme_type() - eq(d.builtinlist['state'], NORMAL) - eq(d.customlist['state'], DISABLED) - eq(d.button_delete_custom.state(), ('disabled',)) - - # Custom theme selected. - d.theme_source.set(False) - d.set_theme_type() - eq(d.builtinlist['state'], DISABLED) - eq(d.custom_theme_on.state(), ('selected',)) - eq(d.customlist['state'], NORMAL) - eq(d.button_delete_custom.state(), ()) - d.set_theme_type = Func() - - def test_get_color(self): - eq = self.assertEqual - d = self.page - orig_chooser = configdialog.tkColorChooser.askcolor - chooser = configdialog.tkColorChooser.askcolor = Func() - gntn = d.get_new_theme_name = Func() - - d.highlight_target.set('Editor Breakpoint') - d.color.set('#ffffff') - - # Nothing selected. - chooser.result = (None, None) - d.button_set_color.invoke() - eq(d.color.get(), '#ffffff') - - # Selection same as previous color. - chooser.result = ('', d.style.lookup(d.frame_color_set['style'], 'background')) - d.button_set_color.invoke() - eq(d.color.get(), '#ffffff') - - # Select different color. - chooser.result = ((222.8671875, 0.0, 0.0), '#de0000') - - # Default theme. - d.color.set('#ffffff') - d.theme_source.set(True) - - # No theme name selected therefore color not saved. - gntn.result = '' - d.button_set_color.invoke() - eq(gntn.called, 1) - eq(d.color.get(), '#ffffff') - # Theme name selected. - gntn.result = 'My New Theme' - d.button_set_color.invoke() - eq(d.custom_name.get(), gntn.result) - eq(d.color.get(), '#de0000') - - # Custom theme. - d.color.set('#ffffff') - d.theme_source.set(False) - d.button_set_color.invoke() - eq(d.color.get(), '#de0000') - - del d.get_new_theme_name - configdialog.tkColorChooser.askcolor = orig_chooser - - def test_on_new_color_set(self): - d = self.page - color = '#3f7cae' - d.custom_name.set('Python') - d.highlight_target.set('Selected Text') - d.fg_bg_toggle.set(True) - - d.color.set(color) - self.assertEqual(d.style.lookup(d.frame_color_set['style'], 'background'), color) - self.assertEqual(d.highlight_sample.tag_cget('hilite', 'foreground'), color) - self.assertEqual(highpage, - {'Python': {'hilite-foreground': color}}) - - def test_get_new_theme_name(self): - orig_sectionname = configdialog.SectionName - sn = configdialog.SectionName = Func(return_self=True) - d = self.page - - sn.result = 'New Theme' - self.assertEqual(d.get_new_theme_name(''), 'New Theme') - - configdialog.SectionName = orig_sectionname - - def test_save_as_new_theme(self): - d = self.page - gntn = d.get_new_theme_name = Func() - d.theme_source.set(True) - - # No name entered. - gntn.result = '' - d.button_save_custom.invoke() - self.assertNotIn(gntn.result, idleConf.userCfg['highlight']) - - # Name entered. - gntn.result = 'my new theme' - gntn.called = 0 - self.assertNotIn(gntn.result, idleConf.userCfg['highlight']) - d.button_save_custom.invoke() - self.assertIn(gntn.result, idleConf.userCfg['highlight']) - - del d.get_new_theme_name - - def test_create_new_and_save_new(self): - eq = self.assertEqual - d = self.page - - # Use default as previously active theme. - d.theme_source.set(True) - d.builtin_name.set('IDLE Classic') - first_new = 'my new custom theme' - second_new = 'my second custom theme' - - # No changes, so themes are an exact copy. - self.assertNotIn(first_new, idleConf.userCfg) - d.create_new(first_new) - eq(idleConf.GetSectionList('user', 'highlight'), [first_new]) - eq(idleConf.GetThemeDict('default', 'IDLE Classic'), - idleConf.GetThemeDict('user', first_new)) - eq(d.custom_name.get(), first_new) - self.assertFalse(d.theme_source.get()) # Use custom set. - eq(d.set_theme_type.called, 1) - - # Test that changed targets are in new theme. - changes.add_option('highlight', first_new, 'hit-background', 'yellow') - self.assertNotIn(second_new, idleConf.userCfg) - d.create_new(second_new) - eq(idleConf.GetSectionList('user', 'highlight'), [first_new, second_new]) - self.assertNotEqual(idleConf.GetThemeDict('user', first_new), - idleConf.GetThemeDict('user', second_new)) - # Check that difference in themes was in `hit-background` from `changes`. - idleConf.SetOption('highlight', first_new, 'hit-background', 'yellow') - eq(idleConf.GetThemeDict('user', first_new), - idleConf.GetThemeDict('user', second_new)) - - def test_set_highlight_target(self): - eq = self.assertEqual - d = self.page - del d.set_highlight_target - - # Target is cursor. - d.highlight_target.set('Cursor') - eq(d.fg_on.state(), ('disabled', 'selected')) - eq(d.bg_on.state(), ('disabled',)) - self.assertTrue(d.fg_bg_toggle) - eq(d.set_color_sample.called, 1) - - # Target is not cursor. - d.highlight_target.set('Comment') - eq(d.fg_on.state(), ('selected',)) - eq(d.bg_on.state(), ()) - self.assertTrue(d.fg_bg_toggle) - eq(d.set_color_sample.called, 2) - - d.set_highlight_target = Func() - - def test_set_color_sample_binding(self): - d = self.page - scs = d.set_color_sample - - d.fg_on.invoke() - self.assertEqual(scs.called, 1) - - d.bg_on.invoke() - self.assertEqual(scs.called, 2) - - def test_set_color_sample(self): - d = self.page - del d.set_color_sample - d.highlight_target.set('Selected Text') - d.fg_bg_toggle.set(True) - d.set_color_sample() - self.assertEqual( - d.style.lookup(d.frame_color_set['style'], 'background'), - d.highlight_sample.tag_cget('hilite', 'foreground')) - d.set_color_sample = Func() - - def test_paint_theme_sample(self): - eq = self.assertEqual - page = self.page - del page.paint_theme_sample # Delete masking mock. - hs_tag = page.highlight_sample.tag_cget - gh = idleConf.GetHighlight - - # Create custom theme based on IDLE Dark. - page.theme_source.set(True) - page.builtin_name.set('IDLE Dark') - theme = 'IDLE Test' - page.create_new(theme) - page.set_color_sample.called = 0 - - # Base theme with nothing in `changes`. - page.paint_theme_sample() - new_console = {'foreground': 'blue', - 'background': 'yellow',} - for key, value in new_console.items(): - self.assertNotEqual(hs_tag('console', key), value) - eq(page.set_color_sample.called, 1) - - # Apply changes. - for key, value in new_console.items(): - changes.add_option('highlight', theme, 'console-'+key, value) - page.paint_theme_sample() - for key, value in new_console.items(): - eq(hs_tag('console', key), value) - eq(page.set_color_sample.called, 2) - - page.paint_theme_sample = Func() - - def test_delete_custom(self): - eq = self.assertEqual - d = self.page - d.button_delete_custom.state(('!disabled',)) - yesno = d.askyesno = Func() - dialog.deactivate_current_config = Func() - dialog.activate_config_changes = Func() - - theme_name = 'spam theme' - idleConf.userCfg['highlight'].SetOption(theme_name, 'name', 'value') - highpage[theme_name] = {'option': 'True'} - - # Force custom theme. - d.theme_source.set(False) - d.custom_name.set(theme_name) +Coverage: 46% just by creating dialog. The other half is change code. - # Cancel deletion. - yesno.result = False - d.button_delete_custom.invoke() - eq(yesno.called, 1) - eq(highpage[theme_name], {'option': 'True'}) - eq(idleConf.GetSectionList('user', 'highlight'), ['spam theme']) - eq(dialog.deactivate_current_config.called, 0) - eq(dialog.activate_config_changes.called, 0) - eq(d.set_theme_type.called, 0) - - # Confirm deletion. - yesno.result = True - d.button_delete_custom.invoke() - eq(yesno.called, 2) - self.assertNotIn(theme_name, highpage) - eq(idleConf.GetSectionList('user', 'highlight'), []) - eq(d.custom_theme_on.state(), ('disabled',)) - eq(d.custom_name.get(), '- no custom themes -') - eq(dialog.deactivate_current_config.called, 1) - eq(dialog.activate_config_changes.called, 1) - eq(d.set_theme_type.called, 1) - - del dialog.activate_config_changes, dialog.deactivate_current_config - del d.askyesno - - -class KeysPageTest(unittest.TestCase): - """Test that keys tab widgets enable users to make changes. - - Test that widget actions set vars, that var changes add - options to changes and that key sets works correctly. - """ - - @classmethod - def setUpClass(cls): - page = cls.page = dialog.keyspage - dialog.note.select(page) - page.set_keys_type = Func() - page.load_keys_list = Func() - - @classmethod - def tearDownClass(cls): - page = cls.page - del page.set_keys_type, page.load_keys_list - - def setUp(self): - d = self.page - # The following is needed for test_load_key_cfg, _delete_custom_keys. - # This may indicate a defect in some test or function. - for section in idleConf.GetSectionList('user', 'keys'): - idleConf.userCfg['keys'].remove_section(section) - changes.clear() - d.set_keys_type.called = 0 - d.load_keys_list.called = 0 - - def test_load_key_cfg(self): - tracers.detach() - d = self.page - eq = self.assertEqual - - # Use builtin keyset with no user keysets created. - idleConf.CurrentKeys = mock.Mock(return_value='IDLE Classic OSX') - d.load_key_cfg() - self.assertTrue(d.keyset_source.get()) - # builtinlist sets variable builtin_name to the CurrentKeys default. - eq(d.builtin_name.get(), 'IDLE Classic OSX') - eq(d.custom_name.get(), '- no custom keys -') - eq(d.custom_keyset_on.state(), ('disabled',)) - eq(d.set_keys_type.called, 1) - eq(d.load_keys_list.called, 1) - eq(d.load_keys_list.args, ('IDLE Classic OSX', )) - - # Builtin keyset with non-empty user keyset list. - idleConf.SetOption('keys', 'test1', 'option', 'value') - idleConf.SetOption('keys', 'test2', 'option2', 'value2') - d.load_key_cfg() - eq(d.builtin_name.get(), 'IDLE Classic OSX') - eq(d.custom_name.get(), 'test1') - eq(d.set_keys_type.called, 2) - eq(d.load_keys_list.called, 2) - eq(d.load_keys_list.args, ('IDLE Classic OSX', )) - - # Use custom keyset. - idleConf.CurrentKeys = mock.Mock(return_value='test2') - idleConf.default_keys = mock.Mock(return_value='IDLE Modern Unix') - idleConf.SetOption('main', 'Keys', 'default', '0') - d.load_key_cfg() - self.assertFalse(d.keyset_source.get()) - eq(d.builtin_name.get(), 'IDLE Modern Unix') - eq(d.custom_name.get(), 'test2') - eq(d.set_keys_type.called, 3) - eq(d.load_keys_list.called, 3) - eq(d.load_keys_list.args, ('test2', )) - - del idleConf.CurrentKeys, idleConf.default_keys - tracers.attach() - - def test_keyset_source(self): - eq = self.assertEqual - d = self.page - # Test these separately. - d.var_changed_builtin_name = Func() - d.var_changed_custom_name = Func() - # Builtin selected. - d.builtin_keyset_on.invoke() - eq(mainpage, {'Keys': {'default': 'True'}}) - eq(d.var_changed_builtin_name.called, 1) - eq(d.var_changed_custom_name.called, 0) - changes.clear() - - # Custom selected. - d.custom_keyset_on.state(('!disabled',)) - d.custom_keyset_on.invoke() - self.assertEqual(mainpage, {'Keys': {'default': 'False'}}) - eq(d.var_changed_builtin_name.called, 1) - eq(d.var_changed_custom_name.called, 1) - del d.var_changed_builtin_name, d.var_changed_custom_name - - def test_builtin_name(self): - eq = self.assertEqual - d = self.page - idleConf.userCfg['main'].remove_section('Keys') - item_list = ['IDLE Classic Windows', 'IDLE Classic OSX', - 'IDLE Modern UNIX'] - - # Not in old_keys, defaults name to first item. - d.builtinlist.SetMenu(item_list, 'IDLE Modern UNIX') - eq(mainpage, {'Keys': {'name': 'IDLE Classic Windows', - 'name2': 'IDLE Modern UNIX'}}) - eq(d.keys_message['text'], 'New key set, see Help') - eq(d.load_keys_list.called, 1) - eq(d.load_keys_list.args, ('IDLE Modern UNIX', )) - - # Not in old keys - uses name2. - changes.clear() - idleConf.SetOption('main', 'Keys', 'name', 'IDLE Classic Unix') - d.builtinlist.SetMenu(item_list, 'IDLE Modern UNIX') - eq(mainpage, {'Keys': {'name2': 'IDLE Modern UNIX'}}) - eq(d.keys_message['text'], 'New key set, see Help') - eq(d.load_keys_list.called, 2) - eq(d.load_keys_list.args, ('IDLE Modern UNIX', )) - - # Builtin name in old_keys. - changes.clear() - d.builtinlist.SetMenu(item_list, 'IDLE Classic OSX') - eq(mainpage, {'Keys': {'name': 'IDLE Classic OSX', 'name2': ''}}) - eq(d.keys_message['text'], '') - eq(d.load_keys_list.called, 3) - eq(d.load_keys_list.args, ('IDLE Classic OSX', )) - - def test_custom_name(self): - d = self.page - - # If no selections, doesn't get added. - d.customlist.SetMenu([], '- no custom keys -') - self.assertNotIn('Keys', mainpage) - self.assertEqual(d.load_keys_list.called, 0) - - # Custom name selected. - changes.clear() - d.customlist.SetMenu(['a', 'b', 'c'], 'c') - self.assertEqual(mainpage, {'Keys': {'name': 'c'}}) - self.assertEqual(d.load_keys_list.called, 1) - - def test_keybinding(self): - idleConf.SetOption('extensions', 'ZzDummy', 'enable', 'True') - d = self.page - d.custom_name.set('my custom keys') - d.bindingslist.delete(0, 'end') - d.bindingslist.insert(0, 'copy') - d.bindingslist.insert(1, 'z-in') - d.bindingslist.selection_set(0) - d.bindingslist.selection_anchor(0) - # Core binding - adds to keys. - d.keybinding.set('<Key-F11>') - self.assertEqual(keyspage, - {'my custom keys': {'copy': '<Key-F11>'}}) - - # Not a core binding - adds to extensions. - d.bindingslist.selection_set(1) - d.bindingslist.selection_anchor(1) - d.keybinding.set('<Key-F11>') - self.assertEqual(extpage, - {'ZzDummy_cfgBindings': {'z-in': '<Key-F11>'}}) - - def test_set_keys_type(self): - eq = self.assertEqual - d = self.page - del d.set_keys_type - - # Builtin keyset selected. - d.keyset_source.set(True) - d.set_keys_type() - eq(d.builtinlist['state'], NORMAL) - eq(d.customlist['state'], DISABLED) - eq(d.button_delete_custom_keys.state(), ('disabled',)) - - # Custom keyset selected. - d.keyset_source.set(False) - d.set_keys_type() - eq(d.builtinlist['state'], DISABLED) - eq(d.custom_keyset_on.state(), ('selected',)) - eq(d.customlist['state'], NORMAL) - eq(d.button_delete_custom_keys.state(), ()) - d.set_keys_type = Func() - - def test_get_new_keys(self): - eq = self.assertEqual - d = self.page - orig_getkeysdialog = configdialog.GetKeysDialog - gkd = configdialog.GetKeysDialog = Func(return_self=True) - gnkn = d.get_new_keys_name = Func() - - d.button_new_keys.state(('!disabled',)) - d.bindingslist.delete(0, 'end') - d.bindingslist.insert(0, 'copy - <Control-Shift-Key-C>') - d.bindingslist.selection_set(0) - d.bindingslist.selection_anchor(0) - d.keybinding.set('Key-a') - d.keyset_source.set(True) # Default keyset. - - # Default keyset; no change to binding. - gkd.result = '' - d.button_new_keys.invoke() - eq(d.bindingslist.get('anchor'), 'copy - <Control-Shift-Key-C>') - # Keybinding isn't changed when there isn't a change entered. - eq(d.keybinding.get(), 'Key-a') - - # Default keyset; binding changed. - gkd.result = '<Key-F11>' - # No keyset name selected therefore binding not saved. - gnkn.result = '' - d.button_new_keys.invoke() - eq(gnkn.called, 1) - eq(d.bindingslist.get('anchor'), 'copy - <Control-Shift-Key-C>') - # Keyset name selected. - gnkn.result = 'My New Key Set' - d.button_new_keys.invoke() - eq(d.custom_name.get(), gnkn.result) - eq(d.bindingslist.get('anchor'), 'copy - <Key-F11>') - eq(d.keybinding.get(), '<Key-F11>') - - # User keyset; binding changed. - d.keyset_source.set(False) # Custom keyset. - gnkn.called = 0 - gkd.result = '<Key-p>' - d.button_new_keys.invoke() - eq(gnkn.called, 0) - eq(d.bindingslist.get('anchor'), 'copy - <Key-p>') - eq(d.keybinding.get(), '<Key-p>') - - del d.get_new_keys_name - configdialog.GetKeysDialog = orig_getkeysdialog - - def test_get_new_keys_name(self): - orig_sectionname = configdialog.SectionName - sn = configdialog.SectionName = Func(return_self=True) - d = self.page - - sn.result = 'New Keys' - self.assertEqual(d.get_new_keys_name(''), 'New Keys') - - configdialog.SectionName = orig_sectionname - - def test_save_as_new_key_set(self): - d = self.page - gnkn = d.get_new_keys_name = Func() - d.keyset_source.set(True) - - # No name entered. - gnkn.result = '' - d.button_save_custom_keys.invoke() - - # Name entered. - gnkn.result = 'my new key set' - gnkn.called = 0 - self.assertNotIn(gnkn.result, idleConf.userCfg['keys']) - d.button_save_custom_keys.invoke() - self.assertIn(gnkn.result, idleConf.userCfg['keys']) - - del d.get_new_keys_name - - def test_on_bindingslist_select(self): - d = self.page - b = d.bindingslist - b.delete(0, 'end') - b.insert(0, 'copy') - b.insert(1, 'find') - b.activate(0) - - b.focus_force() - b.see(1) - b.update() - x, y, dx, dy = b.bbox(1) - x += dx // 2 - y += dy // 2 - b.event_generate('<Enter>', x=0, y=0) - b.event_generate('<Motion>', x=x, y=y) - b.event_generate('<Button-1>', x=x, y=y) - b.event_generate('<ButtonRelease-1>', x=x, y=y) - self.assertEqual(b.get('anchor'), 'find') - self.assertEqual(d.button_new_keys.state(), ()) - - def test_create_new_key_set_and_save_new_key_set(self): - eq = self.assertEqual - d = self.page - - # Use default as previously active keyset. - d.keyset_source.set(True) - d.builtin_name.set('IDLE Classic Windows') - first_new = 'my new custom key set' - second_new = 'my second custom keyset' - - # No changes, so keysets are an exact copy. - self.assertNotIn(first_new, idleConf.userCfg) - d.create_new_key_set(first_new) - eq(idleConf.GetSectionList('user', 'keys'), [first_new]) - eq(idleConf.GetKeySet('IDLE Classic Windows'), - idleConf.GetKeySet(first_new)) - eq(d.custom_name.get(), first_new) - self.assertFalse(d.keyset_source.get()) # Use custom set. - eq(d.set_keys_type.called, 1) - - # Test that changed keybindings are in new keyset. - changes.add_option('keys', first_new, 'copy', '<Key-F11>') - self.assertNotIn(second_new, idleConf.userCfg) - d.create_new_key_set(second_new) - eq(idleConf.GetSectionList('user', 'keys'), [first_new, second_new]) - self.assertNotEqual(idleConf.GetKeySet(first_new), - idleConf.GetKeySet(second_new)) - # Check that difference in keysets was in option `copy` from `changes`. - idleConf.SetOption('keys', first_new, 'copy', '<Key-F11>') - eq(idleConf.GetKeySet(first_new), idleConf.GetKeySet(second_new)) - - def test_load_keys_list(self): - eq = self.assertEqual - d = self.page - gks = idleConf.GetKeySet = Func() - del d.load_keys_list - b = d.bindingslist - - b.delete(0, 'end') - b.insert(0, '<<find>>') - b.insert(1, '<<help>>') - gks.result = {'<<copy>>': ['<Control-Key-c>', '<Control-Key-C>'], - '<<force-open-completions>>': ['<Control-Key-space>'], - '<<spam>>': ['<Key-F11>']} - changes.add_option('keys', 'my keys', 'spam', '<Shift-Key-a>') - expected = ('copy - <Control-Key-c> <Control-Key-C>', - 'force-open-completions - <Control-Key-space>', - 'spam - <Shift-Key-a>') - - # No current selection. - d.load_keys_list('my keys') - eq(b.get(0, 'end'), expected) - eq(b.get('anchor'), '') - eq(b.curselection(), ()) - - # Check selection. - b.selection_set(1) - b.selection_anchor(1) - d.load_keys_list('my keys') - eq(b.get(0, 'end'), expected) - eq(b.get('anchor'), 'force-open-completions - <Control-Key-space>') - eq(b.curselection(), (1, )) - - # Change selection. - b.selection_set(2) - b.selection_anchor(2) - d.load_keys_list('my keys') - eq(b.get(0, 'end'), expected) - eq(b.get('anchor'), 'spam - <Shift-Key-a>') - eq(b.curselection(), (2, )) - d.load_keys_list = Func() - - del idleConf.GetKeySet - - def test_delete_custom_keys(self): - eq = self.assertEqual - d = self.page - d.button_delete_custom_keys.state(('!disabled',)) - yesno = d.askyesno = Func() - dialog.deactivate_current_config = Func() - dialog.activate_config_changes = Func() - - keyset_name = 'spam key set' - idleConf.userCfg['keys'].SetOption(keyset_name, 'name', 'value') - keyspage[keyset_name] = {'option': 'True'} - - # Force custom keyset. - d.keyset_source.set(False) - d.custom_name.set(keyset_name) - - # Cancel deletion. - yesno.result = False - d.button_delete_custom_keys.invoke() - eq(yesno.called, 1) - eq(keyspage[keyset_name], {'option': 'True'}) - eq(idleConf.GetSectionList('user', 'keys'), ['spam key set']) - eq(dialog.deactivate_current_config.called, 0) - eq(dialog.activate_config_changes.called, 0) - eq(d.set_keys_type.called, 0) - - # Confirm deletion. - yesno.result = True - d.button_delete_custom_keys.invoke() - eq(yesno.called, 2) - self.assertNotIn(keyset_name, keyspage) - eq(idleConf.GetSectionList('user', 'keys'), []) - eq(d.custom_keyset_on.state(), ('disabled',)) - eq(d.custom_name.get(), '- no custom keys -') - eq(dialog.deactivate_current_config.called, 1) - eq(dialog.activate_config_changes.called, 1) - eq(d.set_keys_type.called, 1) - - del dialog.activate_config_changes, dialog.deactivate_current_config - del d.askyesno - - -class GenPageTest(unittest.TestCase): - """Test that general tab widgets enable users to make changes. - - Test that widget actions set vars, that var changes add - options to changes and that helplist works correctly. - """ - @classmethod - def setUpClass(cls): - page = cls.page = dialog.genpage - dialog.note.select(page) - page.set = page.set_add_delete_state = Func() - page.upc = page.update_help_changes = Func() - page.update() - - @classmethod - def tearDownClass(cls): - page = cls.page - del page.set, page.set_add_delete_state - del page.upc, page.update_help_changes - page.helplist.delete(0, 'end') - page.user_helplist.clear() - - def setUp(self): - changes.clear() - - def test_load_general_cfg(self): - # Set to wrong values, load, check right values. - eq = self.assertEqual - d = self.page - d.startup_edit.set(1) - d.autosave.set(1) - d.win_width.set(1) - d.win_height.set(1) - d.helplist.insert('end', 'bad') - d.user_helplist = ['bad', 'worse'] - idleConf.SetOption('main', 'HelpFiles', '1', 'name;file') - d.load_general_cfg() - eq(d.startup_edit.get(), 0) - eq(d.autosave.get(), 0) - eq(d.win_width.get(), '80') - eq(d.win_height.get(), '40') - eq(d.helplist.get(0, 'end'), ('name',)) - eq(d.user_helplist, [('name', 'file', '1')]) - - def test_startup(self): - d = self.page - d.startup_editor_on.invoke() - self.assertEqual(mainpage, - {'General': {'editor-on-startup': '1'}}) - changes.clear() - d.startup_shell_on.invoke() - self.assertEqual(mainpage, - {'General': {'editor-on-startup': '0'}}) - - def test_editor_size(self): - d = self.page - d.win_height_int.delete(0, 'end') - d.win_height_int.insert(0, '11') - self.assertEqual(mainpage, {'EditorWindow': {'height': '11'}}) - changes.clear() - d.win_width_int.delete(0, 'end') - d.win_width_int.insert(0, '11') - self.assertEqual(mainpage, {'EditorWindow': {'width': '11'}}) - - def test_cursor_blink(self): - self.page.cursor_blink_bool.invoke() - self.assertEqual(mainpage, {'EditorWindow': {'cursor-blink': 'False'}}) - - def test_autocomplete_wait(self): - self.page.auto_wait_int.delete(0, 'end') - self.page.auto_wait_int.insert(0, '11') - self.assertEqual(extpage, {'AutoComplete': {'popupwait': '11'}}) - - def test_parenmatch(self): - d = self.page - eq = self.assertEqual - d.paren_style_type['menu'].invoke(0) - eq(extpage, {'ParenMatch': {'style': 'opener'}}) - changes.clear() - d.paren_flash_time.delete(0, 'end') - d.paren_flash_time.insert(0, '11') - eq(extpage, {'ParenMatch': {'flash-delay': '11'}}) - changes.clear() - d.bell_on.invoke() - eq(extpage, {'ParenMatch': {'bell': 'False'}}) - - def test_autosave(self): - d = self.page - d.save_auto_on.invoke() - self.assertEqual(mainpage, {'General': {'autosave': '1'}}) - d.save_ask_on.invoke() - self.assertEqual(mainpage, {'General': {'autosave': '0'}}) - - def test_paragraph(self): - self.page.format_width_int.delete(0, 'end') - self.page.format_width_int.insert(0, '11') - self.assertEqual(extpage, {'FormatParagraph': {'max-width': '11'}}) - - def test_context(self): - self.page.context_int.delete(0, 'end') - self.page.context_int.insert(0, '1') - self.assertEqual(extpage, {'CodeContext': {'maxlines': '1'}}) - - def test_source_selected(self): - d = self.page - d.set = d.set_add_delete_state - d.upc = d.update_help_changes - helplist = d.helplist - dex = 'end' - helplist.insert(dex, 'source') - helplist.activate(dex) - - helplist.focus_force() - helplist.see(dex) - helplist.update() - x, y, dx, dy = helplist.bbox(dex) - x += dx // 2 - y += dy // 2 - d.set.called = d.upc.called = 0 - helplist.event_generate('<Enter>', x=0, y=0) - helplist.event_generate('<Motion>', x=x, y=y) - helplist.event_generate('<Button-1>', x=x, y=y) - helplist.event_generate('<ButtonRelease-1>', x=x, y=y) - self.assertEqual(helplist.get('anchor'), 'source') - self.assertTrue(d.set.called) - self.assertFalse(d.upc.called) - - def test_set_add_delete_state(self): - # Call with 0 items, 1 unselected item, 1 selected item. - eq = self.assertEqual - d = self.page - del d.set_add_delete_state # Unmask method. - sad = d.set_add_delete_state - h = d.helplist - - h.delete(0, 'end') - sad() - eq(d.button_helplist_edit.state(), ('disabled',)) - eq(d.button_helplist_remove.state(), ('disabled',)) - - h.insert(0, 'source') - sad() - eq(d.button_helplist_edit.state(), ('disabled',)) - eq(d.button_helplist_remove.state(), ('disabled',)) - - h.selection_set(0) - sad() - eq(d.button_helplist_edit.state(), ()) - eq(d.button_helplist_remove.state(), ()) - d.set_add_delete_state = Func() # Mask method. - - def test_helplist_item_add(self): - # Call without and twice with HelpSource result. - # Double call enables check on order. - eq = self.assertEqual - orig_helpsource = configdialog.HelpSource - hs = configdialog.HelpSource = Func(return_self=True) - d = self.page - d.helplist.delete(0, 'end') - d.user_helplist.clear() - d.set.called = d.upc.called = 0 - - hs.result = '' - d.helplist_item_add() - self.assertTrue(list(d.helplist.get(0, 'end')) == - d.user_helplist == []) - self.assertFalse(d.upc.called) - - hs.result = ('name1', 'file1') - d.helplist_item_add() - hs.result = ('name2', 'file2') - d.helplist_item_add() - eq(d.helplist.get(0, 'end'), ('name1', 'name2')) - eq(d.user_helplist, [('name1', 'file1'), ('name2', 'file2')]) - eq(d.upc.called, 2) - self.assertFalse(d.set.called) - - configdialog.HelpSource = orig_helpsource - - def test_helplist_item_edit(self): - # Call without and with HelpSource change. - eq = self.assertEqual - orig_helpsource = configdialog.HelpSource - hs = configdialog.HelpSource = Func(return_self=True) - d = self.page - d.helplist.delete(0, 'end') - d.helplist.insert(0, 'name1') - d.helplist.selection_set(0) - d.helplist.selection_anchor(0) - d.user_helplist.clear() - d.user_helplist.append(('name1', 'file1')) - d.set.called = d.upc.called = 0 - - hs.result = '' - d.helplist_item_edit() - hs.result = ('name1', 'file1') - d.helplist_item_edit() - eq(d.helplist.get(0, 'end'), ('name1',)) - eq(d.user_helplist, [('name1', 'file1')]) - self.assertFalse(d.upc.called) - - hs.result = ('name2', 'file2') - d.helplist_item_edit() - eq(d.helplist.get(0, 'end'), ('name2',)) - eq(d.user_helplist, [('name2', 'file2')]) - self.assertTrue(d.upc.called == d.set.called == 1) - - configdialog.HelpSource = orig_helpsource - - def test_helplist_item_remove(self): - eq = self.assertEqual - d = self.page - d.helplist.delete(0, 'end') - d.helplist.insert(0, 'name1') - d.helplist.selection_set(0) - d.helplist.selection_anchor(0) - d.user_helplist.clear() - d.user_helplist.append(('name1', 'file1')) - d.set.called = d.upc.called = 0 - - d.helplist_item_remove() - eq(d.helplist.get(0, 'end'), ()) - eq(d.user_helplist, []) - self.assertTrue(d.upc.called == d.set.called == 1) - - def test_update_help_changes(self): - d = self.page - del d.update_help_changes - d.user_helplist.clear() - d.user_helplist.append(('name1', 'file1')) - d.user_helplist.append(('name2', 'file2')) - - d.update_help_changes() - self.assertEqual(mainpage['HelpFiles'], - {'1': 'name1;file1', '2': 'name2;file2'}) - d.update_help_changes = Func() +''' +import unittest +from test.test_support import requires +from Tkinter import Tk +from idlelib.configDialog import ConfigDialog +from idlelib.macosxSupport import _initializeTkVariantTests -class VarTraceTest(unittest.TestCase): +class ConfigDialogTest(unittest.TestCase): @classmethod def setUpClass(cls): - cls.tracers = configdialog.VarTrace() - cls.iv = IntVar(root) - cls.bv = BooleanVar(root) + requires('gui') + cls.root = Tk() + cls.root.withdraw() + _initializeTkVariantTests(cls.root) @classmethod def tearDownClass(cls): - del cls.tracers, cls.iv, cls.bv - - def setUp(self): - self.tracers.clear() - self.called = 0 - - def var_changed_increment(self, *params): - self.called += 13 - - def var_changed_boolean(self, *params): - pass - - def test_init(self): - tr = self.tracers - tr.__init__() - self.assertEqual(tr.untraced, []) - self.assertEqual(tr.traced, []) - - def test_clear(self): - tr = self.tracers - tr.untraced.append(0) - tr.traced.append(1) - tr.clear() - self.assertEqual(tr.untraced, []) - self.assertEqual(tr.traced, []) - - def test_add(self): - tr = self.tracers - func = Func() - cb = tr.make_callback = mock.Mock(return_value=func) - - iv = tr.add(self.iv, self.var_changed_increment) - self.assertIs(iv, self.iv) - bv = tr.add(self.bv, self.var_changed_boolean) - self.assertIs(bv, self.bv) - - sv = StringVar(root) - sv2 = tr.add(sv, ('main', 'section', 'option')) - self.assertIs(sv2, sv) - cb.assert_called_once() - cb.assert_called_with(sv, ('main', 'section', 'option')) - - expected = [(iv, self.var_changed_increment), - (bv, self.var_changed_boolean), - (sv, func)] - self.assertEqual(tr.traced, []) - self.assertEqual(tr.untraced, expected) - - del tr.make_callback - - def test_make_callback(self): - cb = self.tracers.make_callback(self.iv, ('main', 'section', 'option')) - self.assertTrue(callable(cb)) - self.iv.set(42) - # Not attached, so set didn't invoke the callback. - self.assertNotIn('section', changes['main']) - # Invoke callback manually. - cb() - self.assertIn('section', changes['main']) - self.assertEqual(changes['main']['section']['option'], '42') - changes.clear() - - def test_attach_detach(self): - tr = self.tracers - iv = tr.add(self.iv, self.var_changed_increment) - bv = tr.add(self.bv, self.var_changed_boolean) - expected = [(iv, self.var_changed_increment), - (bv, self.var_changed_boolean)] - - # Attach callbacks and test call increment. - tr.attach() - self.assertEqual(tr.untraced, []) - self.assertCountEqual(tr.traced, expected) - iv.set(1) - self.assertEqual(iv.get(), 1) - self.assertEqual(self.called, 13) - - # Check that only one callback is attached to a variable. - # If more than one callback were attached, then var_changed_increment - # would be called twice and the counter would be 2. - self.called = 0 - tr.attach() - iv.set(1) - self.assertEqual(self.called, 13) + cls.root.destroy() + del cls.root - # Detach callbacks. - self.called = 0 - tr.detach() - self.assertEqual(tr.traced, []) - self.assertCountEqual(tr.untraced, expected) - iv.set(1) - self.assertEqual(self.called, 0) + def test_dialog(self): + d = ConfigDialog(self.root, 'Test', _utest=True) + d.remove_var_callbacks() if __name__ == '__main__': diff --git a/Lib/idlelib/idle_test/test_debugger.py b/Lib/idlelib/idle_test/test_debugger.py deleted file mode 100644 index 35efb34..0000000 --- a/Lib/idlelib/idle_test/test_debugger.py +++ /dev/null @@ -1,29 +0,0 @@ -"Test debugger, coverage 19%" - -from idlelib import debugger -import unittest -from test.support import requires -requires('gui') -from tkinter import Tk - - -class NameSpaceTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.root = Tk() - cls.root.withdraw() - - @classmethod - def tearDownClass(cls): - cls.root.destroy() - del cls.root - - def test_init(self): - debugger.NamespaceViewer(self.root, 'Test') - - -# Other classes are Idb, Debugger, and StackViewer. - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_debugger_r.py b/Lib/idlelib/idle_test/test_debugger_r.py deleted file mode 100644 index 199f634..0000000 --- a/Lib/idlelib/idle_test/test_debugger_r.py +++ /dev/null @@ -1,29 +0,0 @@ -"Test debugger_r, coverage 30%." - -from idlelib import debugger_r -import unittest -from test.support import requires -from tkinter import Tk - - -class Test(unittest.TestCase): - -## @classmethod -## def setUpClass(cls): -## requires('gui') -## cls.root = Tk() -## -## @classmethod -## def tearDownClass(cls): -## cls.root.destroy() -## del cls.root - - def test_init(self): - self.assertTrue(True) # Get coverage of import - - -# Classes GUIProxy, IdbAdapter, FrameProxy, CodeProxy, DictProxy, -# GUIAdapter, IdbProxy plus 7 module functions. - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_debugobj.py b/Lib/idlelib/idle_test/test_debugobj.py deleted file mode 100644 index 131ce22..0000000 --- a/Lib/idlelib/idle_test/test_debugobj.py +++ /dev/null @@ -1,57 +0,0 @@ -"Test debugobj, coverage 40%." - -from idlelib import debugobj -import unittest - - -class ObjectTreeItemTest(unittest.TestCase): - - def test_init(self): - ti = debugobj.ObjectTreeItem('label', 22) - self.assertEqual(ti.labeltext, 'label') - self.assertEqual(ti.object, 22) - self.assertEqual(ti.setfunction, None) - - -class ClassTreeItemTest(unittest.TestCase): - - def test_isexpandable(self): - ti = debugobj.ClassTreeItem('label', 0) - self.assertTrue(ti.IsExpandable()) - - -class AtomicObjectTreeItemTest(unittest.TestCase): - - def test_isexpandable(self): - ti = debugobj.AtomicObjectTreeItem('label', 0) - self.assertFalse(ti.IsExpandable()) - - -class SequenceTreeItemTest(unittest.TestCase): - - def test_isexpandable(self): - ti = debugobj.SequenceTreeItem('label', ()) - self.assertFalse(ti.IsExpandable()) - ti = debugobj.SequenceTreeItem('label', (1,)) - self.assertTrue(ti.IsExpandable()) - - def test_keys(self): - ti = debugobj.SequenceTreeItem('label', 'abc') - self.assertEqual(list(ti.keys()), [0, 1, 2]) - - -class DictTreeItemTest(unittest.TestCase): - - def test_isexpandable(self): - ti = debugobj.DictTreeItem('label', {}) - self.assertFalse(ti.IsExpandable()) - ti = debugobj.DictTreeItem('label', {1:1}) - self.assertTrue(ti.IsExpandable()) - - def test_keys(self): - ti = debugobj.DictTreeItem('label', {1:1, 0:0, 2:2}) - self.assertEqual(ti.keys(), [0, 1, 2]) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_debugobj_r.py b/Lib/idlelib/idle_test/test_debugobj_r.py deleted file mode 100644 index 86e51b6..0000000 --- a/Lib/idlelib/idle_test/test_debugobj_r.py +++ /dev/null @@ -1,22 +0,0 @@ -"Test debugobj_r, coverage 56%." - -from idlelib import debugobj_r -import unittest - - -class WrappedObjectTreeItemTest(unittest.TestCase): - - def test_getattr(self): - ti = debugobj_r.WrappedObjectTreeItem(list) - self.assertEqual(ti.append, list.append) - -class StubObjectTreeItemTest(unittest.TestCase): - - def test_init(self): - ti = debugobj_r.StubObjectTreeItem('socket', 1111) - self.assertEqual(ti.sockio, 'socket') - self.assertEqual(ti.oid, 1111) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_delegator.py b/Lib/idlelib/idle_test/test_delegator.py index 9224162..b8ae5ee 100644 --- a/Lib/idlelib/idle_test/test_delegator.py +++ b/Lib/idlelib/idle_test/test_delegator.py @@ -1,44 +1,37 @@ -"Test delegator, coverage 100%." - -from idlelib.delegator import Delegator import unittest - +from idlelib.Delegator import Delegator class DelegatorTest(unittest.TestCase): def test_mydel(self): - # Test a simple use scenario. + # test a simple use scenario - # Initialize an int delegator. + # initialize mydel = Delegator(int) self.assertIs(mydel.delegate, int) self.assertEqual(mydel._Delegator__cache, set()) - # Trying to access a non-attribute of int fails. - self.assertRaises(AttributeError, mydel.__getattr__, 'xyz') - # Add real int attribute 'bit_length' by accessing it. + # add an attribute: + self.assertRaises(AttributeError, mydel.__getattr__, 'xyz') bl = mydel.bit_length self.assertIs(bl, int.bit_length) self.assertIs(mydel.__dict__['bit_length'], int.bit_length) self.assertEqual(mydel._Delegator__cache, {'bit_length'}) - # Add attribute 'numerator'. + # add a second attribute mydel.numerator self.assertEqual(mydel._Delegator__cache, {'bit_length', 'numerator'}) - # Delete 'numerator'. + # delete the second (which, however, leaves it in the name cache) del mydel.numerator self.assertNotIn('numerator', mydel.__dict__) - # The current implementation leaves it in the name cache. - # self.assertIn('numerator', mydel._Delegator__cache) - # However, this is not required and not part of the specification + self.assertIn('numerator', mydel._Delegator__cache) - # Change delegate to float, first resetting the attributes. - mydel.setdelegate(float) # calls resetcache + # reset by calling .setdelegate, which calls .resetcache + mydel.setdelegate(float) + self.assertIs(mydel.delegate, float) self.assertNotIn('bit_length', mydel.__dict__) self.assertEqual(mydel._Delegator__cache, set()) - self.assertIs(mydel.delegate, float) - if __name__ == '__main__': unittest.main(verbosity=2, exit=2) diff --git a/Lib/idlelib/idle_test/test_editmenu.py b/Lib/idlelib/idle_test/test_editmenu.py index 1747847..51d5c16 100644 --- a/Lib/idlelib/idle_test/test_editmenu.py +++ b/Lib/idlelib/idle_test/test_editmenu.py @@ -1,73 +1,100 @@ '''Test (selected) IDLE Edit menu items. -Edit modules have their own test files +Edit modules have their own test files files ''' -from test.support import requires -requires('gui') -import tkinter as tk -from tkinter import ttk +from test.test_support import requires +import Tkinter as tk import unittest -from idlelib import pyshell +from idlelib import PyShell + class PasteTest(unittest.TestCase): '''Test pasting into widgets that allow pasting. On X11, replacing selections requires tk fix. ''' + @classmethod def setUpClass(cls): + requires('gui') cls.root = root = tk.Tk() - cls.root.withdraw() - pyshell.fix_x11_paste(root) + root.withdraw() + PyShell.fix_x11_paste(root) cls.text = tk.Text(root) cls.entry = tk.Entry(root) - cls.tentry = ttk.Entry(root) cls.spin = tk.Spinbox(root) root.clipboard_clear() root.clipboard_append('two') @classmethod def tearDownClass(cls): - del cls.text, cls.entry, cls.tentry + del cls.text, cls.entry, cls.spin cls.root.clipboard_clear() cls.root.update_idletasks() + cls.root.update() cls.root.destroy() del cls.root - def test_paste_text(self): - "Test pasting into text with and without a selection." + def test_paste_text_no_selection(self): + "Test pasting into text without a selection." + text = self.text + tag, ans = '', 'onetwo\n' + text.delete('1.0', 'end') + text.insert('1.0', 'one', tag) + text.event_generate('<<Paste>>') + self.assertEqual(text.get('1.0', 'end'), ans) + + def test_paste_text_selection(self): + "Test pasting into text with a selection." text = self.text - for tag, ans in ('', 'onetwo\n'), ('sel', 'two\n'): - with self.subTest(tag=tag, ans=ans): - text.delete('1.0', 'end') - text.insert('1.0', 'one', tag) - text.event_generate('<<Paste>>') - self.assertEqual(text.get('1.0', 'end'), ans) + tag, ans = 'sel', 'two\n' + text.delete('1.0', 'end') + text.insert('1.0', 'one', tag) + text.event_generate('<<Paste>>') + self.assertEqual(text.get('1.0', 'end'), ans) - def test_paste_entry(self): - "Test pasting into an entry with and without a selection." - # Generated <<Paste>> fails for tk entry without empty select - # range for 'no selection'. Live widget works fine. - for entry in self.entry, self.tentry: - for end, ans in (0, 'onetwo'), ('end', 'two'): - with self.subTest(entry=entry, end=end, ans=ans): - entry.delete(0, 'end') - entry.insert(0, 'one') - entry.select_range(0, end) - entry.event_generate('<<Paste>>') - self.assertEqual(entry.get(), ans) + def test_paste_entry_no_selection(self): + "Test pasting into an entry without a selection." + # On 3.6, generated <<Paste>> fails without empty select range + # for 'no selection'. Live widget works fine. + entry = self.entry + end, ans = 0, 'onetwo' + entry.delete(0, 'end') + entry.insert(0, 'one') + entry.select_range(0, end) # see note + entry.event_generate('<<Paste>>') + self.assertEqual(entry.get(), ans) - def test_paste_spin(self): - "Test pasting into a spinbox with and without a selection." + def test_paste_entry_selection(self): + "Test pasting into an entry with a selection." + entry = self.entry + end, ans = 'end', 'two' + entry.delete(0, 'end') + entry.insert(0, 'one') + entry.select_range(0, end) + entry.event_generate('<<Paste>>') + self.assertEqual(entry.get(), ans) + + def test_paste_spin_no_selection(self): + "Test pasting into a spinbox without a selection." # See note above for entry. spin = self.spin - for end, ans in (0, 'onetwo'), ('end', 'two'): - with self.subTest(end=end, ans=ans): - spin.delete(0, 'end') - spin.insert(0, 'one') - spin.selection('range', 0, end) # see note - spin.event_generate('<<Paste>>') - self.assertEqual(spin.get(), ans) + end, ans = 0, 'onetwo' + spin.delete(0, 'end') + spin.insert(0, 'one') + spin.selection('range', 0, end) # see note + spin.event_generate('<<Paste>>') + self.assertEqual(spin.get(), ans) + + def test_paste_spin_selection(self): + "Test pasting into a spinbox with a selection." + spin = self.spin + end, ans = 'end', 'two' + spin.delete(0, 'end') + spin.insert(0, 'one') + spin.selection('range', 0, end) + spin.event_generate('<<Paste>>') + self.assertEqual(spin.get(), ans) if __name__ == '__main__': diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py deleted file mode 100644 index 240db71..0000000 --- a/Lib/idlelib/idle_test/test_editor.py +++ /dev/null @@ -1,95 +0,0 @@ -"Test editor, coverage 35%." - -from idlelib import editor -import unittest -from test.support import requires -from tkinter import Tk - -Editor = editor.EditorWindow - - -class EditorWindowTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - - @classmethod - def tearDownClass(cls): - cls.root.update_idletasks() - for id in cls.root.tk.call('after', 'info'): - cls.root.after_cancel(id) - cls.root.destroy() - del cls.root - - def test_init(self): - e = Editor(root=self.root) - self.assertEqual(e.root, self.root) - e._close() - - -class TestGetLineIndent(unittest.TestCase): - def test_empty_lines(self): - for tabwidth in [1, 2, 4, 6, 8]: - for line in ['', '\n']: - with self.subTest(line=line, tabwidth=tabwidth): - self.assertEqual( - editor.get_line_indent(line, tabwidth=tabwidth), - (0, 0), - ) - - def test_tabwidth_4(self): - # (line, (raw, effective)) - tests = (('no spaces', (0, 0)), - # Internal space isn't counted. - (' space test', (4, 4)), - ('\ttab test', (1, 4)), - ('\t\tdouble tabs test', (2, 8)), - # Different results when mixing tabs and spaces. - (' \tmixed test', (5, 8)), - (' \t mixed test', (5, 6)), - ('\t mixed test', (5, 8)), - # Spaces not divisible by tabwidth. - (' \tmixed test', (3, 4)), - (' \t mixed test', (3, 5)), - ('\t mixed test', (3, 6)), - # Only checks spaces and tabs. - ('\nnewline test', (0, 0))) - - for line, expected in tests: - with self.subTest(line=line): - self.assertEqual( - editor.get_line_indent(line, tabwidth=4), - expected, - ) - - def test_tabwidth_8(self): - # (line, (raw, effective)) - tests = (('no spaces', (0, 0)), - # Internal space isn't counted. - (' space test', (8, 8)), - ('\ttab test', (1, 8)), - ('\t\tdouble tabs test', (2, 16)), - # Different results when mixing tabs and spaces. - (' \tmixed test', (9, 16)), - (' \t mixed test', (9, 10)), - ('\t mixed test', (9, 16)), - # Spaces not divisible by tabwidth. - (' \tmixed test', (3, 8)), - (' \t mixed test', (3, 9)), - ('\t mixed test', (3, 10)), - # Only checks spaces and tabs. - ('\nnewline test', (0, 0))) - - for line, expected in tests: - with self.subTest(line=line): - self.assertEqual( - editor.get_line_indent(line, tabwidth=8), - expected, - ) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_filelist.py b/Lib/idlelib/idle_test/test_filelist.py deleted file mode 100644 index 731f197..0000000 --- a/Lib/idlelib/idle_test/test_filelist.py +++ /dev/null @@ -1,33 +0,0 @@ -"Test filelist, coverage 19%." - -from idlelib import filelist -import unittest -from test.support import requires -from tkinter import Tk - -class FileListTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - - @classmethod - def tearDownClass(cls): - cls.root.update_idletasks() - for id in cls.root.tk.call('after', 'info'): - cls.root.after_cancel(id) - cls.root.destroy() - del cls.root - - def test_new_empty(self): - flist = filelist.FileList(self.root) - self.assertEqual(flist.root, self.root) - e = flist.new() - self.assertEqual(type(e), flist.EditorWindow) - e._close() - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_format.py b/Lib/idlelib/idle_test/test_formatparagraph.py index a79bb51..068ae38 100644 --- a/Lib/idlelib/idle_test/test_format.py +++ b/Lib/idlelib/idle_test/test_formatparagraph.py @@ -1,12 +1,9 @@ -"Test format, coverage 99%." - -from idlelib import format as ft +# Test the functions and main class method of FormatParagraph.py import unittest -from unittest import mock -from test.support import requires -from tkinter import Tk, Text -from idlelib.editor import EditorWindow -from idlelib.idle_test.mock_idle import Editor as MockEditor +from idlelib import FormatParagraph as fp +from idlelib.EditorWindow import EditorWindow +from Tkinter import Tk, Text +from test.test_support import requires class Is_Get_Test(unittest.TestCase): @@ -18,30 +15,30 @@ class Is_Get_Test(unittest.TestCase): leadingws_nocomment = ' This is not a comment' def test_is_all_white(self): - self.assertTrue(ft.is_all_white('')) - self.assertTrue(ft.is_all_white('\t\n\r\f\v')) - self.assertFalse(ft.is_all_white(self.test_comment)) + self.assertTrue(fp.is_all_white('')) + self.assertTrue(fp.is_all_white('\t\n\r\f\v')) + self.assertFalse(fp.is_all_white(self.test_comment)) def test_get_indent(self): Equal = self.assertEqual - Equal(ft.get_indent(self.test_comment), '') - Equal(ft.get_indent(self.trailingws_comment), '') - Equal(ft.get_indent(self.leadingws_comment), ' ') - Equal(ft.get_indent(self.leadingws_nocomment), ' ') + Equal(fp.get_indent(self.test_comment), '') + Equal(fp.get_indent(self.trailingws_comment), '') + Equal(fp.get_indent(self.leadingws_comment), ' ') + Equal(fp.get_indent(self.leadingws_nocomment), ' ') def test_get_comment_header(self): Equal = self.assertEqual # Test comment strings - Equal(ft.get_comment_header(self.test_comment), '#') - Equal(ft.get_comment_header(self.trailingws_comment), '#') - Equal(ft.get_comment_header(self.leadingws_comment), ' #') + Equal(fp.get_comment_header(self.test_comment), '#') + Equal(fp.get_comment_header(self.trailingws_comment), '#') + Equal(fp.get_comment_header(self.leadingws_comment), ' #') # Test non-comment strings - Equal(ft.get_comment_header(self.leadingws_nocomment), ' ') - Equal(ft.get_comment_header(self.test_nocomment), '') + Equal(fp.get_comment_header(self.leadingws_nocomment), ' ') + Equal(fp.get_comment_header(self.test_nocomment), '') class FindTest(unittest.TestCase): - """Test the find_paragraph function in paragraph module. + """Test the find_paragraph function in FormatParagraph. Using the runcase() function, find_paragraph() is called with 'mark' set at multiple indexes before and inside the test paragraph. @@ -65,7 +62,7 @@ class FindTest(unittest.TestCase): linelength = int(text.index("%d.end" % line).split('.')[1]) for col in (0, linelength//2, linelength): tempindex = "%d.%d" % (line, col) - self.assertEqual(ft.find_paragraph(text, tempindex), expected) + self.assertEqual(fp.find_paragraph(text, tempindex), expected) text.delete('1.0', 'end') def test_find_comment(self): @@ -164,7 +161,7 @@ class ReformatFunctionTest(unittest.TestCase): def test_reformat_paragraph(self): Equal = self.assertEqual - reform = ft.reformat_paragraph + reform = fp.reformat_paragraph hw = "O hello world" Equal(reform(' ', 1), ' ') Equal(reform("Hello world", 20), "Hello world") @@ -195,7 +192,7 @@ class ReformatCommentTest(unittest.TestCase): test_string = ( " \"\"\"this is a test of a reformat for a triple quoted string" " will it reformat to less than 70 characters for me?\"\"\"") - result = ft.reformat_comment(test_string, 70, " ") + result = fp.reformat_comment(test_string, 70, " ") expected = ( " \"\"\"this is a test of a reformat for a triple quoted string will it\n" " reformat to less than 70 characters for me?\"\"\"") @@ -204,7 +201,7 @@ class ReformatCommentTest(unittest.TestCase): test_comment = ( "# this is a test of a reformat for a triple quoted string will " "it reformat to less than 70 characters for me?") - result = ft.reformat_comment(test_comment, 70, "#") + result = fp.reformat_comment(test_comment, 70, "#") expected = ( "# this is a test of a reformat for a triple quoted string will it\n" "# reformat to less than 70 characters for me?") @@ -213,7 +210,7 @@ class ReformatCommentTest(unittest.TestCase): class FormatClassTest(unittest.TestCase): def test_init_close(self): - instance = ft.FormatParagraph('editor') + instance = fp.FormatParagraph('editor') self.assertEqual(instance.editwin, 'editor') instance.close() self.assertEqual(instance.editwin, None) @@ -241,7 +238,7 @@ class TextWrapper: class Editor: def __init__(self, root): self.text = TextWrapper(root) - get_selection_indices = EditorWindow. get_selection_indices + get_selection_indices = EditorWindow. get_selection_indices.im_func class FormatEventTest(unittest.TestCase): """Test the formatting of text inside a Text widget. @@ -272,16 +269,14 @@ class FormatEventTest(unittest.TestCase): def setUpClass(cls): requires('gui') cls.root = Tk() - cls.root.withdraw() editor = Editor(root=cls.root) cls.text = editor.text.text # Test code does not need the wrapper. - cls.formatter = ft.FormatParagraph(editor).format_paragraph_event + cls.formatter = fp.FormatParagraph(editor).format_paragraph_event # Sets the insert mark just after the re-wrapped and inserted text. @classmethod def tearDownClass(cls): del cls.text, cls.formatter - cls.root.update_idletasks() cls.root.destroy() del cls.root @@ -377,292 +372,5 @@ class FormatEventTest(unittest.TestCase): ## text.delete('1.0', 'end') -class DummyEditwin: - def __init__(self, root, text): - self.root = root - self.text = text - self.indentwidth = 4 - self.tabwidth = 4 - self.usetabs = False - self.context_use_ps1 = True - - _make_blanks = EditorWindow._make_blanks - get_selection_indices = EditorWindow.get_selection_indices - - -class FormatRegionTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - cls.text = Text(cls.root) - cls.text.undo_block_start = mock.Mock() - cls.text.undo_block_stop = mock.Mock() - cls.editor = DummyEditwin(cls.root, cls.text) - cls.formatter = ft.FormatRegion(cls.editor) - - @classmethod - def tearDownClass(cls): - del cls.text, cls.formatter, cls.editor - cls.root.update_idletasks() - cls.root.destroy() - del cls.root - - def setUp(self): - self.text.insert('1.0', self.code_sample) - - def tearDown(self): - self.text.delete('1.0', 'end') - - code_sample = """\ -# WS line needed for test. -class C1(): - # Class comment. - def __init__(self, a, b): - self.a = a - self.b = b - - def compare(self): - if a > b: - return a - elif a < b: - return b - else: - return None -""" - - def test_get_region(self): - get = self.formatter.get_region - text = self.text - eq = self.assertEqual - - # Add selection. - text.tag_add('sel', '7.0', '10.0') - expected_lines = ['', - ' def compare(self):', - ' if a > b:', - ''] - eq(get(), ('7.0', '10.0', '\n'.join(expected_lines), expected_lines)) - - # Remove selection. - text.tag_remove('sel', '1.0', 'end') - eq(get(), ('15.0', '16.0', '\n', ['', ''])) - - def test_set_region(self): - set_ = self.formatter.set_region - text = self.text - eq = self.assertEqual - - save_bell = text.bell - text.bell = mock.Mock() - line6 = self.code_sample.splitlines()[5] - line10 = self.code_sample.splitlines()[9] - - text.tag_add('sel', '6.0', '11.0') - head, tail, chars, lines = self.formatter.get_region() - - # No changes. - set_(head, tail, chars, lines) - text.bell.assert_called_once() - eq(text.get('6.0', '11.0'), chars) - eq(text.get('sel.first', 'sel.last'), chars) - text.tag_remove('sel', '1.0', 'end') - - # Alter selected lines by changing lines and adding a newline. - newstring = 'added line 1\n\n\n\n' - newlines = newstring.split('\n') - set_('7.0', '10.0', chars, newlines) - # Selection changed. - eq(text.get('sel.first', 'sel.last'), newstring) - # Additional line added, so last index is changed. - eq(text.get('7.0', '11.0'), newstring) - # Before and after lines unchanged. - eq(text.get('6.0', '7.0-1c'), line6) - eq(text.get('11.0', '12.0-1c'), line10) - text.tag_remove('sel', '1.0', 'end') - - text.bell = save_bell - - def test_indent_region_event(self): - indent = self.formatter.indent_region_event - text = self.text - eq = self.assertEqual - - text.tag_add('sel', '7.0', '10.0') - indent() - # Blank lines aren't affected by indent. - eq(text.get('7.0', '10.0'), ('\n def compare(self):\n if a > b:\n')) - - def test_dedent_region_event(self): - dedent = self.formatter.dedent_region_event - text = self.text - eq = self.assertEqual - - text.tag_add('sel', '7.0', '10.0') - dedent() - # Blank lines aren't affected by dedent. - eq(text.get('7.0', '10.0'), ('\ndef compare(self):\n if a > b:\n')) - - def test_comment_region_event(self): - comment = self.formatter.comment_region_event - text = self.text - eq = self.assertEqual - - text.tag_add('sel', '7.0', '10.0') - comment() - eq(text.get('7.0', '10.0'), ('##\n## def compare(self):\n## if a > b:\n')) - - def test_uncomment_region_event(self): - comment = self.formatter.comment_region_event - uncomment = self.formatter.uncomment_region_event - text = self.text - eq = self.assertEqual - - text.tag_add('sel', '7.0', '10.0') - comment() - uncomment() - eq(text.get('7.0', '10.0'), ('\n def compare(self):\n if a > b:\n')) - - # Only remove comments at the beginning of a line. - text.tag_remove('sel', '1.0', 'end') - text.tag_add('sel', '3.0', '4.0') - uncomment() - eq(text.get('3.0', '3.end'), (' # Class comment.')) - - self.formatter.set_region('3.0', '4.0', '', ['# Class comment.', '']) - uncomment() - eq(text.get('3.0', '3.end'), (' Class comment.')) - - @mock.patch.object(ft.FormatRegion, "_asktabwidth") - def test_tabify_region_event(self, _asktabwidth): - tabify = self.formatter.tabify_region_event - text = self.text - eq = self.assertEqual - - text.tag_add('sel', '7.0', '10.0') - # No tabwidth selected. - _asktabwidth.return_value = None - self.assertIsNone(tabify()) - - _asktabwidth.return_value = 3 - self.assertIsNotNone(tabify()) - eq(text.get('7.0', '10.0'), ('\n\t def compare(self):\n\t\t if a > b:\n')) - - @mock.patch.object(ft.FormatRegion, "_asktabwidth") - def test_untabify_region_event(self, _asktabwidth): - untabify = self.formatter.untabify_region_event - text = self.text - eq = self.assertEqual - - text.tag_add('sel', '7.0', '10.0') - # No tabwidth selected. - _asktabwidth.return_value = None - self.assertIsNone(untabify()) - - _asktabwidth.return_value = 2 - self.formatter.tabify_region_event() - _asktabwidth.return_value = 3 - self.assertIsNotNone(untabify()) - eq(text.get('7.0', '10.0'), ('\n def compare(self):\n if a > b:\n')) - - @mock.patch.object(ft, "askinteger") - def test_ask_tabwidth(self, askinteger): - ask = self.formatter._asktabwidth - askinteger.return_value = 10 - self.assertEqual(ask(), 10) - - -class IndentsTest(unittest.TestCase): - - @mock.patch.object(ft, "askyesno") - def test_toggle_tabs(self, askyesno): - editor = DummyEditwin(None, None) # usetabs == False. - indents = ft.Indents(editor) - askyesno.return_value = True - - indents.toggle_tabs_event(None) - self.assertEqual(editor.usetabs, True) - self.assertEqual(editor.indentwidth, 8) - - indents.toggle_tabs_event(None) - self.assertEqual(editor.usetabs, False) - self.assertEqual(editor.indentwidth, 8) - - @mock.patch.object(ft, "askinteger") - def test_change_indentwidth(self, askinteger): - editor = DummyEditwin(None, None) # indentwidth == 4. - indents = ft.Indents(editor) - - askinteger.return_value = None - indents.change_indentwidth_event(None) - self.assertEqual(editor.indentwidth, 4) - - askinteger.return_value = 3 - indents.change_indentwidth_event(None) - self.assertEqual(editor.indentwidth, 3) - - askinteger.return_value = 5 - editor.usetabs = True - indents.change_indentwidth_event(None) - self.assertEqual(editor.indentwidth, 3) - - -class RstripTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - cls.text = Text(cls.root) - cls.editor = MockEditor(text=cls.text) - cls.do_rstrip = ft.Rstrip(cls.editor).do_rstrip - - @classmethod - def tearDownClass(cls): - del cls.text, cls.do_rstrip, cls.editor - cls.root.update_idletasks() - cls.root.destroy() - del cls.root - - def tearDown(self): - self.text.delete('1.0', 'end-1c') - - def test_rstrip_lines(self): - original = ( - "Line with an ending tab \n" - "Line ending in 5 spaces \n" - "Linewithnospaces\n" - " indented line\n" - " indented line with trailing space \n" - " \n") - stripped = ( - "Line with an ending tab\n" - "Line ending in 5 spaces\n" - "Linewithnospaces\n" - " indented line\n" - " indented line with trailing space\n") - - self.text.insert('1.0', original) - self.do_rstrip() - self.assertEqual(self.text.get('1.0', 'insert'), stripped) - - def test_rstrip_end(self): - text = self.text - for code in ('', '\n', '\n\n\n'): - with self.subTest(code=code): - text.insert('1.0', code) - self.do_rstrip() - self.assertEqual(text.get('1.0','end-1c'), '') - for code in ('a\n', 'a\n\n', 'a\n\n\n'): - with self.subTest(code=code): - text.delete('1.0', 'end-1c') - text.insert('1.0', code) - self.do_rstrip() - self.assertEqual(text.get('1.0','end-1c'), 'a\n') - - if __name__ == '__main__': unittest.main(verbosity=2, exit=2) diff --git a/Lib/idlelib/idle_test/test_grep.py b/Lib/idlelib/idle_test/test_grep.py index a0b5b69..e9f4f22 100644 --- a/Lib/idlelib/idle_test/test_grep.py +++ b/Lib/idlelib/idle_test/test_grep.py @@ -1,17 +1,17 @@ """ !Changing this line will break Test_findfile.test_found! -Non-gui unit tests for grep.GrepDialog methods. +Non-gui unit tests for idlelib.GrepDialog methods. dummy_command calls grep_it calls findfiles. An exception raised in one method will fail callers. Otherwise, tests are mostly independent. -Currently only test grep_it, coverage 51%. +*** Currently only test grep_it. """ -from idlelib import grep import unittest -from test.support import captured_stdout +from test.test_support import captured_stdout, findfile from idlelib.idle_test.mock_tk import Var -import os +from idlelib.GrepDialog import GrepDialog import re +__file__ = findfile('idlelib/idle_test') + '/test_grep.py' class Dummy_searchengine: '''GrepDialog.__init__ calls parent SearchDiabolBase which attaches the @@ -23,97 +23,25 @@ class Dummy_searchengine: searchengine = Dummy_searchengine() - class Dummy_grep: # Methods tested #default_command = GrepDialog.default_command - grep_it = grep.GrepDialog.grep_it + grep_it = GrepDialog.grep_it.im_func + findfiles = GrepDialog.findfiles.im_func # Other stuff needed recvar = Var(False) engine = searchengine def close(self): # gui method pass -_grep = Dummy_grep() - +grep = Dummy_grep() class FindfilesTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.realpath = os.path.realpath(__file__) - cls.path = os.path.dirname(cls.realpath) - - @classmethod - def tearDownClass(cls): - del cls.realpath, cls.path - - def test_invaliddir(self): - with captured_stdout() as s: - filelist = list(grep.findfiles('invaliddir', '*.*', False)) - self.assertEqual(filelist, []) - self.assertIn('invalid', s.getvalue()) - - def test_curdir(self): - # Test os.curdir. - ff = grep.findfiles - save_cwd = os.getcwd() - os.chdir(self.path) - filename = 'test_grep.py' - filelist = list(ff(os.curdir, filename, False)) - self.assertIn(os.path.join(os.curdir, filename), filelist) - os.chdir(save_cwd) - - def test_base(self): - ff = grep.findfiles - readme = os.path.join(self.path, 'README.txt') - - # Check for Python files in path where this file lives. - filelist = list(ff(self.path, '*.py', False)) - # This directory has many Python files. - self.assertGreater(len(filelist), 10) - self.assertIn(self.realpath, filelist) - self.assertNotIn(readme, filelist) - - # Look for .txt files in path where this file lives. - filelist = list(ff(self.path, '*.txt', False)) - self.assertNotEqual(len(filelist), 0) - self.assertNotIn(self.realpath, filelist) - self.assertIn(readme, filelist) - - # Look for non-matching pattern. - filelist = list(ff(self.path, 'grep.*', False)) - self.assertEqual(len(filelist), 0) - self.assertNotIn(self.realpath, filelist) - - def test_recurse(self): - ff = grep.findfiles - parent = os.path.dirname(self.path) - grepfile = os.path.join(parent, 'grep.py') - pat = '*.py' - - # Get Python files only in parent directory. - filelist = list(ff(parent, pat, False)) - parent_size = len(filelist) - # Lots of Python files in idlelib. - self.assertGreater(parent_size, 20) - self.assertIn(grepfile, filelist) - # Without subdirectories, this file isn't returned. - self.assertNotIn(self.realpath, filelist) - - # Include subdirectories. - filelist = list(ff(parent, pat, True)) - # More files found now. - self.assertGreater(len(filelist), parent_size) - self.assertIn(grepfile, filelist) - # This file exists in list now. - self.assertIn(self.realpath, filelist) - - # Check another level up the tree. - parent = os.path.dirname(parent) - filelist = list(ff(parent, '*.py', True)) - self.assertIn(self.realpath, filelist) - + # findfiles is really a function, not a method, could be iterator + # test that filename return filename + # test that idlelib has many .py files + # test that recursive flag adds idle_test .py files + pass class Grep_itTest(unittest.TestCase): # Test captured reports with 0 and some hits. @@ -121,9 +49,9 @@ class Grep_itTest(unittest.TestCase): # from incomplete replacement, so 'later'. def report(self, pat): - _grep.engine._pat = pat + grep.engine._pat = pat with captured_stdout() as s: - _grep.grep_it(re.compile(pat), __file__) + grep.grep_it(re.compile(pat), __file__) lines = s.getvalue().split('\n') lines.pop() # remove bogus '' after last \n return lines @@ -145,12 +73,10 @@ class Grep_itTest(unittest.TestCase): self.assertIn('2', lines[3]) # hits found 2 self.assertTrue(lines[4].startswith('(Hint:')) - class Default_commandTest(unittest.TestCase): - # To write this, move outwin import to top of GrepDialog + # To write this, mode OutputWindow import to top of GrepDialog # so it can be replaced by captured_stdout in class setup/teardown. pass - if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, exit=False) diff --git a/Lib/idlelib/idle_test/test_help.py b/Lib/idlelib/idle_test/test_help.py deleted file mode 100644 index b542659..0000000 --- a/Lib/idlelib/idle_test/test_help.py +++ /dev/null @@ -1,34 +0,0 @@ -"Test help, coverage 87%." - -from idlelib import help -import unittest -from test.support import requires -requires('gui') -from os.path import abspath, dirname, join -from tkinter import Tk - - -class HelpFrameTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - "By itself, this tests that file parsed without exception." - cls.root = root = Tk() - root.withdraw() - helpfile = join(dirname(dirname(abspath(__file__))), 'help.html') - cls.frame = help.HelpFrame(root, helpfile) - - @classmethod - def tearDownClass(cls): - del cls.frame - cls.root.update_idletasks() - cls.root.destroy() - del cls.root - - def test_line1(self): - text = self.frame.text - self.assertEqual(text.get('1.0', '1.end'), ' IDLE ') - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_help_about.py b/Lib/idlelib/idle_test/test_help_about.py deleted file mode 100644 index 7c148d2..0000000 --- a/Lib/idlelib/idle_test/test_help_about.py +++ /dev/null @@ -1,182 +0,0 @@ -"""Test help_about, coverage 100%. -help_about.build_bits branches on sys.platform='darwin'. -'100% combines coverage on Mac and others. -""" - -from idlelib import help_about -import unittest -from test.support import requires, findfile -from tkinter import Tk, TclError -from idlelib.idle_test.mock_idle import Func -from idlelib.idle_test.mock_tk import Mbox_func -from idlelib import textview -import os.path -from platform import python_version - -About = help_about.AboutDialog - - -class LiveDialogTest(unittest.TestCase): - """Simulate user clicking buttons other than [Close]. - - Test that invoked textview has text from source. - """ - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - cls.dialog = About(cls.root, 'About IDLE', _utest=True) - - @classmethod - def tearDownClass(cls): - del cls.dialog - cls.root.update_idletasks() - cls.root.destroy() - del cls.root - - def test_build_bits(self): - self.assertIn(help_about.build_bits(), ('32', '64')) - - def test_dialog_title(self): - """Test about dialog title""" - self.assertEqual(self.dialog.title(), 'About IDLE') - - def test_dialog_logo(self): - """Test about dialog logo.""" - path, file = os.path.split(self.dialog.icon_image['file']) - fn, ext = os.path.splitext(file) - self.assertEqual(fn, 'idle_48') - - def test_printer_buttons(self): - """Test buttons whose commands use printer function.""" - dialog = self.dialog - button_sources = [(dialog.py_license, license, 'license'), - (dialog.py_copyright, copyright, 'copyright'), - (dialog.py_credits, credits, 'credits')] - - for button, printer, name in button_sources: - with self.subTest(name=name): - printer._Printer__setup() - button.invoke() - get = dialog._current_textview.viewframe.textframe.text.get - lines = printer._Printer__lines - if len(lines) < 2: - self.fail(name + ' full text was not found') - self.assertEqual(lines[0], get('1.0', '1.end')) - self.assertEqual(lines[1], get('2.0', '2.end')) - dialog._current_textview.destroy() - - def test_file_buttons(self): - """Test buttons that display files.""" - dialog = self.dialog - button_sources = [(self.dialog.readme, 'README.txt', 'readme'), - (self.dialog.idle_news, 'NEWS.txt', 'news'), - (self.dialog.idle_credits, 'CREDITS.txt', 'credits')] - - for button, filename, name in button_sources: - with self.subTest(name=name): - button.invoke() - fn = findfile(filename, subdir='idlelib') - get = dialog._current_textview.viewframe.textframe.text.get - with open(fn, encoding='utf-8') as f: - self.assertEqual(f.readline().strip(), get('1.0', '1.end')) - f.readline() - self.assertEqual(f.readline().strip(), get('3.0', '3.end')) - dialog._current_textview.destroy() - - -class DefaultTitleTest(unittest.TestCase): - "Test default title." - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - cls.dialog = About(cls.root, _utest=True) - - @classmethod - def tearDownClass(cls): - del cls.dialog - cls.root.update_idletasks() - cls.root.destroy() - del cls.root - - def test_dialog_title(self): - """Test about dialog title""" - self.assertEqual(self.dialog.title(), - f'About IDLE {python_version()}' - f' ({help_about.build_bits()} bit)') - - -class CloseTest(unittest.TestCase): - """Simulate user clicking [Close] button""" - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - cls.dialog = About(cls.root, 'About IDLE', _utest=True) - - @classmethod - def tearDownClass(cls): - del cls.dialog - cls.root.update_idletasks() - cls.root.destroy() - del cls.root - - def test_close(self): - self.assertEqual(self.dialog.winfo_class(), 'Toplevel') - self.dialog.button_ok.invoke() - with self.assertRaises(TclError): - self.dialog.winfo_class() - - -class Dummy_about_dialog(): - # Dummy class for testing file display functions. - idle_credits = About.show_idle_credits - idle_readme = About.show_readme - idle_news = About.show_idle_news - # Called by the above - display_file_text = About.display_file_text - _utest = True - - -class DisplayFileTest(unittest.TestCase): - """Test functions that display files. - - While somewhat redundant with gui-based test_file_dialog, - these unit tests run on all buildbots, not just a few. - """ - dialog = Dummy_about_dialog() - - @classmethod - def setUpClass(cls): - cls.orig_error = textview.showerror - cls.orig_view = textview.view_text - cls.error = Mbox_func() - cls.view = Func() - textview.showerror = cls.error - textview.view_text = cls.view - - @classmethod - def tearDownClass(cls): - textview.showerror = cls.orig_error - textview.view_text = cls.orig_view - - def test_file_display(self): - for handler in (self.dialog.idle_credits, - self.dialog.idle_readme, - self.dialog.idle_news): - self.error.message = '' - self.view.called = False - with self.subTest(handler=handler): - handler() - self.assertEqual(self.error.message, '') - self.assertEqual(self.view.called, True) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_helpabout.py b/Lib/idlelib/idle_test/test_helpabout.py new file mode 100644 index 0000000..0046f87 --- /dev/null +++ b/Lib/idlelib/idle_test/test_helpabout.py @@ -0,0 +1,52 @@ +'''Test idlelib.help_about. + +Coverage: +''' +from idlelib import aboutDialog as help_about +from idlelib import textView as textview +from idlelib.idle_test.mock_idle import Func +from idlelib.idle_test.mock_tk import Mbox +import unittest + +About = help_about.AboutDialog +class Dummy_about_dialog(): + # Dummy class for testing file display functions. + idle_credits = About.ShowIDLECredits.im_func + idle_readme = About.ShowIDLEAbout.im_func + idle_news = About.ShowIDLENEWS.im_func + # Called by the above + display_file_text = About.display_file_text.im_func + + +class DisplayFileTest(unittest.TestCase): + "Test that .txt files are found and properly decoded." + dialog = Dummy_about_dialog() + + @classmethod + def setUpClass(cls): + cls.orig_mbox = textview.tkMessageBox + cls.orig_view = textview.view_text + cls.mbox = Mbox() + cls.view = Func() + textview.tkMessageBox = cls.mbox + textview.view_text = cls.view + cls.About = Dummy_about_dialog() + + @classmethod + def tearDownClass(cls): + textview.tkMessageBox = cls.orig_mbox + textview.view_text = cls.orig_view.im_func + + def test_file_isplay(self): + for handler in (self.dialog.idle_credits, + self.dialog.idle_readme, + self.dialog.idle_news): + self.mbox.showerror.message = '' + self.view.called = False + handler() + self.assertEqual(self.mbox.showerror.message, '') + self.assertEqual(self.view.called, True) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_hyperparser.py b/Lib/idlelib/idle_test/test_hyperparser.py index 343843c..0a1809d 100644 --- a/Lib/idlelib/idle_test/test_hyperparser.py +++ b/Lib/idlelib/idle_test/test_hyperparser.py @@ -1,21 +1,20 @@ -"Test hyperparser, coverage 98%." - -from idlelib.hyperparser import HyperParser +"""Unittest for idlelib.HyperParser""" import unittest -from test.support import requires -from tkinter import Tk, Text -from idlelib.editor import EditorWindow +from test.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.prompt_last_line = '>>>' + 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 + _build_char_in_string_func = EditorWindow._build_char_in_string_func.im_func + is_char_in_string = EditorWindow.is_char_in_string.im_func class HyperParserTest(unittest.TestCase): @@ -31,7 +30,6 @@ class HyperParserTest(unittest.TestCase): "z = ((r'asdf')+('a')))\n" '[x for x in\n' 'for = False\n' - 'cliché = "this is a string with unicode, what a cliché"' ) @classmethod @@ -53,7 +51,7 @@ class HyperParserTest(unittest.TestCase): def tearDown(self): self.text.delete('1.0', 'end') - self.editwin.prompt_last_line = '>>>' + self.editwin.context_use_ps1 = True def get_parser(self, index): """ @@ -71,7 +69,7 @@ class HyperParserTest(unittest.TestCase): self.assertIn('precedes', str(ve.exception)) # test without ps1 - self.editwin.prompt_last_line = '' + self.editwin.context_use_ps1 = False # number of lines lesser than 50 p = self.get_parser('end') @@ -96,8 +94,6 @@ class HyperParserTest(unittest.TestCase): 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 @@ -185,92 +181,12 @@ class HyperParserTest(unittest.TestCase): 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_history.py b/Lib/idlelib/idle_test/test_idlehistory.py index 6753965..b076757 100644 --- a/Lib/idlelib/idle_test/test_history.py +++ b/Lib/idlelib/idle_test/test_idlehistory.py @@ -1,18 +1,15 @@ -" Test history, coverage 100%." - -from idlelib.history import History import unittest -from test.support import requires +from test.test_support import requires -import tkinter as tk -from tkinter import Text as tkText +import Tkinter as tk +from Tkinter import Text as tkText from idlelib.idle_test.mock_tk import Text as mkText -from idlelib.config import idleConf +from idlelib.IdleHistory import History +from idlelib.configHandler import idleConf line1 = 'a = 7' line2 = 'b = a' - class StoreTest(unittest.TestCase): '''Tests History.__init__ and History.store with mock Text''' @@ -64,7 +61,6 @@ class TextWrapper: def bell(self): self._bell = True - class FetchTest(unittest.TestCase): '''Test History.fetch with wrapped tk.Text. ''' @@ -87,7 +83,7 @@ class FetchTest(unittest.TestCase): cls.root.destroy() del cls.root - def fetch_test(self, reverse, line, prefix, index, *, bell=False): + def fetch_test(self, reverse, line, prefix, index, bell=False): # Perform one fetch as invoked by Alt-N or Alt-P # Test the result. The line test is the most important. # The last two are diagnostic of fetch internals. diff --git a/Lib/idlelib/idle_test/test_run.py b/Lib/idlelib/idle_test/test_io.py index 9995dbe..ee017bb 100644 --- a/Lib/idlelib/idle_test/test_run.py +++ b/Lib/idlelib/idle_test/test_io.py @@ -1,44 +1,10 @@ -"Test run, coverage 42%." - -from idlelib import run import unittest -from unittest import mock -from test.support import captured_stderr - import io -import sys - - -class RunTest(unittest.TestCase): - - def test_print_exception_unhashable(self): - class UnhashableException(Exception): - def __eq__(self, other): - return True - - ex1 = UnhashableException('ex1') - ex2 = UnhashableException('ex2') - try: - raise ex2 from ex1 - except UnhashableException: - try: - raise ex1 - except UnhashableException: - with captured_stderr() as output: - with mock.patch.object(run, - 'cleanup_traceback') as ct: - ct.side_effect = lambda t, e: t - run.print_exception() +from idlelib.PyShell import PseudoInputFile, PseudoOutputFile +from test import test_support as support - tb = output.getvalue().strip().splitlines() - self.assertEqual(11, len(tb)) - self.assertIn('UnhashableException: ex2', tb[3]) - self.assertIn('UnhashableException: ex1', tb[10]) - -# StdioFile tests. - -class S(str): +class Base(object): def __str__(self): return '%s:str' % type(self).__name__ def __unicode__(self): @@ -52,30 +18,153 @@ class S(str): def __getslice__(self, *args): return '%s:slice' % type(self).__name__ +class S(Base, str): + pass + +class U(Base, unicode): + pass + +class BA(Base, bytearray): + pass 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 StdInputFilesTest(unittest.TestCase): +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(IOError, f.fileno) + self.assertRaises(IOError, f.tell) + self.assertRaises(IOError, f.seek, 0) + self.assertRaises(IOError, f.read, 0) + self.assertRaises(IOError, 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(u't\xe8st') + self.assertEqual(shell.written, [(u'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() + f.write(BA('t\xe8st')) + self.assertEqual(shell.written, [('t\xe8st', 'stdout')]) + self.assertEqual(type(shell.written[0][0]), str) + shell.reset() + f.write(U(u't\xe8st')) + self.assertEqual(shell.written, [(u't\xe8st', 'stdout')]) + self.assertEqual(type(shell.written[0][0]), unicode) + shell.reset() + + self.assertRaises(TypeError, f.write) + self.assertEqual(shell.written, []) + 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([u'on\xe8\n', u'tw\xf2']) + self.assertEqual(shell.written, + [(u'on\xe8\n', 'stdout'), (u'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() + f.writelines([BA('t\xe8st')]) + self.assertEqual(shell.written, [('t\xe8st', 'stdout')]) + self.assertEqual(type(shell.written[0][0]), str) + shell.reset() + f.writelines([U(u't\xe8st')]) + self.assertEqual(shell.written, [(u't\xe8st', 'stdout')]) + self.assertEqual(type(shell.written[0][0]), unicode) + 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, [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 = run.StdInputFile(shell, 'stdin') + f = PseudoInputFile(shell, 'stdin', 'utf-8') self.assertIsInstance(f, io.TextIOBase) self.assertEqual(f.encoding, 'utf-8') - self.assertEqual(f.errors, 'strict') + self.assertIsNone(f.errors) self.assertIsNone(f.newlines) self.assertEqual(f.name, '<stdin>') self.assertFalse(f.closed) @@ -86,16 +175,16 @@ class StdInputFilesTest(unittest.TestCase): def test_unsupported(self): shell = MockShell() - f = run.StdInputFile(shell, 'stdin') - 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']) + f = PseudoInputFile(shell, 'stdin', 'utf-8') + self.assertRaises(IOError, f.fileno) + self.assertRaises(IOError, f.tell) + self.assertRaises(IOError, f.seek, 0) + self.assertRaises(IOError, f.write, 'x') + self.assertRaises(IOError, f.writelines, ['x']) def test_read(self): shell = MockShell() - f = run.StdInputFile(shell, 'stdin') + 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', '']) @@ -115,7 +204,7 @@ class StdInputFilesTest(unittest.TestCase): def test_readline(self): shell = MockShell() - f = run.StdInputFile(shell, 'stdin') + 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') @@ -140,7 +229,7 @@ class StdInputFilesTest(unittest.TestCase): def test_readlines(self): shell = MockShell() - f = run.StdInputFile(shell, 'stdin') + 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', '']) @@ -161,7 +250,7 @@ class StdInputFilesTest(unittest.TestCase): def test_close(self): shell = MockShell() - f = run.StdInputFile(shell, 'stdin') + f = PseudoInputFile(shell, 'stdin', 'utf-8') shell.push(['one\n', 'two\n', '']) self.assertFalse(f.closed) self.assertEqual(f.readline(), 'one\n') @@ -171,155 +260,8 @@ class StdInputFilesTest(unittest.TestCase): self.assertRaises(TypeError, f.close, 1) -class StdOutputFilesTest(unittest.TestCase): - - def test_misc(self): - shell = MockShell() - f = run.StdOutputFile(shell, 'stdout') - self.assertIsInstance(f, io.TextIOBase) - self.assertEqual(f.encoding, 'utf-8') - self.assertEqual(f.errors, 'strict') - 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 = run.StdOutputFile(shell, 'stdout') - 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 = run.StdOutputFile(shell, 'stdout') - f.write('test') - self.assertEqual(shell.written, [('test', 'stdout')]) - shell.reset() - f.write('t\xe8\u015b\U0001d599') - self.assertEqual(shell.written, [('t\xe8\u015b\U0001d599', 'stdout')]) - shell.reset() - - f.write(S('t\xe8\u015b\U0001d599')) - self.assertEqual(shell.written, [('t\xe8\u015b\U0001d599', '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_write_stderr_nonencodable(self): - shell = MockShell() - f = run.StdOutputFile(shell, 'stderr', 'iso-8859-15', 'backslashreplace') - f.write('t\xe8\u015b\U0001d599\xa4') - self.assertEqual(shell.written, [('t\xe8\\u015b\\U0001d599\\xa4', 'stderr')]) - shell.reset() - - f.write(S('t\xe8\u015b\U0001d599\xa4')) - self.assertEqual(shell.written, [('t\xe8\\u015b\\U0001d599\\xa4', 'stderr')]) - 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 = run.StdOutputFile(shell, 'stdout') - 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 = run.StdOutputFile(shell, 'stdout') - 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 TestSysRecursionLimitWrappers(unittest.TestCase): - - def test_bad_setrecursionlimit_calls(self): - run.install_recursionlimit_wrappers() - self.addCleanup(run.uninstall_recursionlimit_wrappers) - f = sys.setrecursionlimit - self.assertRaises(TypeError, f, limit=100) - self.assertRaises(TypeError, f, 100, 1000) - self.assertRaises(ValueError, f, 0) - - def test_roundtrip(self): - run.install_recursionlimit_wrappers() - self.addCleanup(run.uninstall_recursionlimit_wrappers) - - # check that setting the recursion limit works - orig_reclimit = sys.getrecursionlimit() - self.addCleanup(sys.setrecursionlimit, orig_reclimit) - sys.setrecursionlimit(orig_reclimit + 3) - - # check that the new limit is returned by sys.getrecursionlimit() - new_reclimit = sys.getrecursionlimit() - self.assertEqual(new_reclimit, orig_reclimit + 3) - - def test_default_recursion_limit_preserved(self): - orig_reclimit = sys.getrecursionlimit() - run.install_recursionlimit_wrappers() - self.addCleanup(run.uninstall_recursionlimit_wrappers) - new_reclimit = sys.getrecursionlimit() - self.assertEqual(new_reclimit, orig_reclimit) - - def test_fixdoc(self): - def func(): "docstring" - run.fixdoc(func, "more") - self.assertEqual(func.__doc__, "docstring\n\nmore") - func.__doc__ = None - run.fixdoc(func, "more") - self.assertEqual(func.__doc__, "more") - +def test_main(): + support.run_unittest(PseudeOutputFilesTest, PseudeInputFilesTest) if __name__ == '__main__': - unittest.main(verbosity=2) + test_main() diff --git a/Lib/idlelib/idle_test/test_iomenu.py b/Lib/idlelib/idle_test/test_iomenu.py deleted file mode 100644 index 99f4048..0000000 --- a/Lib/idlelib/idle_test/test_iomenu.py +++ /dev/null @@ -1,49 +0,0 @@ -"Test , coverage 17%." - -from idlelib import iomenu -import unittest -from test.support import requires -from tkinter import Tk -from idlelib.editor import EditorWindow - - -class IOBindingTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - cls.editwin = EditorWindow(root=cls.root) - cls.io = iomenu.IOBinding(cls.editwin) - - @classmethod - def tearDownClass(cls): - cls.io.close() - cls.editwin._close() - del cls.editwin - cls.root.update_idletasks() - for id in cls.root.tk.call('after', 'info'): - cls.root.after_cancel(id) # Need for EditorWindow. - cls.root.destroy() - del cls.root - - def test_init(self): - self.assertIs(self.io.editwin, self.editwin) - - def test_fixnewlines_end(self): - eq = self.assertEqual - io = self.io - fix = io.fixnewlines - text = io.editwin.text - self.editwin.interp = None - eq(fix(), '') - del self.editwin.interp - text.insert(1.0, 'a') - eq(fix(), 'a'+io.eol_convention) - eq(text.get('1.0', 'end-1c'), 'a\n') - eq(fix(), 'a'+io.eol_convention) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_macosx.py b/Lib/idlelib/idle_test/test_macosx.py deleted file mode 100644 index b6bd922..0000000 --- a/Lib/idlelib/idle_test/test_macosx.py +++ /dev/null @@ -1,104 +0,0 @@ -"Test macosx, coverage 45% on Windows." - -from idlelib import macosx -import unittest -from test.support import requires -import tkinter as tk -import unittest.mock as mock -from idlelib.filelist import FileList - -mactypes = {'carbon', 'cocoa', 'xquartz'} -nontypes = {'other'} -alltypes = mactypes | nontypes - - -class InitTktypeTest(unittest.TestCase): - "Test _init_tk_type." - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = tk.Tk() - cls.root.withdraw() - cls.orig_platform = macosx.platform - - @classmethod - def tearDownClass(cls): - cls.root.update_idletasks() - cls.root.destroy() - del cls.root - macosx.platform = cls.orig_platform - - def test_init_sets_tktype(self): - "Test that _init_tk_type sets _tk_type according to platform." - for platform, types in ('darwin', alltypes), ('other', nontypes): - with self.subTest(platform=platform): - macosx.platform = platform - macosx._tk_type == None - macosx._init_tk_type() - self.assertIn(macosx._tk_type, types) - - -class IsTypeTkTest(unittest.TestCase): - "Test each of the four isTypeTk predecates." - isfuncs = ((macosx.isAquaTk, ('carbon', 'cocoa')), - (macosx.isCarbonTk, ('carbon')), - (macosx.isCocoaTk, ('cocoa')), - (macosx.isXQuartz, ('xquartz')), - ) - - @mock.patch('idlelib.macosx._init_tk_type') - def test_is_calls_init(self, mockinit): - "Test that each isTypeTk calls _init_tk_type when _tk_type is None." - macosx._tk_type = None - for func, whentrue in self.isfuncs: - with self.subTest(func=func): - func() - self.assertTrue(mockinit.called) - mockinit.reset_mock() - - def test_isfuncs(self): - "Test that each isTypeTk return correct bool." - for func, whentrue in self.isfuncs: - for tktype in alltypes: - with self.subTest(func=func, whentrue=whentrue, tktype=tktype): - macosx._tk_type = tktype - (self.assertTrue if tktype in whentrue else self.assertFalse)\ - (func()) - - -class SetupTest(unittest.TestCase): - "Test setupApp." - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = tk.Tk() - cls.root.withdraw() - def cmd(tkpath, func): - assert isinstance(tkpath, str) - assert isinstance(func, type(cmd)) - cls.root.createcommand = cmd - - @classmethod - def tearDownClass(cls): - cls.root.update_idletasks() - cls.root.destroy() - del cls.root - - @mock.patch('idlelib.macosx.overrideRootMenu') #27312 - def test_setupapp(self, overrideRootMenu): - "Call setupApp with each possible graphics type." - root = self.root - flist = FileList(root) - for tktype in alltypes: - with self.subTest(tktype=tktype): - macosx._tk_type = tktype - macosx.setupApp(root, flist) - if tktype in ('carbon', 'cocoa'): - self.assertTrue(overrideRootMenu.called) - overrideRootMenu.reset_mock() - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_mainmenu.py b/Lib/idlelib/idle_test/test_mainmenu.py deleted file mode 100644 index 7ec0368..0000000 --- a/Lib/idlelib/idle_test/test_mainmenu.py +++ /dev/null @@ -1,21 +0,0 @@ -"Test mainmenu, coverage 100%." -# Reported as 88%; mocking turtledemo absence would have no point. - -from idlelib import mainmenu -import unittest - - -class MainMenuTest(unittest.TestCase): - - def test_menudefs(self): - actual = [item[0] for item in mainmenu.menudefs] - expect = ['file', 'edit', 'format', 'run', 'shell', - 'debug', 'options', 'window', 'help'] - self.assertEqual(actual, expect) - - def test_default_keydefs(self): - self.assertGreaterEqual(len(mainmenu.default_keydefs), 50) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_multicall.py b/Lib/idlelib/idle_test/test_multicall.py deleted file mode 100644 index ba582bb..0000000 --- a/Lib/idlelib/idle_test/test_multicall.py +++ /dev/null @@ -1,48 +0,0 @@ -"Test multicall, coverage 33%." - -from idlelib import multicall -import unittest -from test.support import requires -from tkinter import Tk, Text - - -class MultiCallTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - cls.mc = multicall.MultiCallCreator(Text) - - @classmethod - def tearDownClass(cls): - del cls.mc - cls.root.update_idletasks() -## for id in cls.root.tk.call('after', 'info'): -## cls.root.after_cancel(id) # Need for EditorWindow. - cls.root.destroy() - del cls.root - - def test_creator(self): - mc = self.mc - self.assertIs(multicall._multicall_dict[Text], mc) - self.assertTrue(issubclass(mc, Text)) - mc2 = multicall.MultiCallCreator(Text) - self.assertIs(mc, mc2) - - def test_init(self): - mctext = self.mc(self.root) - self.assertIsInstance(mctext._MultiCall__binders, list) - - def test_yview(self): - # Added for tree.wheel_event - # (it depends on yview to not be overriden) - mc = self.mc - self.assertIs(mc.yview, Text.yview) - mctext = self.mc(self.root) - self.assertIs(mctext.yview.__func__, Text.yview) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_outwin.py b/Lib/idlelib/idle_test/test_outwin.py deleted file mode 100644 index cd099ec..0000000 --- a/Lib/idlelib/idle_test/test_outwin.py +++ /dev/null @@ -1,171 +0,0 @@ -"Test outwin, coverage 76%." - -from idlelib import outwin -import unittest -from test.support import requires -from tkinter import Tk, Text -from idlelib.idle_test.mock_tk import Mbox_func -from idlelib.idle_test.mock_idle import Func -from unittest import mock - - -class OutputWindowTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - root = cls.root = Tk() - root.withdraw() - w = cls.window = outwin.OutputWindow(None, None, None, root) - cls.text = w.text = Text(root) - - @classmethod - def tearDownClass(cls): - cls.window.close() - del cls.text, cls.window - cls.root.destroy() - del cls.root - - def setUp(self): - self.text.delete('1.0', 'end') - - def test_ispythonsource(self): - # OutputWindow overrides ispythonsource to always return False. - w = self.window - self.assertFalse(w.ispythonsource('test.txt')) - self.assertFalse(w.ispythonsource(__file__)) - - def test_window_title(self): - self.assertEqual(self.window.top.title(), 'Output') - - def test_maybesave(self): - w = self.window - eq = self.assertEqual - w.get_saved = Func() - - w.get_saved.result = False - eq(w.maybesave(), 'no') - eq(w.get_saved.called, 1) - - w.get_saved.result = True - eq(w.maybesave(), 'yes') - eq(w.get_saved.called, 2) - del w.get_saved - - def test_write(self): - eq = self.assertEqual - delete = self.text.delete - get = self.text.get - write = self.window.write - - # Test bytes. - b = b'Test bytes.' - eq(write(b), len(b)) - eq(get('1.0', '1.end'), b.decode()) - - # No new line - insert stays on same line. - delete('1.0', 'end') - test_text = 'test text' - eq(write(test_text), len(test_text)) - eq(get('1.0', '1.end'), 'test text') - eq(get('insert linestart', 'insert lineend'), 'test text') - - # New line - insert moves to next line. - delete('1.0', 'end') - test_text = 'test text\n' - eq(write(test_text), len(test_text)) - eq(get('1.0', '1.end'), 'test text') - eq(get('insert linestart', 'insert lineend'), '') - - # Text after new line is tagged for second line of Text widget. - delete('1.0', 'end') - test_text = 'test text\nLine 2' - eq(write(test_text), len(test_text)) - eq(get('1.0', '1.end'), 'test text') - eq(get('2.0', '2.end'), 'Line 2') - eq(get('insert linestart', 'insert lineend'), 'Line 2') - - # Test tags. - delete('1.0', 'end') - test_text = 'test text\n' - test_text2 = 'Line 2\n' - eq(write(test_text, tags='mytag'), len(test_text)) - eq(write(test_text2, tags='secondtag'), len(test_text2)) - eq(get('mytag.first', 'mytag.last'), test_text) - eq(get('secondtag.first', 'secondtag.last'), test_text2) - eq(get('1.0', '1.end'), test_text.rstrip('\n')) - eq(get('2.0', '2.end'), test_text2.rstrip('\n')) - - def test_writelines(self): - eq = self.assertEqual - get = self.text.get - writelines = self.window.writelines - - writelines(('Line 1\n', 'Line 2\n', 'Line 3\n')) - eq(get('1.0', '1.end'), 'Line 1') - eq(get('2.0', '2.end'), 'Line 2') - eq(get('3.0', '3.end'), 'Line 3') - eq(get('insert linestart', 'insert lineend'), '') - - def test_goto_file_line(self): - eq = self.assertEqual - w = self.window - text = self.text - - w.flist = mock.Mock() - gfl = w.flist.gotofileline = Func() - showerror = w.showerror = Mbox_func() - - # No file/line number. - w.write('Not a file line') - self.assertIsNone(w.goto_file_line()) - eq(gfl.called, 0) - eq(showerror.title, 'No special line') - - # Current file/line number. - w.write(f'{str(__file__)}: 42: spam\n') - w.write(f'{str(__file__)}: 21: spam') - self.assertIsNone(w.goto_file_line()) - eq(gfl.args, (str(__file__), 21)) - - # Previous line has file/line number. - text.delete('1.0', 'end') - w.write(f'{str(__file__)}: 42: spam\n') - w.write('Not a file line') - self.assertIsNone(w.goto_file_line()) - eq(gfl.args, (str(__file__), 42)) - - del w.flist.gotofileline, w.showerror - - -class ModuleFunctionTest(unittest.TestCase): - - @classmethod - def setUp(cls): - outwin.file_line_progs = None - - def test_compile_progs(self): - outwin.compile_progs() - for pat, regex in zip(outwin.file_line_pats, outwin.file_line_progs): - self.assertEqual(regex.pattern, pat) - - @mock.patch('builtins.open') - def test_file_line_helper(self, mock_open): - flh = outwin.file_line_helper - test_lines = ( - (r'foo file "testfile1", line 42, bar', ('testfile1', 42)), - (r'foo testfile2(21) bar', ('testfile2', 21)), - (r' testfile3 : 42: foo bar\n', (' testfile3 ', 42)), - (r'foo testfile4.py :1: ', ('foo testfile4.py ', 1)), - ('testfile5: \u19D4\u19D2: ', ('testfile5', 42)), - (r'testfile6: 42', None), # only one `:` - (r'testfile7 42 text', None) # no separators - ) - for line, expected_output in test_lines: - self.assertEqual(flh(line), expected_output) - if expected_output: - mock_open.assert_called_with(expected_output[0], 'r') - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_parenmatch.py b/Lib/idlelib/idle_test/test_parenmatch.py index 4a41d84..1621981 100644 --- a/Lib/idlelib/idle_test/test_parenmatch.py +++ b/Lib/idlelib/idle_test/test_parenmatch.py @@ -1,31 +1,39 @@ -"""Test parenmatch, coverage 91%. - -This must currently be a gui test because ParenMatch methods use -several text methods not defined on idlelib.idle_test.mock_tk.Text. -""" -from idlelib.parenmatch import ParenMatch -from test.support import requires -requires('gui') +"""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 tkinter import Tk, Text +from test.test_support import requires +from Tkinter import Tk, Text +from idlelib.ParenMatch import ParenMatch + +class Mock: # 2.7 does not have unittest.mock + def __init__(self, *args, **kwargs): + self.called = False + + def __call__(self, *args, **kwargs): + self.called = True + def reset_mock(self, *args, **kwargs): + self.called = False + + def after(self, *args, **kwargs): + pass class DummyEditwin: def __init__(self, text): self.text = text self.indentwidth = 8 self.tabwidth = 8 - self.prompt_last_line = '>>>' # Currently not used by parenmatch. + self.context_use_ps1 = True class ParenMatchTest(unittest.TestCase): @classmethod def setUpClass(cls): + requires('gui') cls.root = Tk() - cls.root.withdraw() cls.text = Text(cls.root) cls.editwin = DummyEditwin(cls.text) cls.editwin.text_frame = Mock() @@ -33,51 +41,52 @@ class ParenMatchTest(unittest.TestCase): @classmethod def tearDownClass(cls): del cls.text, cls.editwin - cls.root.update_idletasks() cls.root.destroy() del cls.root def tearDown(self): self.text.delete('1.0', 'end') - def get_parenmatch(self): + def test_paren_expression(self): + """ + Test ParenMatch with 'expression' style. + """ + text = self.text pm = ParenMatch(self.editwin) - pm.bell = lambda: None - return pm + 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_styles(self): + def test_paren_default(self): """ - Test ParenMatch with each style. + Test ParenMatch with 'default' style. """ text = self.text - pm = self.get_parenmatch() - for style, range1, range2 in ( - ('opener', ('1.10', '1.11'), ('1.10', '1.11')), - ('default',('1.10', '1.11'),('1.10', '1.11')), - ('parens', ('1.14', '1.15'), ('1.15', '1.16')), - ('expression', ('1.10', '1.15'), ('1.10', '1.16'))): - with self.subTest(style=style): - text.delete('1.0', 'end') - pm.STYLE = style - text.insert('insert', 'def foobar(a, b') - - pm.flash_paren_event('event') - self.assertIn('<<parenmatch-check-restore>>', text.event_info()) - if style == 'parens': - self.assertTupleEqual(text.tag_nextrange('paren', '1.0'), - ('1.10', '1.11')) - self.assertTupleEqual( - text.tag_prevrange('paren', 'end'), range1) - - text.insert('insert', ')') - pm.restore_event() - self.assertNotIn('<<parenmatch-check-restore>>', - text.event_info()) - self.assertEqual(text.tag_prevrange('paren', 'end'), ()) - - pm.paren_closed_event('event') - self.assertTupleEqual( - text.tag_prevrange('paren', 'end'), range2) + 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): """ @@ -86,20 +95,20 @@ class ParenMatchTest(unittest.TestCase): These cases force conditional expression and alternate paths. """ text = self.text - pm = self.get_parenmatch() + pm = ParenMatch(self.editwin) text.insert('insert', '# this is a commen)') - pm.paren_closed_event('event') + self.assertIsNone(pm.paren_closed_event('event')) text.insert('insert', '\ndef') - pm.flash_paren_event('event') - pm.paren_closed_event('event') + self.assertIsNone(pm.flash_paren_event('event')) + self.assertIsNone(pm.paren_closed_event('event')) text.insert('insert', ' a, *arg)') - pm.paren_closed_event('event') + self.assertIsNone(pm.paren_closed_event('event')) def test_handle_restore_timer(self): - pm = self.get_parenmatch() + pm = ParenMatch(self.editwin) pm.restore_event = Mock() pm.handle_restore_timer(0) self.assertTrue(pm.restore_event.called) diff --git a/Lib/idlelib/idle_test/test_pathbrowser.py b/Lib/idlelib/idle_test/test_pathbrowser.py index 13d8b9e..f028414 100644 --- a/Lib/idlelib/idle_test/test_pathbrowser.py +++ b/Lib/idlelib/idle_test/test_pathbrowser.py @@ -1,69 +1,14 @@ -"Test pathbrowser, coverage 95%." - -from idlelib import pathbrowser import unittest -from test.support import requires -from tkinter import Tk - -import os.path -import pyclbr # for _modules -import sys # for sys.path - -from idlelib.idle_test.mock_idle import Func -import idlelib # for __file__ -from idlelib import browser -from idlelib.tree import TreeNode - +import os +import sys +import idlelib +from idlelib import PathBrowser class PathBrowserTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - cls.pb = pathbrowser.PathBrowser(cls.root, _utest=True) - - @classmethod - def tearDownClass(cls): - cls.pb.close() - cls.root.update_idletasks() - cls.root.destroy() - del cls.root, cls.pb - - def test_init(self): - pb = self.pb - eq = self.assertEqual - eq(pb.master, self.root) - eq(pyclbr._modules, {}) - self.assertIsInstance(pb.node, TreeNode) - self.assertIsNotNone(browser.file_open) - - def test_settitle(self): - pb = self.pb - self.assertEqual(pb.top.title(), 'Path Browser') - self.assertEqual(pb.top.iconname(), 'Path Browser') - - def test_rootnode(self): - pb = self.pb - rn = pb.rootnode() - self.assertIsInstance(rn, pathbrowser.PathBrowserTreeItem) - - def test_close(self): - pb = self.pb - pb.top.destroy = Func() - pb.node.destroy = Func() - pb.close() - self.assertTrue(pb.top.destroy.called) - self.assertTrue(pb.node.destroy.called) - del pb.top.destroy, pb.node.destroy - - -class DirBrowserTreeItemTest(unittest.TestCase): - def test_DirBrowserTreeItem(self): # Issue16226 - make sure that getting a sublist works - d = pathbrowser.DirBrowserTreeItem('') + d = PathBrowser.DirBrowserTreeItem('') d.GetSubList() self.assertEqual('', d.GetText()) @@ -71,16 +16,13 @@ class DirBrowserTreeItemTest(unittest.TestCase): self.assertEqual(d.ispackagedir(dir), True) self.assertEqual(d.ispackagedir(dir + '/Icons'), False) - -class PathBrowserTreeItemTest(unittest.TestCase): - def test_PathBrowserTreeItem(self): - p = pathbrowser.PathBrowserTreeItem() + 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) - + # Following fails in 2.7 because old-style class + #self.assertEqual(type(sub[0]), PathBrowser.DirBrowserTreeItem) if __name__ == '__main__': unittest.main(verbosity=2, exit=False) diff --git a/Lib/idlelib/idle_test/test_percolator.py b/Lib/idlelib/idle_test/test_percolator.py deleted file mode 100644 index 17668cc..0000000 --- a/Lib/idlelib/idle_test/test_percolator.py +++ /dev/null @@ -1,118 +0,0 @@ -"Test percolator, coverage 100%." - -from idlelib.percolator import Percolator, Delegator -import unittest -from test.support import requires -requires('gui') -from tkinter import Text, Tk, END - - -class MyFilter(Delegator): - def __init__(self): - Delegator.__init__(self, None) - - def insert(self, *args): - self.insert_called_with = args - self.delegate.insert(*args) - - def delete(self, *args): - self.delete_called_with = args - self.delegate.delete(*args) - - def uppercase_insert(self, index, chars, tags=None): - chars = chars.upper() - self.delegate.insert(index, chars) - - def lowercase_insert(self, index, chars, tags=None): - chars = chars.lower() - self.delegate.insert(index, chars) - - def dont_insert(self, index, chars, tags=None): - pass - - -class PercolatorTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.root = Tk() - cls.text = Text(cls.root) - - @classmethod - def tearDownClass(cls): - del cls.text - cls.root.destroy() - del cls.root - - def setUp(self): - self.percolator = Percolator(self.text) - self.filter_one = MyFilter() - self.filter_two = MyFilter() - self.percolator.insertfilter(self.filter_one) - self.percolator.insertfilter(self.filter_two) - - def tearDown(self): - self.percolator.close() - self.text.delete('1.0', END) - - def test_insertfilter(self): - self.assertIsNotNone(self.filter_one.delegate) - self.assertEqual(self.percolator.top, self.filter_two) - self.assertEqual(self.filter_two.delegate, self.filter_one) - self.assertEqual(self.filter_one.delegate, self.percolator.bottom) - - def test_removefilter(self): - filter_three = MyFilter() - self.percolator.removefilter(self.filter_two) - self.assertEqual(self.percolator.top, self.filter_one) - self.assertIsNone(self.filter_two.delegate) - - filter_three = MyFilter() - self.percolator.insertfilter(self.filter_two) - self.percolator.insertfilter(filter_three) - self.percolator.removefilter(self.filter_one) - self.assertEqual(self.percolator.top, filter_three) - self.assertEqual(filter_three.delegate, self.filter_two) - self.assertEqual(self.filter_two.delegate, self.percolator.bottom) - self.assertIsNone(self.filter_one.delegate) - - def test_insert(self): - self.text.insert('insert', 'foo') - self.assertEqual(self.text.get('1.0', END), 'foo\n') - self.assertTupleEqual(self.filter_one.insert_called_with, - ('insert', 'foo', None)) - - def test_modify_insert(self): - self.filter_one.insert = self.filter_one.uppercase_insert - self.text.insert('insert', 'bAr') - self.assertEqual(self.text.get('1.0', END), 'BAR\n') - - def test_modify_chain_insert(self): - filter_three = MyFilter() - self.percolator.insertfilter(filter_three) - self.filter_two.insert = self.filter_two.uppercase_insert - self.filter_one.insert = self.filter_one.lowercase_insert - self.text.insert('insert', 'BaR') - self.assertEqual(self.text.get('1.0', END), 'bar\n') - - def test_dont_insert(self): - self.filter_one.insert = self.filter_one.dont_insert - self.text.insert('insert', 'foo bar') - self.assertEqual(self.text.get('1.0', END), '\n') - self.filter_one.insert = self.filter_one.dont_insert - self.text.insert('insert', 'foo bar') - self.assertEqual(self.text.get('1.0', END), '\n') - - def test_without_filter(self): - self.text.insert('insert', 'hello') - self.assertEqual(self.text.get('1.0', 'end'), 'hello\n') - - def test_delete(self): - self.text.insert('insert', 'foo') - self.text.delete('1.0', '1.2') - self.assertEqual(self.text.get('1.0', END), 'o\n') - self.assertTupleEqual(self.filter_one.delete_called_with, - ('1.0', '1.2')) - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_pyparse.py b/Lib/idlelib/idle_test/test_pyparse.py deleted file mode 100644 index f7154e6..0000000 --- a/Lib/idlelib/idle_test/test_pyparse.py +++ /dev/null @@ -1,466 +0,0 @@ -"Test pyparse, coverage 96%." - -from idlelib import pyparse -import unittest -from collections import namedtuple - - -class ParseMapTest(unittest.TestCase): - - def test_parsemap(self): - keepwhite = {ord(c): ord(c) for c in ' \t\n\r'} - mapping = pyparse.ParseMap(keepwhite) - self.assertEqual(mapping[ord('\t')], ord('\t')) - self.assertEqual(mapping[ord('a')], ord('x')) - self.assertEqual(mapping[1000], ord('x')) - - def test_trans(self): - # trans is the production instance of ParseMap, used in _study1 - parser = pyparse.Parser(4, 4) - self.assertEqual('\t a([{b}])b"c\'d\n'.translate(pyparse.trans), - 'xxx(((x)))x"x\'x\n') - - -class PyParseTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.parser = pyparse.Parser(indentwidth=4, tabwidth=4) - - @classmethod - def tearDownClass(cls): - del cls.parser - - def test_init(self): - self.assertEqual(self.parser.indentwidth, 4) - self.assertEqual(self.parser.tabwidth, 4) - - def test_set_code(self): - eq = self.assertEqual - p = self.parser - setcode = p.set_code - - # Not empty and doesn't end with newline. - with self.assertRaises(AssertionError): - setcode('a') - - tests = ('', - 'a\n') - - for string in tests: - with self.subTest(string=string): - setcode(string) - eq(p.code, string) - eq(p.study_level, 0) - - def test_find_good_parse_start(self): - eq = self.assertEqual - p = self.parser - setcode = p.set_code - start = p.find_good_parse_start - - # Split def across lines. - setcode('"""This is a module docstring"""\n' - 'class C():\n' - ' def __init__(self, a,\n' - ' b=True):\n' - ' pass\n' - ) - - # No value sent for is_char_in_string(). - self.assertIsNone(start()) - - # Make text look like a string. This returns pos as the start - # position, but it's set to None. - self.assertIsNone(start(is_char_in_string=lambda index: True)) - - # Make all text look like it's not in a string. This means that it - # found a good start position. - eq(start(is_char_in_string=lambda index: False), 44) - - # If the beginning of the def line is not in a string, then it - # returns that as the index. - eq(start(is_char_in_string=lambda index: index > 44), 44) - # If the beginning of the def line is in a string, then it - # looks for a previous index. - eq(start(is_char_in_string=lambda index: index >= 44), 33) - # If everything before the 'def' is in a string, then returns None. - # The non-continuation def line returns 44 (see below). - eq(start(is_char_in_string=lambda index: index < 44), None) - - # Code without extra line break in def line - mostly returns the same - # values. - setcode('"""This is a module docstring"""\n' - 'class C():\n' - ' def __init__(self, a, b=True):\n' - ' pass\n' - ) - eq(start(is_char_in_string=lambda index: False), 44) - eq(start(is_char_in_string=lambda index: index > 44), 44) - eq(start(is_char_in_string=lambda index: index >= 44), 33) - # When the def line isn't split, this returns which doesn't match the - # split line test. - eq(start(is_char_in_string=lambda index: index < 44), 44) - - def test_set_lo(self): - code = ( - '"""This is a module docstring"""\n' - 'class C():\n' - ' def __init__(self, a,\n' - ' b=True):\n' - ' pass\n' - ) - p = self.parser - p.set_code(code) - - # Previous character is not a newline. - with self.assertRaises(AssertionError): - p.set_lo(5) - - # A value of 0 doesn't change self.code. - p.set_lo(0) - self.assertEqual(p.code, code) - - # An index that is preceded by a newline. - p.set_lo(44) - self.assertEqual(p.code, code[44:]) - - def test_study1(self): - eq = self.assertEqual - p = self.parser - setcode = p.set_code - study = p._study1 - - (NONE, BACKSLASH, FIRST, NEXT, BRACKET) = range(5) - TestInfo = namedtuple('TestInfo', ['string', 'goodlines', - 'continuation']) - tests = ( - TestInfo('', [0], NONE), - # Docstrings. - TestInfo('"""This is a complete docstring."""\n', [0, 1], NONE), - TestInfo("'''This is a complete docstring.'''\n", [0, 1], NONE), - TestInfo('"""This is a continued docstring.\n', [0, 1], FIRST), - TestInfo("'''This is a continued docstring.\n", [0, 1], FIRST), - TestInfo('"""Closing quote does not match."\n', [0, 1], FIRST), - TestInfo('"""Bracket in docstring [\n', [0, 1], FIRST), - TestInfo("'''Incomplete two line docstring.\n\n", [0, 2], NEXT), - # Single-quoted strings. - TestInfo('"This is a complete string."\n', [0, 1], NONE), - TestInfo('"This is an incomplete string.\n', [0, 1], NONE), - TestInfo("'This is more incomplete.\n\n", [0, 1, 2], NONE), - # Comment (backslash does not continue comments). - TestInfo('# Comment\\\n', [0, 1], NONE), - # Brackets. - TestInfo('("""Complete string in bracket"""\n', [0, 1], BRACKET), - TestInfo('("""Open string in bracket\n', [0, 1], FIRST), - TestInfo('a = (1 + 2) - 5 *\\\n', [0, 1], BACKSLASH), # No bracket. - TestInfo('\n def function1(self, a,\n b):\n', - [0, 1, 3], NONE), - TestInfo('\n def function1(self, a,\\\n', [0, 1, 2], BRACKET), - TestInfo('\n def function1(self, a,\n', [0, 1, 2], BRACKET), - TestInfo('())\n', [0, 1], NONE), # Extra closer. - TestInfo(')(\n', [0, 1], BRACKET), # Extra closer. - # For the mismatched example, it doesn't look like continuation. - TestInfo('{)(]\n', [0, 1], NONE), # Mismatched. - ) - - for test in tests: - with self.subTest(string=test.string): - setcode(test.string) # resets study_level - study() - eq(p.study_level, 1) - eq(p.goodlines, test.goodlines) - eq(p.continuation, test.continuation) - - # Called again, just returns without reprocessing. - self.assertIsNone(study()) - - def test_get_continuation_type(self): - eq = self.assertEqual - p = self.parser - setcode = p.set_code - gettype = p.get_continuation_type - - (NONE, BACKSLASH, FIRST, NEXT, BRACKET) = range(5) - TestInfo = namedtuple('TestInfo', ['string', 'continuation']) - tests = ( - TestInfo('', NONE), - TestInfo('"""This is a continuation docstring.\n', FIRST), - TestInfo("'''This is a multiline-continued docstring.\n\n", NEXT), - TestInfo('a = (1 + 2) - 5 *\\\n', BACKSLASH), - TestInfo('\n def function1(self, a,\\\n', BRACKET) - ) - - for test in tests: - with self.subTest(string=test.string): - setcode(test.string) - eq(gettype(), test.continuation) - - def test_study2(self): - eq = self.assertEqual - p = self.parser - setcode = p.set_code - study = p._study2 - - TestInfo = namedtuple('TestInfo', ['string', 'start', 'end', 'lastch', - 'openbracket', 'bracketing']) - tests = ( - TestInfo('', 0, 0, '', None, ((0, 0),)), - TestInfo("'''This is a multiline continuation docstring.\n\n", - 0, 48, "'", None, ((0, 0), (0, 1), (48, 0))), - TestInfo(' # Comment\\\n', - 0, 12, '', None, ((0, 0), (1, 1), (12, 0))), - # A comment without a space is a special case - TestInfo(' #Comment\\\n', - 0, 0, '', None, ((0, 0),)), - # Backslash continuation. - TestInfo('a = (1 + 2) - 5 *\\\n', - 0, 19, '*', None, ((0, 0), (4, 1), (11, 0))), - # Bracket continuation with close. - TestInfo('\n def function1(self, a,\n b):\n', - 1, 48, ':', None, ((1, 0), (17, 1), (46, 0))), - # Bracket continuation with unneeded backslash. - TestInfo('\n def function1(self, a,\\\n', - 1, 28, ',', 17, ((1, 0), (17, 1))), - # Bracket continuation. - TestInfo('\n def function1(self, a,\n', - 1, 27, ',', 17, ((1, 0), (17, 1))), - # Bracket continuation with comment at end of line with text. - TestInfo('\n def function1(self, a, # End of line comment.\n', - 1, 51, ',', 17, ((1, 0), (17, 1), (28, 2), (51, 1))), - # Multi-line statement with comment line in between code lines. - TestInfo(' a = ["first item",\n # Comment line\n "next item",\n', - 0, 55, ',', 6, ((0, 0), (6, 1), (7, 2), (19, 1), - (23, 2), (38, 1), (42, 2), (53, 1))), - TestInfo('())\n', - 0, 4, ')', None, ((0, 0), (0, 1), (2, 0), (3, 0))), - TestInfo(')(\n', 0, 3, '(', 1, ((0, 0), (1, 0), (1, 1))), - # Wrong closers still decrement stack level. - TestInfo('{)(]\n', - 0, 5, ']', None, ((0, 0), (0, 1), (2, 0), (2, 1), (4, 0))), - # Character after backslash. - TestInfo(':\\a\n', 0, 4, '\\a', None, ((0, 0),)), - TestInfo('\n', 0, 0, '', None, ((0, 0),)), - ) - - for test in tests: - with self.subTest(string=test.string): - setcode(test.string) - study() - eq(p.study_level, 2) - eq(p.stmt_start, test.start) - eq(p.stmt_end, test.end) - eq(p.lastch, test.lastch) - eq(p.lastopenbracketpos, test.openbracket) - eq(p.stmt_bracketing, test.bracketing) - - # Called again, just returns without reprocessing. - self.assertIsNone(study()) - - def test_get_num_lines_in_stmt(self): - eq = self.assertEqual - p = self.parser - setcode = p.set_code - getlines = p.get_num_lines_in_stmt - - TestInfo = namedtuple('TestInfo', ['string', 'lines']) - tests = ( - TestInfo('[x for x in a]\n', 1), # Closed on one line. - TestInfo('[x\nfor x in a\n', 2), # Not closed. - TestInfo('[x\\\nfor x in a\\\n', 2), # "", uneeded backslashes. - TestInfo('[x\nfor x in a\n]\n', 3), # Closed on multi-line. - TestInfo('\n"""Docstring comment L1"""\nL2\nL3\nL4\n', 1), - TestInfo('\n"""Docstring comment L1\nL2"""\nL3\nL4\n', 1), - TestInfo('\n"""Docstring comment L1\\\nL2\\\nL3\\\nL4\\\n', 4), - TestInfo('\n\n"""Docstring comment L1\\\nL2\\\nL3\\\nL4\\\n"""\n', 5) - ) - - # Blank string doesn't have enough elements in goodlines. - setcode('') - with self.assertRaises(IndexError): - getlines() - - for test in tests: - with self.subTest(string=test.string): - setcode(test.string) - eq(getlines(), test.lines) - - def test_compute_bracket_indent(self): - eq = self.assertEqual - p = self.parser - setcode = p.set_code - indent = p.compute_bracket_indent - - TestInfo = namedtuple('TestInfo', ['string', 'spaces']) - tests = ( - TestInfo('def function1(self, a,\n', 14), - # Characters after bracket. - TestInfo('\n def function1(self, a,\n', 18), - TestInfo('\n\tdef function1(self, a,\n', 18), - # No characters after bracket. - TestInfo('\n def function1(\n', 8), - TestInfo('\n\tdef function1(\n', 8), - TestInfo('\n def function1( \n', 8), # Ignore extra spaces. - TestInfo('[\n"first item",\n # Comment line\n "next item",\n', 0), - TestInfo('[\n "first item",\n # Comment line\n "next item",\n', 2), - TestInfo('["first item",\n # Comment line\n "next item",\n', 1), - TestInfo('(\n', 4), - TestInfo('(a\n', 1), - ) - - # Must be C_BRACKET continuation type. - setcode('def function1(self, a, b):\n') - with self.assertRaises(AssertionError): - indent() - - for test in tests: - setcode(test.string) - eq(indent(), test.spaces) - - def test_compute_backslash_indent(self): - eq = self.assertEqual - p = self.parser - setcode = p.set_code - indent = p.compute_backslash_indent - - # Must be C_BACKSLASH continuation type. - errors = (('def function1(self, a, b\\\n'), # Bracket. - (' """ (\\\n'), # Docstring. - ('a = #\\\n'), # Inline comment. - ) - for string in errors: - with self.subTest(string=string): - setcode(string) - with self.assertRaises(AssertionError): - indent() - - TestInfo = namedtuple('TestInfo', ('string', 'spaces')) - tests = (TestInfo('a = (1 + 2) - 5 *\\\n', 4), - TestInfo('a = 1 + 2 - 5 *\\\n', 4), - TestInfo(' a = 1 + 2 - 5 *\\\n', 8), - TestInfo(' a = "spam"\\\n', 6), - TestInfo(' a = \\\n"a"\\\n', 4), - TestInfo(' a = #\\\n"a"\\\n', 5), - TestInfo('a == \\\n', 2), - TestInfo('a != \\\n', 2), - # Difference between containing = and those not. - TestInfo('\\\n', 2), - TestInfo(' \\\n', 6), - TestInfo('\t\\\n', 6), - TestInfo('a\\\n', 3), - TestInfo('{}\\\n', 4), - TestInfo('(1 + 2) - 5 *\\\n', 3), - ) - for test in tests: - with self.subTest(string=test.string): - setcode(test.string) - eq(indent(), test.spaces) - - def test_get_base_indent_string(self): - eq = self.assertEqual - p = self.parser - setcode = p.set_code - baseindent = p.get_base_indent_string - - TestInfo = namedtuple('TestInfo', ['string', 'indent']) - tests = (TestInfo('', ''), - TestInfo('def a():\n', ''), - TestInfo('\tdef a():\n', '\t'), - TestInfo(' def a():\n', ' '), - TestInfo(' def a(\n', ' '), - TestInfo('\t\n def a(\n', ' '), - TestInfo('\t\n # Comment.\n', ' '), - ) - - for test in tests: - with self.subTest(string=test.string): - setcode(test.string) - eq(baseindent(), test.indent) - - def test_is_block_opener(self): - yes = self.assertTrue - no = self.assertFalse - p = self.parser - setcode = p.set_code - opener = p.is_block_opener - - TestInfo = namedtuple('TestInfo', ['string', 'assert_']) - tests = ( - TestInfo('def a():\n', yes), - TestInfo('\n def function1(self, a,\n b):\n', yes), - TestInfo(':\n', yes), - TestInfo('a:\n', yes), - TestInfo('):\n', yes), - TestInfo('(:\n', yes), - TestInfo('":\n', no), - TestInfo('\n def function1(self, a,\n', no), - TestInfo('def function1(self, a):\n pass\n', no), - TestInfo('# A comment:\n', no), - TestInfo('"""A docstring:\n', no), - TestInfo('"""A docstring:\n', no), - ) - - for test in tests: - with self.subTest(string=test.string): - setcode(test.string) - test.assert_(opener()) - - def test_is_block_closer(self): - yes = self.assertTrue - no = self.assertFalse - p = self.parser - setcode = p.set_code - closer = p.is_block_closer - - TestInfo = namedtuple('TestInfo', ['string', 'assert_']) - tests = ( - TestInfo('return\n', yes), - TestInfo('\tbreak\n', yes), - TestInfo(' continue\n', yes), - TestInfo(' raise\n', yes), - TestInfo('pass \n', yes), - TestInfo('pass\t\n', yes), - TestInfo('return #\n', yes), - TestInfo('raised\n', no), - TestInfo('returning\n', no), - TestInfo('# return\n', no), - TestInfo('"""break\n', no), - TestInfo('"continue\n', no), - TestInfo('def function1(self, a):\n pass\n', yes), - ) - - for test in tests: - with self.subTest(string=test.string): - setcode(test.string) - test.assert_(closer()) - - def test_get_last_stmt_bracketing(self): - eq = self.assertEqual - p = self.parser - setcode = p.set_code - bracketing = p.get_last_stmt_bracketing - - TestInfo = namedtuple('TestInfo', ['string', 'bracket']) - tests = ( - TestInfo('', ((0, 0),)), - TestInfo('a\n', ((0, 0),)), - TestInfo('()()\n', ((0, 0), (0, 1), (2, 0), (2, 1), (4, 0))), - TestInfo('(\n)()\n', ((0, 0), (0, 1), (3, 0), (3, 1), (5, 0))), - TestInfo('()\n()\n', ((3, 0), (3, 1), (5, 0))), - TestInfo('()(\n)\n', ((0, 0), (0, 1), (2, 0), (2, 1), (5, 0))), - TestInfo('(())\n', ((0, 0), (0, 1), (1, 2), (3, 1), (4, 0))), - TestInfo('(\n())\n', ((0, 0), (0, 1), (2, 2), (4, 1), (5, 0))), - # Same as matched test. - TestInfo('{)(]\n', ((0, 0), (0, 1), (2, 0), (2, 1), (4, 0))), - TestInfo('(((())\n', - ((0, 0), (0, 1), (1, 2), (2, 3), (3, 4), (5, 3), (6, 2))), - ) - - for test in tests: - with self.subTest(string=test.string): - setcode(test.string) - eq(bracketing(), test.bracket) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_pyshell.py b/Lib/idlelib/idle_test/test_pyshell.py deleted file mode 100644 index 4a09667..0000000 --- a/Lib/idlelib/idle_test/test_pyshell.py +++ /dev/null @@ -1,64 +0,0 @@ -"Test pyshell, coverage 12%." -# Plus coverage of test_warning. Was 20% with test_openshell. - -from idlelib import pyshell -import unittest -from test.support import requires -from tkinter import Tk - - -class FunctionTest(unittest.TestCase): - # Test stand-alone module level non-gui functions. - - def test_restart_line_wide(self): - eq = self.assertEqual - for file, mul, extra in (('', 22, ''), ('finame', 21, '=')): - width = 60 - bar = mul * '=' - with self.subTest(file=file, bar=bar): - file = file or 'Shell' - line = pyshell.restart_line(width, file) - eq(len(line), width) - eq(line, f"{bar+extra} RESTART: {file} {bar}") - - def test_restart_line_narrow(self): - expect, taglen = "= RESTART: Shell", 16 - for width in (taglen-1, taglen, taglen+1): - with self.subTest(width=width): - self.assertEqual(pyshell.restart_line(width, ''), expect) - self.assertEqual(pyshell.restart_line(taglen+2, ''), expect+' =') - - -class PyShellFileListTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - - @classmethod - def tearDownClass(cls): - #cls.root.update_idletasks() -## for id in cls.root.tk.call('after', 'info'): -## cls.root.after_cancel(id) # Need for EditorWindow. - cls.root.destroy() - del cls.root - - def test_init(self): - psfl = pyshell.PyShellFileList(self.root) - self.assertEqual(psfl.EditorWindow, pyshell.PyShellEditorWindow) - self.assertIsNone(psfl.pyshell) - -# The following sometimes causes 'invalid command name "109734456recolorize"'. -# Uncommenting after_cancel above prevents this, but results in -# TclError: bad window path name ".!listedtoplevel.!frame.text" -# which is normally prevented by after_cancel. -## def test_openshell(self): -## pyshell.use_subprocess = False -## ps = pyshell.PyShellFileList(self.root).open_shell() -## self.assertIsInstance(ps, pyshell.PyShell) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_query.py b/Lib/idlelib/idle_test/test_query.py deleted file mode 100644 index f957585..0000000 --- a/Lib/idlelib/idle_test/test_query.py +++ /dev/null @@ -1,405 +0,0 @@ -"""Test query, coverage 93%). - -Non-gui tests for Query, SectionName, ModuleName, and HelpSource use -dummy versions that extract the non-gui methods and add other needed -attributes. GUI tests create an instance of each class and simulate -entries and button clicks. Subclass tests only target the new code in -the subclass definition. - -The appearance of the widgets is checked by the Query and -HelpSource htests. These are run by running query.py. -""" -from idlelib import query -import unittest -from test.support import requires -from tkinter import Tk, END - -import sys -from unittest import mock -from idlelib.idle_test.mock_tk import Var - - -# NON-GUI TESTS - -class QueryTest(unittest.TestCase): - "Test Query base class." - - class Dummy_Query: - # Test the following Query methods. - entry_ok = query.Query.entry_ok - ok = query.Query.ok - cancel = query.Query.cancel - # Add attributes and initialization needed for tests. - def __init__(self, dummy_entry): - self.entry = Var(value=dummy_entry) - self.entry_error = {'text': ''} - self.result = None - self.destroyed = False - def showerror(self, message): - self.entry_error['text'] = message - def destroy(self): - self.destroyed = True - - def test_entry_ok_blank(self): - dialog = self.Dummy_Query(' ') - self.assertEqual(dialog.entry_ok(), None) - self.assertEqual((dialog.result, dialog.destroyed), (None, False)) - self.assertIn('blank line', dialog.entry_error['text']) - - def test_entry_ok_good(self): - dialog = self.Dummy_Query(' good ') - Equal = self.assertEqual - Equal(dialog.entry_ok(), 'good') - Equal((dialog.result, dialog.destroyed), (None, False)) - Equal(dialog.entry_error['text'], '') - - def test_ok_blank(self): - dialog = self.Dummy_Query('') - dialog.entry.focus_set = mock.Mock() - self.assertEqual(dialog.ok(), None) - self.assertTrue(dialog.entry.focus_set.called) - del dialog.entry.focus_set - self.assertEqual((dialog.result, dialog.destroyed), (None, False)) - - def test_ok_good(self): - dialog = self.Dummy_Query('good') - self.assertEqual(dialog.ok(), None) - self.assertEqual((dialog.result, dialog.destroyed), ('good', True)) - - def test_cancel(self): - dialog = self.Dummy_Query('does not matter') - self.assertEqual(dialog.cancel(), None) - self.assertEqual((dialog.result, dialog.destroyed), (None, True)) - - -class SectionNameTest(unittest.TestCase): - "Test SectionName subclass of Query." - - class Dummy_SectionName: - entry_ok = query.SectionName.entry_ok # Function being tested. - used_names = ['used'] - def __init__(self, dummy_entry): - self.entry = Var(value=dummy_entry) - self.entry_error = {'text': ''} - def showerror(self, message): - self.entry_error['text'] = message - - def test_blank_section_name(self): - dialog = self.Dummy_SectionName(' ') - self.assertEqual(dialog.entry_ok(), None) - self.assertIn('no name', dialog.entry_error['text']) - - def test_used_section_name(self): - dialog = self.Dummy_SectionName('used') - self.assertEqual(dialog.entry_ok(), None) - self.assertIn('use', dialog.entry_error['text']) - - def test_long_section_name(self): - dialog = self.Dummy_SectionName('good'*8) - self.assertEqual(dialog.entry_ok(), None) - self.assertIn('longer than 30', dialog.entry_error['text']) - - def test_good_section_name(self): - dialog = self.Dummy_SectionName(' good ') - self.assertEqual(dialog.entry_ok(), 'good') - self.assertEqual(dialog.entry_error['text'], '') - - -class ModuleNameTest(unittest.TestCase): - "Test ModuleName subclass of Query." - - class Dummy_ModuleName: - entry_ok = query.ModuleName.entry_ok # Function being tested. - text0 = '' - def __init__(self, dummy_entry): - self.entry = Var(value=dummy_entry) - self.entry_error = {'text': ''} - def showerror(self, message): - self.entry_error['text'] = message - - def test_blank_module_name(self): - dialog = self.Dummy_ModuleName(' ') - self.assertEqual(dialog.entry_ok(), None) - self.assertIn('no name', dialog.entry_error['text']) - - def test_bogus_module_name(self): - dialog = self.Dummy_ModuleName('__name_xyz123_should_not_exist__') - self.assertEqual(dialog.entry_ok(), None) - self.assertIn('not found', dialog.entry_error['text']) - - def test_c_source_name(self): - dialog = self.Dummy_ModuleName('itertools') - self.assertEqual(dialog.entry_ok(), None) - self.assertIn('source-based', dialog.entry_error['text']) - - def test_good_module_name(self): - dialog = self.Dummy_ModuleName('idlelib') - self.assertTrue(dialog.entry_ok().endswith('__init__.py')) - self.assertEqual(dialog.entry_error['text'], '') - - -# 3 HelpSource test classes each test one method. - -class HelpsourceBrowsefileTest(unittest.TestCase): - "Test browse_file method of ModuleName subclass of Query." - - class Dummy_HelpSource: - browse_file = query.HelpSource.browse_file - pathvar = Var() - - def test_file_replaces_path(self): - dialog = self.Dummy_HelpSource() - # Path is widget entry, either '' or something. - # Func return is file dialog return, either '' or something. - # Func return should override widget entry. - # We need all 4 combinations to test all (most) code paths. - for path, func, result in ( - ('', lambda a,b,c:'', ''), - ('', lambda a,b,c: __file__, __file__), - ('htest', lambda a,b,c:'', 'htest'), - ('htest', lambda a,b,c: __file__, __file__)): - with self.subTest(): - dialog.pathvar.set(path) - dialog.askfilename = func - dialog.browse_file() - self.assertEqual(dialog.pathvar.get(), result) - - -class HelpsourcePathokTest(unittest.TestCase): - "Test path_ok method of HelpSource subclass of Query." - - class Dummy_HelpSource: - path_ok = query.HelpSource.path_ok - def __init__(self, dummy_path): - self.path = Var(value=dummy_path) - self.path_error = {'text': ''} - def showerror(self, message, widget=None): - self.path_error['text'] = message - - orig_platform = query.platform # Set in test_path_ok_file. - @classmethod - def tearDownClass(cls): - query.platform = cls.orig_platform - - def test_path_ok_blank(self): - dialog = self.Dummy_HelpSource(' ') - self.assertEqual(dialog.path_ok(), None) - self.assertIn('no help file', dialog.path_error['text']) - - def test_path_ok_bad(self): - dialog = self.Dummy_HelpSource(__file__ + 'bad-bad-bad') - self.assertEqual(dialog.path_ok(), None) - self.assertIn('not exist', dialog.path_error['text']) - - def test_path_ok_web(self): - dialog = self.Dummy_HelpSource('') - Equal = self.assertEqual - for url in 'www.py.org', 'http://py.org': - with self.subTest(): - dialog.path.set(url) - self.assertEqual(dialog.path_ok(), url) - self.assertEqual(dialog.path_error['text'], '') - - def test_path_ok_file(self): - dialog = self.Dummy_HelpSource('') - for platform, prefix in ('darwin', 'file://'), ('other', ''): - with self.subTest(): - query.platform = platform - dialog.path.set(__file__) - self.assertEqual(dialog.path_ok(), prefix + __file__) - self.assertEqual(dialog.path_error['text'], '') - - -class HelpsourceEntryokTest(unittest.TestCase): - "Test entry_ok method of HelpSource subclass of Query." - - class Dummy_HelpSource: - entry_ok = query.HelpSource.entry_ok - entry_error = {} - path_error = {} - def item_ok(self): - return self.name - def path_ok(self): - return self.path - - def test_entry_ok_helpsource(self): - dialog = self.Dummy_HelpSource() - for name, path, result in ((None, None, None), - (None, 'doc.txt', None), - ('doc', None, None), - ('doc', 'doc.txt', ('doc', 'doc.txt'))): - with self.subTest(): - dialog.name, dialog.path = name, path - self.assertEqual(dialog.entry_ok(), result) - - -# 2 CustomRun test classes each test one method. - -class CustomRunCLIargsokTest(unittest.TestCase): - "Test cli_ok method of the CustomRun subclass of Query." - - class Dummy_CustomRun: - cli_args_ok = query.CustomRun.cli_args_ok - def __init__(self, dummy_entry): - self.entry = Var(value=dummy_entry) - self.entry_error = {'text': ''} - def showerror(self, message): - self.entry_error['text'] = message - - def test_blank_args(self): - dialog = self.Dummy_CustomRun(' ') - self.assertEqual(dialog.cli_args_ok(), []) - - def test_invalid_args(self): - dialog = self.Dummy_CustomRun("'no-closing-quote") - self.assertEqual(dialog.cli_args_ok(), None) - self.assertIn('No closing', dialog.entry_error['text']) - - def test_good_args(self): - args = ['-n', '10', '--verbose', '-p', '/path', '--name'] - dialog = self.Dummy_CustomRun(' '.join(args) + ' "my name"') - self.assertEqual(dialog.cli_args_ok(), args + ["my name"]) - self.assertEqual(dialog.entry_error['text'], '') - - -class CustomRunEntryokTest(unittest.TestCase): - "Test entry_ok method of the CustomRun subclass of Query." - - class Dummy_CustomRun: - entry_ok = query.CustomRun.entry_ok - entry_error = {} - restartvar = Var() - def cli_args_ok(self): - return self.cli_args - - def test_entry_ok_customrun(self): - dialog = self.Dummy_CustomRun() - for restart in {True, False}: - dialog.restartvar.set(restart) - for cli_args, result in ((None, None), - (['my arg'], (['my arg'], restart))): - with self.subTest(restart=restart, cli_args=cli_args): - dialog.cli_args = cli_args - self.assertEqual(dialog.entry_ok(), result) - - -# GUI TESTS - -class QueryGuiTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = root = Tk() - cls.root.withdraw() - cls.dialog = query.Query(root, 'TEST', 'test', _utest=True) - cls.dialog.destroy = mock.Mock() - - @classmethod - def tearDownClass(cls): - del cls.dialog.destroy - del cls.dialog - cls.root.destroy() - del cls.root - - def setUp(self): - self.dialog.entry.delete(0, 'end') - self.dialog.result = None - self.dialog.destroy.reset_mock() - - def test_click_ok(self): - dialog = self.dialog - dialog.entry.insert(0, 'abc') - dialog.button_ok.invoke() - self.assertEqual(dialog.result, 'abc') - self.assertTrue(dialog.destroy.called) - - def test_click_blank(self): - dialog = self.dialog - dialog.button_ok.invoke() - self.assertEqual(dialog.result, None) - self.assertFalse(dialog.destroy.called) - - def test_click_cancel(self): - dialog = self.dialog - dialog.entry.insert(0, 'abc') - dialog.button_cancel.invoke() - self.assertEqual(dialog.result, None) - self.assertTrue(dialog.destroy.called) - - -class SectionnameGuiTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - - def test_click_section_name(self): - root = Tk() - root.withdraw() - dialog = query.SectionName(root, 'T', 't', {'abc'}, _utest=True) - Equal = self.assertEqual - self.assertEqual(dialog.used_names, {'abc'}) - dialog.entry.insert(0, 'okay') - dialog.button_ok.invoke() - self.assertEqual(dialog.result, 'okay') - root.destroy() - - -class ModulenameGuiTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - - def test_click_module_name(self): - root = Tk() - root.withdraw() - dialog = query.ModuleName(root, 'T', 't', 'idlelib', _utest=True) - self.assertEqual(dialog.text0, 'idlelib') - self.assertEqual(dialog.entry.get(), 'idlelib') - dialog.button_ok.invoke() - self.assertTrue(dialog.result.endswith('__init__.py')) - root.destroy() - - -class HelpsourceGuiTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - - def test_click_help_source(self): - root = Tk() - root.withdraw() - dialog = query.HelpSource(root, 'T', menuitem='__test__', - filepath=__file__, _utest=True) - Equal = self.assertEqual - Equal(dialog.entry.get(), '__test__') - Equal(dialog.path.get(), __file__) - dialog.button_ok.invoke() - prefix = "file://" if sys.platform == 'darwin' else '' - Equal(dialog.result, ('__test__', prefix + __file__)) - root.destroy() - - -class CustomRunGuiTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - - def test_click_args(self): - root = Tk() - root.withdraw() - dialog = query.CustomRun(root, 'Title', - cli_args=['a', 'b=1'], _utest=True) - self.assertEqual(dialog.entry.get(), 'a b=1') - dialog.entry.insert(END, ' c') - dialog.button_ok.invoke() - self.assertEqual(dialog.result, (['a', 'b=1', 'c'], True)) - root.destroy() - - -if __name__ == '__main__': - unittest.main(verbosity=2, exit=False) diff --git a/Lib/idlelib/idle_test/test_replace.py b/Lib/idlelib/idle_test/test_replace.py deleted file mode 100644 index c3c5d2e..0000000 --- a/Lib/idlelib/idle_test/test_replace.py +++ /dev/null @@ -1,294 +0,0 @@ -"Test replace, coverage 78%." - -from idlelib.replace import ReplaceDialog -import unittest -from test.support import requires -requires('gui') -from tkinter import Tk, Text - -from unittest.mock import Mock -from idlelib.idle_test.mock_tk import Mbox -import idlelib.searchengine as se - -orig_mbox = se.tkMessageBox -showerror = Mbox.showerror - - -class ReplaceDialogTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.root = Tk() - cls.root.withdraw() - se.tkMessageBox = Mbox - cls.engine = se.SearchEngine(cls.root) - cls.dialog = ReplaceDialog(cls.root, cls.engine) - cls.dialog.bell = lambda: None - cls.dialog.ok = Mock() - cls.text = Text(cls.root) - cls.text.undo_block_start = Mock() - cls.text.undo_block_stop = Mock() - cls.dialog.text = cls.text - - @classmethod - def tearDownClass(cls): - se.tkMessageBox = orig_mbox - del cls.text, cls.dialog, cls.engine - cls.root.destroy() - del cls.root - - def setUp(self): - self.text.insert('insert', 'This is a sample sTring') - - def tearDown(self): - self.engine.patvar.set('') - self.dialog.replvar.set('') - self.engine.wordvar.set(False) - self.engine.casevar.set(False) - self.engine.revar.set(False) - self.engine.wrapvar.set(True) - self.engine.backvar.set(False) - showerror.title = '' - showerror.message = '' - self.text.delete('1.0', 'end') - - def test_replace_simple(self): - # Test replace function with all options at default setting. - # Wrap around - True - # Regular Expression - False - # Match case - False - # Match word - False - # Direction - Forwards - text = self.text - equal = self.assertEqual - pv = self.engine.patvar - rv = self.dialog.replvar - replace = self.dialog.replace_it - - # test accessor method - self.engine.setpat('asdf') - equal(self.engine.getpat(), pv.get()) - - # text found and replaced - pv.set('a') - rv.set('asdf') - replace() - equal(text.get('1.8', '1.12'), 'asdf') - - # don't "match word" case - text.mark_set('insert', '1.0') - pv.set('is') - rv.set('hello') - replace() - equal(text.get('1.2', '1.7'), 'hello') - - # don't "match case" case - pv.set('string') - rv.set('world') - replace() - equal(text.get('1.23', '1.28'), 'world') - - # without "regular expression" case - text.mark_set('insert', 'end') - text.insert('insert', '\nline42:') - before_text = text.get('1.0', 'end') - pv.set(r'[a-z][\d]+') - replace() - after_text = text.get('1.0', 'end') - equal(before_text, after_text) - - # test with wrap around selected and complete a cycle - text.mark_set('insert', '1.9') - pv.set('i') - rv.set('j') - replace() - equal(text.get('1.8'), 'i') - equal(text.get('2.1'), 'j') - replace() - equal(text.get('2.1'), 'j') - equal(text.get('1.8'), 'j') - before_text = text.get('1.0', 'end') - replace() - after_text = text.get('1.0', 'end') - equal(before_text, after_text) - - # text not found - before_text = text.get('1.0', 'end') - pv.set('foobar') - replace() - after_text = text.get('1.0', 'end') - equal(before_text, after_text) - - # test access method - self.dialog.find_it(0) - - def test_replace_wrap_around(self): - text = self.text - equal = self.assertEqual - pv = self.engine.patvar - rv = self.dialog.replvar - replace = self.dialog.replace_it - self.engine.wrapvar.set(False) - - # replace candidate found both after and before 'insert' - text.mark_set('insert', '1.4') - pv.set('i') - rv.set('j') - replace() - equal(text.get('1.2'), 'i') - equal(text.get('1.5'), 'j') - replace() - equal(text.get('1.2'), 'i') - equal(text.get('1.20'), 'j') - replace() - equal(text.get('1.2'), 'i') - - # replace candidate found only before 'insert' - text.mark_set('insert', '1.8') - pv.set('is') - before_text = text.get('1.0', 'end') - replace() - after_text = text.get('1.0', 'end') - equal(before_text, after_text) - - def test_replace_whole_word(self): - text = self.text - equal = self.assertEqual - pv = self.engine.patvar - rv = self.dialog.replvar - replace = self.dialog.replace_it - self.engine.wordvar.set(True) - - pv.set('is') - rv.set('hello') - replace() - equal(text.get('1.0', '1.4'), 'This') - equal(text.get('1.5', '1.10'), 'hello') - - def test_replace_match_case(self): - equal = self.assertEqual - text = self.text - pv = self.engine.patvar - rv = self.dialog.replvar - replace = self.dialog.replace_it - self.engine.casevar.set(True) - - before_text = self.text.get('1.0', 'end') - pv.set('this') - rv.set('that') - replace() - after_text = self.text.get('1.0', 'end') - equal(before_text, after_text) - - pv.set('This') - replace() - equal(text.get('1.0', '1.4'), 'that') - - def test_replace_regex(self): - equal = self.assertEqual - text = self.text - pv = self.engine.patvar - rv = self.dialog.replvar - replace = self.dialog.replace_it - self.engine.revar.set(True) - - before_text = text.get('1.0', 'end') - pv.set(r'[a-z][\d]+') - rv.set('hello') - replace() - after_text = text.get('1.0', 'end') - equal(before_text, after_text) - - text.insert('insert', '\nline42') - replace() - equal(text.get('2.0', '2.8'), 'linhello') - - pv.set('') - replace() - self.assertIn('error', showerror.title) - self.assertIn('Empty', showerror.message) - - pv.set(r'[\d') - replace() - self.assertIn('error', showerror.title) - self.assertIn('Pattern', showerror.message) - - showerror.title = '' - showerror.message = '' - pv.set('[a]') - rv.set('test\\') - replace() - self.assertIn('error', showerror.title) - self.assertIn('Invalid Replace Expression', showerror.message) - - # test access method - self.engine.setcookedpat("?") - equal(pv.get(), "\\?") - - def test_replace_backwards(self): - equal = self.assertEqual - text = self.text - pv = self.engine.patvar - rv = self.dialog.replvar - replace = self.dialog.replace_it - self.engine.backvar.set(True) - - text.insert('insert', '\nis as ') - - pv.set('is') - rv.set('was') - replace() - equal(text.get('1.2', '1.4'), 'is') - equal(text.get('2.0', '2.3'), 'was') - replace() - equal(text.get('1.5', '1.8'), 'was') - replace() - equal(text.get('1.2', '1.5'), 'was') - - def test_replace_all(self): - text = self.text - pv = self.engine.patvar - rv = self.dialog.replvar - replace_all = self.dialog.replace_all - - text.insert('insert', '\n') - text.insert('insert', text.get('1.0', 'end')*100) - pv.set('is') - rv.set('was') - replace_all() - self.assertNotIn('is', text.get('1.0', 'end')) - - self.engine.revar.set(True) - pv.set('') - replace_all() - self.assertIn('error', showerror.title) - self.assertIn('Empty', showerror.message) - - pv.set('[s][T]') - rv.set('\\') - replace_all() - - self.engine.revar.set(False) - pv.set('text which is not present') - rv.set('foobar') - replace_all() - - def test_default_command(self): - text = self.text - pv = self.engine.patvar - rv = self.dialog.replvar - replace_find = self.dialog.default_command - equal = self.assertEqual - - pv.set('This') - rv.set('was') - replace_find() - equal(text.get('sel.first', 'sel.last'), 'was') - - self.engine.revar.set(True) - pv.set('') - replace_find() - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_rpc.py b/Lib/idlelib/idle_test/test_rpc.py deleted file mode 100644 index 81eff39..0000000 --- a/Lib/idlelib/idle_test/test_rpc.py +++ /dev/null @@ -1,29 +0,0 @@ -"Test rpc, coverage 20%." - -from idlelib import rpc -import unittest - - - -class CodePicklerTest(unittest.TestCase): - - def test_pickle_unpickle(self): - def f(): return a + b + c - func, (cbytes,) = rpc.pickle_code(f.__code__) - self.assertIs(func, rpc.unpickle_code) - self.assertIn(b'test_rpc.py', cbytes) - code = rpc.unpickle_code(cbytes) - self.assertEqual(code.co_names, ('a', 'b', 'c')) - - def test_code_pickler(self): - self.assertIn(type((lambda:None).__code__), - rpc.CodePickler.dispatch_table) - - def test_dumps(self): - def f(): pass - # The main test here is that pickling code does not raise. - self.assertIn(b'test_rpc.py', rpc.dumps(f.__code__)) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_rstrip.py b/Lib/idlelib/idle_test/test_rstrip.py new file mode 100644 index 0000000..1c90b93 --- /dev/null +++ b/Lib/idlelib/idle_test/test_rstrip.py @@ -0,0 +1,49 @@ +import unittest +import idlelib.RstripExtension as rs +from idlelib.idle_test.mock_idle import Editor + +class rstripTest(unittest.TestCase): + + def test_rstrip_line(self): + editor = Editor() + text = editor.text + do_rstrip = rs.RstripExtension(editor).do_rstrip + + do_rstrip() + self.assertEqual(text.get('1.0', 'insert'), '') + text.insert('1.0', ' ') + do_rstrip() + self.assertEqual(text.get('1.0', 'insert'), '') + text.insert('1.0', ' \n') + do_rstrip() + self.assertEqual(text.get('1.0', 'insert'), '\n') + + def test_rstrip_multiple(self): + editor = Editor() + # Uncomment following to verify that test passes with real widgets. +## from idlelib.EditorWindow import EditorWindow as Editor +## from tkinter import Tk +## editor = Editor(root=Tk()) + text = editor.text + do_rstrip = rs.RstripExtension(editor).do_rstrip + + original = ( + "Line with an ending tab \n" + "Line ending in 5 spaces \n" + "Linewithnospaces\n" + " indented line\n" + " indented line with trailing space \n" + " ") + stripped = ( + "Line with an ending tab\n" + "Line ending in 5 spaces\n" + "Linewithnospaces\n" + " indented line\n" + " indented line with trailing space\n") + + text.insert('1.0', original) + do_rstrip() + self.assertEqual(text.get('1.0', 'insert'), stripped) + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=False) diff --git a/Lib/idlelib/idle_test/test_runscript.py b/Lib/idlelib/idle_test/test_runscript.py deleted file mode 100644 index 5fc6018..0000000 --- a/Lib/idlelib/idle_test/test_runscript.py +++ /dev/null @@ -1,33 +0,0 @@ -"Test runscript, coverage 16%." - -from idlelib import runscript -import unittest -from test.support import requires -from tkinter import Tk -from idlelib.editor import EditorWindow - - -class ScriptBindingTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - - @classmethod - def tearDownClass(cls): - cls.root.update_idletasks() - for id in cls.root.tk.call('after', 'info'): - cls.root.after_cancel(id) # Need for EditorWindow. - cls.root.destroy() - del cls.root - - def test_init(self): - ew = EditorWindow(root=self.root) - sb = runscript.ScriptBinding(ew) - ew._close() - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_scrolledlist.py b/Lib/idlelib/idle_test/test_scrolledlist.py deleted file mode 100644 index 2f819fd..0000000 --- a/Lib/idlelib/idle_test/test_scrolledlist.py +++ /dev/null @@ -1,27 +0,0 @@ -"Test scrolledlist, coverage 38%." - -from idlelib.scrolledlist import ScrolledList -import unittest -from test.support import requires -requires('gui') -from tkinter import Tk - - -class ScrolledListTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.root = Tk() - - @classmethod - def tearDownClass(cls): - cls.root.destroy() - del cls.root - - - def test_init(self): - ScrolledList(self.root) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_search.py b/Lib/idlelib/idle_test/test_search.py deleted file mode 100644 index de703c1..0000000 --- a/Lib/idlelib/idle_test/test_search.py +++ /dev/null @@ -1,80 +0,0 @@ -"Test search, coverage 69%." - -from idlelib import search -import unittest -from test.support import requires -requires('gui') -from tkinter import Tk, Text, BooleanVar -from idlelib import searchengine - -# Does not currently test the event handler wrappers. -# A usage test should simulate clicks and check highlighting. -# Tests need to be coordinated with SearchDialogBase tests -# to avoid duplication. - - -class SearchDialogTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.root = Tk() - - @classmethod - def tearDownClass(cls): - cls.root.destroy() - del cls.root - - def setUp(self): - self.engine = searchengine.SearchEngine(self.root) - self.dialog = search.SearchDialog(self.root, self.engine) - self.dialog.bell = lambda: None - self.text = Text(self.root) - self.text.insert('1.0', 'Hello World!') - - def test_find_again(self): - # Search for various expressions - text = self.text - - self.engine.setpat('') - self.assertFalse(self.dialog.find_again(text)) - self.dialog.bell = lambda: None - - self.engine.setpat('Hello') - self.assertTrue(self.dialog.find_again(text)) - - self.engine.setpat('Goodbye') - self.assertFalse(self.dialog.find_again(text)) - - self.engine.setpat('World!') - self.assertTrue(self.dialog.find_again(text)) - - self.engine.setpat('Hello World!') - self.assertTrue(self.dialog.find_again(text)) - - # Regular expression - self.engine.revar = BooleanVar(self.root, True) - self.engine.setpat('W[aeiouy]r') - self.assertTrue(self.dialog.find_again(text)) - - def test_find_selection(self): - # Select some text and make sure it's found - text = self.text - # Add additional line to find - self.text.insert('2.0', 'Hello World!') - - text.tag_add('sel', '1.0', '1.4') # Select 'Hello' - self.assertTrue(self.dialog.find_selection(text)) - - text.tag_remove('sel', '1.0', 'end') - text.tag_add('sel', '1.6', '1.11') # Select 'World!' - self.assertTrue(self.dialog.find_selection(text)) - - text.tag_remove('sel', '1.0', 'end') - text.tag_add('sel', '1.0', '1.11') # Select 'Hello World!' - self.assertTrue(self.dialog.find_selection(text)) - - # Remove additional line - text.delete('2.0', 'end') - -if __name__ == '__main__': - unittest.main(verbosity=2, exit=2) diff --git a/Lib/idlelib/idle_test/test_searchbase.py b/Lib/idlelib/idle_test/test_searchdialogbase.py index aee0c4c..59b9bbf 100644 --- a/Lib/idlelib/idle_test/test_searchbase.py +++ b/Lib/idlelib/idle_test/test_searchdialogbase.py @@ -1,18 +1,18 @@ -"Test searchbase, coverage 98%." -# The only thing not covered is inconsequential -- -# testing skipping of suite when self.needwrapbutton is false. +'''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 Text, Tk, Toplevel -from tkinter.ttk import Frame -from idlelib import searchengine as se -from idlelib import searchbase as sdb +from test.test_support import requires +from Tkinter import Text, 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 +##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. +# 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 @@ -22,7 +22,6 @@ from idlelib.idle_test.mock_idle import Func ## se.BooleanVar = BooleanVar ## se.StringVar = StringVar - class SearchDialogBaseTest(unittest.TestCase): @classmethod @@ -32,7 +31,6 @@ class SearchDialogBaseTest(unittest.TestCase): @classmethod def tearDownClass(cls): - cls.root.update_idletasks() cls.root.destroy() del cls.root @@ -76,7 +74,7 @@ class SearchDialogBaseTest(unittest.TestCase): def test_make_entry(self): equal = self.assertEqual self.dialog.row = 0 - self.dialog.top = self.root + self.dialog.top = Toplevel(self.root) entry, label = self.dialog.make_entry("Test:", 'hello') equal(label['text'], 'Test:') @@ -89,7 +87,6 @@ class SearchDialogBaseTest(unittest.TestCase): equal(self.dialog.row, 1) def test_create_entries(self): - self.dialog.top = self.root self.dialog.row = 0 self.engine.setpat('hello') self.dialog.create_entries() @@ -97,18 +94,17 @@ class SearchDialogBaseTest(unittest.TestCase): def test_make_frame(self): self.dialog.row = 0 - self.dialog.top = self.root + self.dialog.top = Toplevel(self.root) frame, label = self.dialog.make_frame() self.assertEqual(label, '') - self.assertEqual(str(type(frame)), "<class 'tkinter.ttk.Frame'>") - # self.assertIsInstance(frame, Frame) fails when test is run by - # test_idle not run from IDLE editor. See issue 33987 PR. + 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 = self.root + self.dialog.top = Toplevel(self.root) self.dialog.row = 0 return meth() @@ -123,6 +119,11 @@ class SearchDialogBaseTest(unittest.TestCase): 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): @@ -138,23 +139,27 @@ class SearchDialogBaseTest(unittest.TestCase): # 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 = self.root + 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.top = self.root 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': + 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 3d26d62..8bf9d47 100644 --- a/Lib/idlelib/idle_test/test_searchengine.py +++ b/Lib/idlelib/idle_test/test_searchengine.py @@ -1,19 +1,18 @@ -"Test searchengine, coverage 99%." - -from idlelib import searchengine as se -import unittest -# from test.support import requires -from tkinter import BooleanVar, StringVar, TclError # ,Tk, Text -import tkinter.messagebox as tkMessageBox -from idlelib.idle_test.mock_tk import Var, Mbox -from idlelib.idle_test.mock_tk import Text as mockText -import re +'''Test functions and SearchEngine class in SearchEngine.py.''' # With mock replacements, the module does not use any gui widgets. # The use of tk.Text is avoided (for now, until mock Text is improved) # by patching instances with an index function returning what is needed. # This works because mock Text.get does not use .index. -# The tkinter imports are used to restore searchengine. + +import re +import unittest +#from test.test_support import requires +from Tkinter import BooleanVar, StringVar, TclError # ,Tk, Text +import tkMessageBox +from idlelib import SearchEngine as se +from idlelib.idle_test.mock_tk import Var, Mbox +from idlelib.idle_test.mock_tk import Text as mockText def setUpModule(): # Replace s-e module tkinter imports other than non-gui TclError. @@ -140,10 +139,10 @@ class SearchEngineTest(unittest.TestCase): def test_setcookedpat(self): engine = self.engine - engine.setcookedpat(r'\s') - self.assertEqual(engine.getpat(), r'\s') + engine.setcookedpat('\s') + self.assertEqual(engine.getpat(), '\s') engine.revar.set(1) - engine.setcookedpat(r'\s') + engine.setcookedpat('\s') self.assertEqual(engine.getpat(), r'\\s') def test_getcookedpat(self): @@ -157,10 +156,10 @@ class SearchEngineTest(unittest.TestCase): Equal(engine.getcookedpat(), r'\bhello\b') engine.wordvar.set(False) - engine.setpat(r'\s') + engine.setpat('\s') Equal(engine.getcookedpat(), r'\\s') engine.revar.set(True) - Equal(engine.getcookedpat(), r'\s') + Equal(engine.getcookedpat(), '\s') def test_getprog(self): engine = self.engine @@ -179,7 +178,7 @@ class SearchEngineTest(unittest.TestCase): engine.revar.set(1) Equal(engine.getprog(), None) self.assertEqual(Mbox.showerror.message, - 'Error: nothing to repeat at position 0\nPattern: +') + 'Error: nothing to repeat\nPattern: +') def test_report_error(self): showerror = Mbox.showerror @@ -283,7 +282,7 @@ class ForwardBackwardTest(unittest.TestCase): cls.pat = re.compile('target') cls.res = (2, (10, 16)) # line, slice indexes of 'target' cls.failpat = re.compile('xyz') # not in text - cls.emptypat = re.compile(r'\w*') # empty match possible + cls.emptypat = re.compile('\w*') # empty match possible def make_search(self, func): def search(pat, line, col, wrap, ok=0): @@ -327,4 +326,4 @@ class ForwardBackwardTest(unittest.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, exit=2) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py deleted file mode 100644 index 0f5b4c7..0000000 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ /dev/null @@ -1,375 +0,0 @@ -"""Test sidebar, coverage 93%""" -import idlelib.sidebar -from sys import platform -from itertools import chain -import unittest -import unittest.mock -from test.support import requires -import tkinter as tk - -from idlelib.delegator import Delegator -from idlelib.percolator import Percolator - - -class Dummy_editwin: - def __init__(self, text): - self.text = text - self.text_frame = self.text.master - self.per = Percolator(text) - self.undo = Delegator() - self.per.insertfilter(self.undo) - - def setvar(self, name, value): - pass - - def getlineno(self, index): - return int(float(self.text.index(index))) - - -class LineNumbersTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = tk.Tk() - - cls.text_frame = tk.Frame(cls.root) - cls.text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - cls.text_frame.rowconfigure(1, weight=1) - cls.text_frame.columnconfigure(1, weight=1) - - cls.text = tk.Text(cls.text_frame, width=80, height=24, wrap=tk.NONE) - cls.text.grid(row=1, column=1, sticky=tk.NSEW) - - cls.editwin = Dummy_editwin(cls.text) - cls.editwin.vbar = tk.Scrollbar(cls.text_frame) - - @classmethod - def tearDownClass(cls): - cls.editwin.per.close() - cls.root.update() - cls.root.destroy() - del cls.text, cls.text_frame, cls.editwin, cls.root - - def setUp(self): - self.linenumber = idlelib.sidebar.LineNumbers(self.editwin) - - self.highlight_cfg = {"background": '#abcdef', - "foreground": '#123456'} - orig_idleConf_GetHighlight = idlelib.sidebar.idleConf.GetHighlight - def mock_idleconf_GetHighlight(theme, element): - if element == 'linenumber': - return self.highlight_cfg - return orig_idleConf_GetHighlight(theme, element) - GetHighlight_patcher = unittest.mock.patch.object( - idlelib.sidebar.idleConf, 'GetHighlight', mock_idleconf_GetHighlight) - GetHighlight_patcher.start() - self.addCleanup(GetHighlight_patcher.stop) - - self.font_override = 'TkFixedFont' - def mock_idleconf_GetFont(root, configType, section): - return self.font_override - GetFont_patcher = unittest.mock.patch.object( - idlelib.sidebar.idleConf, 'GetFont', mock_idleconf_GetFont) - GetFont_patcher.start() - self.addCleanup(GetFont_patcher.stop) - - def tearDown(self): - self.text.delete('1.0', 'end') - - def get_selection(self): - return tuple(map(str, self.text.tag_ranges('sel'))) - - def get_line_screen_position(self, line): - bbox = self.linenumber.sidebar_text.bbox(f'{line}.end -1c') - x = bbox[0] + 2 - y = bbox[1] + 2 - return x, y - - def assert_state_disabled(self): - state = self.linenumber.sidebar_text.config()['state'] - self.assertEqual(state[-1], tk.DISABLED) - - def get_sidebar_text_contents(self): - return self.linenumber.sidebar_text.get('1.0', tk.END) - - def assert_sidebar_n_lines(self, n_lines): - expected = '\n'.join(chain(map(str, range(1, n_lines + 1)), [''])) - self.assertEqual(self.get_sidebar_text_contents(), expected) - - def assert_text_equals(self, expected): - return self.assertEqual(self.text.get('1.0', 'end'), expected) - - def test_init_empty(self): - self.assert_sidebar_n_lines(1) - - def test_init_not_empty(self): - self.text.insert('insert', 'foo bar\n'*3) - self.assert_text_equals('foo bar\n'*3 + '\n') - self.assert_sidebar_n_lines(4) - - def test_toggle_linenumbering(self): - self.assertEqual(self.linenumber.is_shown, False) - self.linenumber.show_sidebar() - self.assertEqual(self.linenumber.is_shown, True) - self.linenumber.hide_sidebar() - self.assertEqual(self.linenumber.is_shown, False) - self.linenumber.hide_sidebar() - self.assertEqual(self.linenumber.is_shown, False) - self.linenumber.show_sidebar() - self.assertEqual(self.linenumber.is_shown, True) - self.linenumber.show_sidebar() - self.assertEqual(self.linenumber.is_shown, True) - - def test_insert(self): - self.text.insert('insert', 'foobar') - self.assert_text_equals('foobar\n') - self.assert_sidebar_n_lines(1) - self.assert_state_disabled() - - self.text.insert('insert', '\nfoo') - self.assert_text_equals('foobar\nfoo\n') - self.assert_sidebar_n_lines(2) - self.assert_state_disabled() - - self.text.insert('insert', 'hello\n'*2) - self.assert_text_equals('foobar\nfoohello\nhello\n\n') - self.assert_sidebar_n_lines(4) - self.assert_state_disabled() - - self.text.insert('insert', '\nworld') - self.assert_text_equals('foobar\nfoohello\nhello\n\nworld\n') - self.assert_sidebar_n_lines(5) - self.assert_state_disabled() - - def test_delete(self): - self.text.insert('insert', 'foobar') - self.assert_text_equals('foobar\n') - self.text.delete('1.1', '1.3') - self.assert_text_equals('fbar\n') - self.assert_sidebar_n_lines(1) - self.assert_state_disabled() - - self.text.insert('insert', 'foo\n'*2) - self.assert_text_equals('fbarfoo\nfoo\n\n') - self.assert_sidebar_n_lines(3) - self.assert_state_disabled() - - # Note: deleting up to "2.end" doesn't delete the final newline. - self.text.delete('2.0', '2.end') - self.assert_text_equals('fbarfoo\n\n\n') - self.assert_sidebar_n_lines(3) - self.assert_state_disabled() - - self.text.delete('1.3', 'end') - self.assert_text_equals('fba\n') - self.assert_sidebar_n_lines(1) - self.assert_state_disabled() - - # Note: Text widgets always keep a single '\n' character at the end. - self.text.delete('1.0', 'end') - self.assert_text_equals('\n') - self.assert_sidebar_n_lines(1) - self.assert_state_disabled() - - def test_sidebar_text_width(self): - """ - Test that linenumber text widget is always at the minimum - width - """ - def get_width(): - return self.linenumber.sidebar_text.config()['width'][-1] - - self.assert_sidebar_n_lines(1) - self.assertEqual(get_width(), 1) - - self.text.insert('insert', 'foo') - self.assert_sidebar_n_lines(1) - self.assertEqual(get_width(), 1) - - self.text.insert('insert', 'foo\n'*8) - self.assert_sidebar_n_lines(9) - self.assertEqual(get_width(), 1) - - self.text.insert('insert', 'foo\n') - self.assert_sidebar_n_lines(10) - self.assertEqual(get_width(), 2) - - self.text.insert('insert', 'foo\n') - self.assert_sidebar_n_lines(11) - self.assertEqual(get_width(), 2) - - self.text.delete('insert -1l linestart', 'insert linestart') - self.assert_sidebar_n_lines(10) - self.assertEqual(get_width(), 2) - - self.text.delete('insert -1l linestart', 'insert linestart') - self.assert_sidebar_n_lines(9) - self.assertEqual(get_width(), 1) - - self.text.insert('insert', 'foo\n'*90) - self.assert_sidebar_n_lines(99) - self.assertEqual(get_width(), 2) - - self.text.insert('insert', 'foo\n') - self.assert_sidebar_n_lines(100) - self.assertEqual(get_width(), 3) - - self.text.insert('insert', 'foo\n') - self.assert_sidebar_n_lines(101) - self.assertEqual(get_width(), 3) - - self.text.delete('insert -1l linestart', 'insert linestart') - self.assert_sidebar_n_lines(100) - self.assertEqual(get_width(), 3) - - self.text.delete('insert -1l linestart', 'insert linestart') - self.assert_sidebar_n_lines(99) - self.assertEqual(get_width(), 2) - - self.text.delete('50.0 -1c', 'end -1c') - self.assert_sidebar_n_lines(49) - self.assertEqual(get_width(), 2) - - self.text.delete('5.0 -1c', 'end -1c') - self.assert_sidebar_n_lines(4) - self.assertEqual(get_width(), 1) - - # Note: Text widgets always keep a single '\n' character at the end. - self.text.delete('1.0', 'end -1c') - self.assert_sidebar_n_lines(1) - self.assertEqual(get_width(), 1) - - def test_click_selection(self): - self.linenumber.show_sidebar() - self.text.insert('1.0', 'one\ntwo\nthree\nfour\n') - self.root.update() - - # Click on the second line. - x, y = self.get_line_screen_position(2) - self.linenumber.sidebar_text.event_generate('<Button-1>', x=x, y=y) - self.linenumber.sidebar_text.update() - self.root.update() - - self.assertEqual(self.get_selection(), ('2.0', '3.0')) - - def simulate_drag(self, start_line, end_line): - start_x, start_y = self.get_line_screen_position(start_line) - end_x, end_y = self.get_line_screen_position(end_line) - - self.linenumber.sidebar_text.event_generate('<Button-1>', - x=start_x, y=start_y) - self.root.update() - - def lerp(a, b, steps): - """linearly interpolate from a to b (inclusive) in equal steps""" - last_step = steps - 1 - for i in range(steps): - yield ((last_step - i) / last_step) * a + (i / last_step) * b - - for x, y in zip( - map(int, lerp(start_x, end_x, steps=11)), - map(int, lerp(start_y, end_y, steps=11)), - ): - self.linenumber.sidebar_text.event_generate('<B1-Motion>', x=x, y=y) - self.root.update() - - self.linenumber.sidebar_text.event_generate('<ButtonRelease-1>', - x=end_x, y=end_y) - self.root.update() - - def test_drag_selection_down(self): - self.linenumber.show_sidebar() - self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') - self.root.update() - - # Drag from the second line to the fourth line. - self.simulate_drag(2, 4) - self.assertEqual(self.get_selection(), ('2.0', '5.0')) - - def test_drag_selection_up(self): - self.linenumber.show_sidebar() - self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') - self.root.update() - - # Drag from the fourth line to the second line. - self.simulate_drag(4, 2) - self.assertEqual(self.get_selection(), ('2.0', '5.0')) - - def test_scroll(self): - self.linenumber.show_sidebar() - self.text.insert('1.0', 'line\n' * 100) - self.root.update() - - # Scroll down 10 lines. - self.text.yview_scroll(10, 'unit') - self.root.update() - self.assertEqual(self.text.index('@0,0'), '11.0') - self.assertEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0') - - # Generate a mouse-wheel event and make sure it scrolled up or down. - # The meaning of the "delta" is OS-dependant, so this just checks for - # any change. - self.linenumber.sidebar_text.event_generate('<MouseWheel>', - x=0, y=0, - delta=10) - self.root.update() - self.assertNotEqual(self.text.index('@0,0'), '11.0') - self.assertNotEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0') - - def test_font(self): - ln = self.linenumber - - orig_font = ln.sidebar_text['font'] - test_font = 'TkTextFont' - self.assertNotEqual(orig_font, test_font) - - # Ensure line numbers aren't shown. - ln.hide_sidebar() - - self.font_override = test_font - # Nothing breaks when line numbers aren't shown. - ln.update_font() - - # Activate line numbers, previous font change is immediately effective. - ln.show_sidebar() - self.assertEqual(ln.sidebar_text['font'], test_font) - - # Call the font update with line numbers shown, change is picked up. - self.font_override = orig_font - ln.update_font() - self.assertEqual(ln.sidebar_text['font'], orig_font) - - def test_highlight_colors(self): - ln = self.linenumber - - orig_colors = dict(self.highlight_cfg) - test_colors = {'background': '#222222', 'foreground': '#ffff00'} - - def assert_colors_are_equal(colors): - self.assertEqual(ln.sidebar_text['background'], colors['background']) - self.assertEqual(ln.sidebar_text['foreground'], colors['foreground']) - - # Ensure line numbers aren't shown. - ln.hide_sidebar() - - self.highlight_cfg = test_colors - # Nothing breaks with inactive code context. - ln.update_colors() - - # Show line numbers, previous colors change is immediately effective. - ln.show_sidebar() - assert_colors_are_equal(test_colors) - - # Call colors update with no change to the configured colors. - ln.update_colors() - assert_colors_are_equal(test_colors) - - # Call the colors update with line numbers shown, change is picked up. - self.highlight_cfg = orig_colors - ln.update_colors() - assert_colors_are_equal(orig_colors) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_squeezer.py b/Lib/idlelib/idle_test/test_squeezer.py deleted file mode 100644 index 1af2ce8..0000000 --- a/Lib/idlelib/idle_test/test_squeezer.py +++ /dev/null @@ -1,476 +0,0 @@ -"Test squeezer, coverage 95%" - -from collections import namedtuple -from textwrap import dedent -from tkinter import Text, Tk -import unittest -from unittest.mock import Mock, NonCallableMagicMock, patch, sentinel, ANY -from test.support import requires - -from idlelib.config import idleConf -from idlelib.squeezer import count_lines_with_wrapping, ExpandingButton, \ - Squeezer -from idlelib import macosx -from idlelib.textview import view_text -from idlelib.tooltip import Hovertip -from idlelib.pyshell import PyShell - - -SENTINEL_VALUE = sentinel.SENTINEL_VALUE - - -def get_test_tk_root(test_instance): - """Helper for tests: Create a root Tk object.""" - requires('gui') - root = Tk() - root.withdraw() - - def cleanup_root(): - root.update_idletasks() - root.destroy() - test_instance.addCleanup(cleanup_root) - - return root - - -class CountLinesTest(unittest.TestCase): - """Tests for the count_lines_with_wrapping function.""" - def check(self, expected, text, linewidth): - return self.assertEqual( - expected, - count_lines_with_wrapping(text, linewidth), - ) - - def test_count_empty(self): - """Test with an empty string.""" - self.assertEqual(count_lines_with_wrapping(""), 0) - - def test_count_begins_with_empty_line(self): - """Test with a string which begins with a newline.""" - self.assertEqual(count_lines_with_wrapping("\ntext"), 2) - - def test_count_ends_with_empty_line(self): - """Test with a string which ends with a newline.""" - self.assertEqual(count_lines_with_wrapping("text\n"), 1) - - def test_count_several_lines(self): - """Test with several lines of text.""" - self.assertEqual(count_lines_with_wrapping("1\n2\n3\n"), 3) - - def test_empty_lines(self): - self.check(expected=1, text='\n', linewidth=80) - self.check(expected=2, text='\n\n', linewidth=80) - self.check(expected=10, text='\n' * 10, linewidth=80) - - def test_long_line(self): - self.check(expected=3, text='a' * 200, linewidth=80) - self.check(expected=3, text='a' * 200 + '\n', linewidth=80) - - def test_several_lines_different_lengths(self): - text = dedent("""\ - 13 characters - 43 is the number of characters on this line - - 7 chars - 13 characters""") - self.check(expected=5, text=text, linewidth=80) - self.check(expected=5, text=text + '\n', linewidth=80) - self.check(expected=6, text=text, linewidth=40) - self.check(expected=7, text=text, linewidth=20) - self.check(expected=11, text=text, linewidth=10) - - -class SqueezerTest(unittest.TestCase): - """Tests for the Squeezer class.""" - def make_mock_editor_window(self, with_text_widget=False): - """Create a mock EditorWindow instance.""" - editwin = NonCallableMagicMock() - editwin.width = 80 - - if with_text_widget: - editwin.root = get_test_tk_root(self) - text_widget = self.make_text_widget(root=editwin.root) - editwin.text = editwin.per.bottom = text_widget - - return editwin - - def make_squeezer_instance(self, editor_window=None): - """Create an actual Squeezer instance with a mock EditorWindow.""" - if editor_window is None: - editor_window = self.make_mock_editor_window() - squeezer = Squeezer(editor_window) - return squeezer - - def make_text_widget(self, root=None): - if root is None: - root = get_test_tk_root(self) - text_widget = Text(root) - text_widget["font"] = ('Courier', 10) - text_widget.mark_set("iomark", "1.0") - return text_widget - - def set_idleconf_option_with_cleanup(self, configType, section, option, value): - prev_val = idleConf.GetOption(configType, section, option) - idleConf.SetOption(configType, section, option, value) - self.addCleanup(idleConf.SetOption, - configType, section, option, prev_val) - - def test_count_lines(self): - """Test Squeezer.count_lines() with various inputs.""" - editwin = self.make_mock_editor_window() - squeezer = self.make_squeezer_instance(editwin) - - for text_code, line_width, expected in [ - (r"'\n'", 80, 1), - (r"'\n' * 3", 80, 3), - (r"'a' * 40 + '\n'", 80, 1), - (r"'a' * 80 + '\n'", 80, 1), - (r"'a' * 200 + '\n'", 80, 3), - (r"'aa\t' * 20", 80, 2), - (r"'aa\t' * 21", 80, 3), - (r"'aa\t' * 20", 40, 4), - ]: - with self.subTest(text_code=text_code, - line_width=line_width, - expected=expected): - text = eval(text_code) - with patch.object(editwin, 'width', line_width): - self.assertEqual(squeezer.count_lines(text), expected) - - def test_init(self): - """Test the creation of Squeezer instances.""" - editwin = self.make_mock_editor_window() - squeezer = self.make_squeezer_instance(editwin) - self.assertIs(squeezer.editwin, editwin) - self.assertEqual(squeezer.expandingbuttons, []) - - def test_write_no_tags(self): - """Test Squeezer's overriding of the EditorWindow's write() method.""" - editwin = self.make_mock_editor_window() - for text in ['', 'TEXT', 'LONG TEXT' * 1000, 'MANY_LINES\n' * 100]: - editwin.write = orig_write = Mock(return_value=SENTINEL_VALUE) - squeezer = self.make_squeezer_instance(editwin) - - self.assertEqual(squeezer.editwin.write(text, ()), SENTINEL_VALUE) - self.assertEqual(orig_write.call_count, 1) - orig_write.assert_called_with(text, ()) - self.assertEqual(len(squeezer.expandingbuttons), 0) - - def test_write_not_stdout(self): - """Test Squeezer's overriding of the EditorWindow's write() method.""" - for text in ['', 'TEXT', 'LONG TEXT' * 1000, 'MANY_LINES\n' * 100]: - editwin = self.make_mock_editor_window() - editwin.write.return_value = SENTINEL_VALUE - orig_write = editwin.write - squeezer = self.make_squeezer_instance(editwin) - - self.assertEqual(squeezer.editwin.write(text, "stderr"), - SENTINEL_VALUE) - self.assertEqual(orig_write.call_count, 1) - orig_write.assert_called_with(text, "stderr") - self.assertEqual(len(squeezer.expandingbuttons), 0) - - def test_write_stdout(self): - """Test Squeezer's overriding of the EditorWindow's write() method.""" - editwin = self.make_mock_editor_window() - - for text in ['', 'TEXT']: - editwin.write = orig_write = Mock(return_value=SENTINEL_VALUE) - squeezer = self.make_squeezer_instance(editwin) - squeezer.auto_squeeze_min_lines = 50 - - self.assertEqual(squeezer.editwin.write(text, "stdout"), - SENTINEL_VALUE) - self.assertEqual(orig_write.call_count, 1) - orig_write.assert_called_with(text, "stdout") - self.assertEqual(len(squeezer.expandingbuttons), 0) - - for text in ['LONG TEXT' * 1000, 'MANY_LINES\n' * 100]: - editwin.write = orig_write = Mock(return_value=SENTINEL_VALUE) - squeezer = self.make_squeezer_instance(editwin) - squeezer.auto_squeeze_min_lines = 50 - - self.assertEqual(squeezer.editwin.write(text, "stdout"), None) - self.assertEqual(orig_write.call_count, 0) - self.assertEqual(len(squeezer.expandingbuttons), 1) - - def test_auto_squeeze(self): - """Test that the auto-squeezing creates an ExpandingButton properly.""" - editwin = self.make_mock_editor_window(with_text_widget=True) - text_widget = editwin.text - squeezer = self.make_squeezer_instance(editwin) - squeezer.auto_squeeze_min_lines = 5 - squeezer.count_lines = Mock(return_value=6) - - editwin.write('TEXT\n'*6, "stdout") - self.assertEqual(text_widget.get('1.0', 'end'), '\n') - self.assertEqual(len(squeezer.expandingbuttons), 1) - - def test_squeeze_current_text_event(self): - """Test the squeeze_current_text event.""" - # Squeezing text should work for both stdout and stderr. - for tag_name in ["stdout", "stderr"]: - editwin = self.make_mock_editor_window(with_text_widget=True) - text_widget = editwin.text - squeezer = self.make_squeezer_instance(editwin) - squeezer.count_lines = Mock(return_value=6) - - # Prepare some text in the Text widget. - text_widget.insert("1.0", "SOME\nTEXT\n", tag_name) - text_widget.mark_set("insert", "1.0") - self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n') - - self.assertEqual(len(squeezer.expandingbuttons), 0) - - # Test squeezing the current text. - retval = squeezer.squeeze_current_text_event(event=Mock()) - self.assertEqual(retval, "break") - self.assertEqual(text_widget.get('1.0', 'end'), '\n\n') - self.assertEqual(len(squeezer.expandingbuttons), 1) - self.assertEqual(squeezer.expandingbuttons[0].s, 'SOME\nTEXT') - - # Test that expanding the squeezed text works and afterwards - # the Text widget contains the original text. - squeezer.expandingbuttons[0].expand(event=Mock()) - self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n') - self.assertEqual(len(squeezer.expandingbuttons), 0) - - def test_squeeze_current_text_event_no_allowed_tags(self): - """Test that the event doesn't squeeze text without a relevant tag.""" - editwin = self.make_mock_editor_window(with_text_widget=True) - text_widget = editwin.text - squeezer = self.make_squeezer_instance(editwin) - squeezer.count_lines = Mock(return_value=6) - - # Prepare some text in the Text widget. - text_widget.insert("1.0", "SOME\nTEXT\n", "TAG") - text_widget.mark_set("insert", "1.0") - self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n') - - self.assertEqual(len(squeezer.expandingbuttons), 0) - - # Test squeezing the current text. - retval = squeezer.squeeze_current_text_event(event=Mock()) - self.assertEqual(retval, "break") - self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n') - self.assertEqual(len(squeezer.expandingbuttons), 0) - - def test_squeeze_text_before_existing_squeezed_text(self): - """Test squeezing text before existing squeezed text.""" - editwin = self.make_mock_editor_window(with_text_widget=True) - text_widget = editwin.text - squeezer = self.make_squeezer_instance(editwin) - squeezer.count_lines = Mock(return_value=6) - - # Prepare some text in the Text widget and squeeze it. - text_widget.insert("1.0", "SOME\nTEXT\n", "stdout") - text_widget.mark_set("insert", "1.0") - squeezer.squeeze_current_text_event(event=Mock()) - self.assertEqual(len(squeezer.expandingbuttons), 1) - - # Test squeezing the current text. - text_widget.insert("1.0", "MORE\nSTUFF\n", "stdout") - text_widget.mark_set("insert", "1.0") - retval = squeezer.squeeze_current_text_event(event=Mock()) - self.assertEqual(retval, "break") - self.assertEqual(text_widget.get('1.0', 'end'), '\n\n\n') - self.assertEqual(len(squeezer.expandingbuttons), 2) - self.assertTrue(text_widget.compare( - squeezer.expandingbuttons[0], - '<', - squeezer.expandingbuttons[1], - )) - - def test_reload(self): - """Test the reload() class-method.""" - editwin = self.make_mock_editor_window(with_text_widget=True) - squeezer = self.make_squeezer_instance(editwin) - - orig_auto_squeeze_min_lines = squeezer.auto_squeeze_min_lines - - # Increase auto-squeeze-min-lines. - new_auto_squeeze_min_lines = orig_auto_squeeze_min_lines + 10 - self.set_idleconf_option_with_cleanup( - 'main', 'PyShell', 'auto-squeeze-min-lines', - str(new_auto_squeeze_min_lines)) - - Squeezer.reload() - self.assertEqual(squeezer.auto_squeeze_min_lines, - new_auto_squeeze_min_lines) - - def test_reload_no_squeezer_instances(self): - """Test that Squeezer.reload() runs without any instances existing.""" - Squeezer.reload() - - -class ExpandingButtonTest(unittest.TestCase): - """Tests for the ExpandingButton class.""" - # In these tests the squeezer instance is a mock, but actual tkinter - # Text and Button instances are created. - def make_mock_squeezer(self): - """Helper for tests: Create a mock Squeezer object.""" - root = get_test_tk_root(self) - squeezer = Mock() - squeezer.editwin.text = Text(root) - - # Set default values for the configuration settings. - squeezer.auto_squeeze_min_lines = 50 - return squeezer - - @patch('idlelib.squeezer.Hovertip', autospec=Hovertip) - def test_init(self, MockHovertip): - """Test the simplest creation of an ExpandingButton.""" - squeezer = self.make_mock_squeezer() - text_widget = squeezer.editwin.text - - expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer) - self.assertEqual(expandingbutton.s, 'TEXT') - - # Check that the underlying tkinter.Button is properly configured. - self.assertEqual(expandingbutton.master, text_widget) - self.assertTrue('50 lines' in expandingbutton.cget('text')) - - # Check that the text widget still contains no text. - self.assertEqual(text_widget.get('1.0', 'end'), '\n') - - # Check that the mouse events are bound. - self.assertIn('<Double-Button-1>', expandingbutton.bind()) - right_button_code = '<Button-%s>' % ('2' if macosx.isAquaTk() else '3') - self.assertIn(right_button_code, expandingbutton.bind()) - - # Check that ToolTip was called once, with appropriate values. - self.assertEqual(MockHovertip.call_count, 1) - MockHovertip.assert_called_with(expandingbutton, ANY, hover_delay=ANY) - - # Check that 'right-click' appears in the tooltip text. - tooltip_text = MockHovertip.call_args[0][1] - self.assertIn('right-click', tooltip_text.lower()) - - def test_expand(self): - """Test the expand event.""" - squeezer = self.make_mock_squeezer() - expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer) - - # Insert the button into the text widget - # (this is normally done by the Squeezer class). - text_widget = expandingbutton.text - text_widget.window_create("1.0", window=expandingbutton) - - # Set base_text to the text widget, so that changes are actually - # made to it (by ExpandingButton) and we can inspect these - # changes afterwards. - expandingbutton.base_text = expandingbutton.text - - # trigger the expand event - retval = expandingbutton.expand(event=Mock()) - self.assertEqual(retval, None) - - # Check that the text was inserted into the text widget. - self.assertEqual(text_widget.get('1.0', 'end'), 'TEXT\n') - - # Check that the 'TAGS' tag was set on the inserted text. - text_end_index = text_widget.index('end-1c') - self.assertEqual(text_widget.get('1.0', text_end_index), 'TEXT') - self.assertEqual(text_widget.tag_nextrange('TAGS', '1.0'), - ('1.0', text_end_index)) - - # Check that the button removed itself from squeezer.expandingbuttons. - self.assertEqual(squeezer.expandingbuttons.remove.call_count, 1) - squeezer.expandingbuttons.remove.assert_called_with(expandingbutton) - - def test_expand_dangerous_oupput(self): - """Test that expanding very long output asks user for confirmation.""" - squeezer = self.make_mock_squeezer() - text = 'a' * 10**5 - expandingbutton = ExpandingButton(text, 'TAGS', 50, squeezer) - expandingbutton.set_is_dangerous() - self.assertTrue(expandingbutton.is_dangerous) - - # Insert the button into the text widget - # (this is normally done by the Squeezer class). - text_widget = expandingbutton.text - text_widget.window_create("1.0", window=expandingbutton) - - # Set base_text to the text widget, so that changes are actually - # made to it (by ExpandingButton) and we can inspect these - # changes afterwards. - expandingbutton.base_text = expandingbutton.text - - # Patch the message box module to always return False. - with patch('idlelib.squeezer.tkMessageBox') as mock_msgbox: - mock_msgbox.askokcancel.return_value = False - mock_msgbox.askyesno.return_value = False - # Trigger the expand event. - retval = expandingbutton.expand(event=Mock()) - - # Check that the event chain was broken and no text was inserted. - self.assertEqual(retval, 'break') - self.assertEqual(expandingbutton.text.get('1.0', 'end-1c'), '') - - # Patch the message box module to always return True. - with patch('idlelib.squeezer.tkMessageBox') as mock_msgbox: - mock_msgbox.askokcancel.return_value = True - mock_msgbox.askyesno.return_value = True - # Trigger the expand event. - retval = expandingbutton.expand(event=Mock()) - - # Check that the event chain wasn't broken and the text was inserted. - self.assertEqual(retval, None) - self.assertEqual(expandingbutton.text.get('1.0', 'end-1c'), text) - - def test_copy(self): - """Test the copy event.""" - # Testing with the actual clipboard proved problematic, so this - # test replaces the clipboard manipulation functions with mocks - # and checks that they are called appropriately. - squeezer = self.make_mock_squeezer() - expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer) - expandingbutton.clipboard_clear = Mock() - expandingbutton.clipboard_append = Mock() - - # Trigger the copy event. - retval = expandingbutton.copy(event=Mock()) - self.assertEqual(retval, None) - - # Vheck that the expanding button called clipboard_clear() and - # clipboard_append('TEXT') once each. - self.assertEqual(expandingbutton.clipboard_clear.call_count, 1) - self.assertEqual(expandingbutton.clipboard_append.call_count, 1) - expandingbutton.clipboard_append.assert_called_with('TEXT') - - def test_view(self): - """Test the view event.""" - squeezer = self.make_mock_squeezer() - expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer) - expandingbutton.selection_own = Mock() - - with patch('idlelib.squeezer.view_text', autospec=view_text)\ - as mock_view_text: - # Trigger the view event. - expandingbutton.view(event=Mock()) - - # Check that the expanding button called view_text. - self.assertEqual(mock_view_text.call_count, 1) - - # Check that the proper text was passed. - self.assertEqual(mock_view_text.call_args[0][2], 'TEXT') - - def test_rmenu(self): - """Test the context menu.""" - squeezer = self.make_mock_squeezer() - expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer) - with patch('tkinter.Menu') as mock_Menu: - mock_menu = Mock() - mock_Menu.return_value = mock_menu - mock_event = Mock() - mock_event.x = 10 - mock_event.y = 10 - expandingbutton.context_menu_event(event=mock_event) - self.assertEqual(mock_menu.add_command.call_count, - len(expandingbutton.rmenu_specs)) - for label, *data in expandingbutton.rmenu_specs: - mock_menu.add_command.assert_any_call(label=label, command=ANY) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_stackviewer.py b/Lib/idlelib/idle_test/test_stackviewer.py deleted file mode 100644 index 98f53f9..0000000 --- a/Lib/idlelib/idle_test/test_stackviewer.py +++ /dev/null @@ -1,47 +0,0 @@ -"Test stackviewer, coverage 63%." - -from idlelib import stackviewer -import unittest -from test.support import requires -from tkinter import Tk - -from idlelib.tree import TreeNode, ScrolledCanvas -import sys - - -class StackBrowserTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - svs = stackviewer.sys - try: - abc - except NameError: - svs.last_type, svs.last_value, svs.last_traceback = ( - sys.exc_info()) - - requires('gui') - cls.root = Tk() - cls.root.withdraw() - - @classmethod - def tearDownClass(cls): - svs = stackviewer.sys - del svs.last_traceback, svs.last_type, svs.last_value - - cls.root.update_idletasks() -## for id in cls.root.tk.call('after', 'info'): -## cls.root.after_cancel(id) # Need for EditorWindow. - cls.root.destroy() - del cls.root - - def test_init(self): - sb = stackviewer.StackBrowser(self.root) - isi = self.assertIsInstance - isi(stackviewer.sc, ScrolledCanvas) - isi(stackviewer.item, stackviewer.StackTreeItem) - isi(stackviewer.node, TreeNode) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_statusbar.py b/Lib/idlelib/idle_test/test_statusbar.py deleted file mode 100644 index 203a57d..0000000 --- a/Lib/idlelib/idle_test/test_statusbar.py +++ /dev/null @@ -1,41 +0,0 @@ -"Test statusbar, coverage 100%." - -from idlelib import statusbar -import unittest -from test.support import requires -from tkinter import Tk - - -class Test(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - - @classmethod - def tearDownClass(cls): - cls.root.update_idletasks() - cls.root.destroy() - del cls.root - - def test_init(self): - bar = statusbar.MultiStatusBar(self.root) - self.assertEqual(bar.labels, {}) - - def test_set_label(self): - bar = statusbar.MultiStatusBar(self.root) - bar.set_label('left', text='sometext', width=10) - self.assertIn('left', bar.labels) - left = bar.labels['left'] - self.assertEqual(left['text'], 'sometext') - self.assertEqual(left['width'], 10) - bar.set_label('left', text='revised text') - self.assertEqual(left['text'], 'revised text') - bar.set_label('right', text='correct text') - self.assertEqual(bar.labels['right']['text'], 'correct text') - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_text.py b/Lib/idlelib/idle_test/test_text.py index 0f31179..50d3fac 100644 --- a/Lib/idlelib/idle_test/test_text.py +++ b/Lib/idlelib/idle_test/test_text.py @@ -1,19 +1,17 @@ -''' Test mock_tk.Text class against tkinter.Text class - -Run same tests with both by creating a mixin class. -''' +# Test mock_tk.Text class against tkinter.Text class by running same tests with both. import unittest -from test.support import requires +from test.test_support import requires + from _tkinter import TclError class TextTest(object): - "Define items common to both sets of tests." - hw = 'hello\nworld' # Several tests insert this after initialization. + hw = 'hello\nworld' # usual initial insert after initialization hwn = hw+'\n' # \n present at initialization, before insert - # setUpClass defines cls.Text and maybe cls.root. - # setUp defines self.text from Text and maybe root. + Text = None + def setUp(self): + self.text = self.Text() def test_init(self): self.assertEqual(self.text.get('1.0'), '\n') @@ -198,10 +196,6 @@ class MockTextTest(TextTest, unittest.TestCase): from idlelib.idle_test.mock_tk import Text cls.Text = Text - def setUp(self): - self.text = self.Text() - - def test_decode(self): # test endflags (-1, 0) not tested by test_index (which uses +1) decode = self.text._decode @@ -219,7 +213,7 @@ class TkTextTest(TextTest, unittest.TestCase): @classmethod def setUpClass(cls): requires('gui') - from tkinter import Tk, Text + from Tkinter import Tk, Text cls.Text = Text cls.root = Tk() @@ -228,9 +222,6 @@ class TkTextTest(TextTest, unittest.TestCase): cls.root.destroy() del cls.root - def setUp(self): - self.text = self.Text(self.root) - if __name__ == '__main__': unittest.main(verbosity=2, exit=False) diff --git a/Lib/idlelib/idle_test/test_textview.py b/Lib/idlelib/idle_test/test_textview.py index 7189378..fa437fc 100644 --- a/Lib/idlelib/idle_test/test_textview.py +++ b/Lib/idlelib/idle_test/test_textview.py @@ -1,232 +1,95 @@ -"""Test textview, coverage 100%. +'''Test the functions and main class method of textView.py.''' -Since all methods and functions create (or destroy) a ViewWindow, which -is a widget containing a widget, etcetera, all tests must be gui tests. -Using mock Text would not change this. Other mocks are used to retrieve -information about calls. -""" -from idlelib import textview as tv -from test.support import requires -requires('gui') - -import os import unittest -from tkinter import Tk, TclError, CHAR, NONE, WORD -from tkinter.ttk import Button +import os +from test.test_support import requires +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_func - -def setUpModule(): - global root - root = Tk() - root.withdraw() - -def tearDownModule(): - global root - root.update_idletasks() - root.destroy() - del root - -# If we call ViewWindow or wrapper functions with defaults -# modal=True, _utest=False, test hangs on call to wait_window. -# Have also gotten tk error 'can't invoke "event" command'. +from idlelib.idle_test.mock_tk import Mbox -class VW(tv.ViewWindow): # Used in ViewWindowTest. +class TV(tv.TextViewer): # Use in TextViewTest transient = Func() grab_set = Func() wait_window = Func() - -# Call wrapper class VW with mock wait_window. -class ViewWindowTest(unittest.TestCase): - - def setUp(self): - VW.transient.__init__() - VW.grab_set.__init__() - VW.wait_window.__init__() - - def test_init_modal(self): - view = VW(root, 'Title', 'test text') - self.assertTrue(VW.transient.called) - self.assertTrue(VW.grab_set.called) - self.assertTrue(VW.wait_window.called) - view.ok() - - def test_init_nonmodal(self): - view = VW(root, 'Title', 'test text', modal=False) - self.assertFalse(VW.transient.called) - self.assertFalse(VW.grab_set.called) - self.assertFalse(VW.wait_window.called) - view.ok() - - def test_ok(self): - view = VW(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 AutoHideScrollbarTest(unittest.TestCase): - # Method set is tested in ScrollableTextFrameTest - def test_forbidden_geometry(self): - scroll = tv.AutoHideScrollbar(root) - self.assertRaises(TclError, scroll.pack) - self.assertRaises(TclError, scroll.place) - - -class ScrollableTextFrameTest(unittest.TestCase): +class textviewClassTest(unittest.TestCase): @classmethod def setUpClass(cls): - cls.root = root = Tk() - root.withdraw() + requires('gui') + cls.root = Tk() + cls.root.withdraw() @classmethod def tearDownClass(cls): - cls.root.update_idletasks() cls.root.destroy() del cls.root - def make_frame(self, wrap=NONE, **kwargs): - frame = tv.ScrollableTextFrame(self.root, wrap=wrap, **kwargs) - def cleanup_frame(): - frame.update_idletasks() - frame.destroy() - self.addCleanup(cleanup_frame) - return frame - - def test_line1(self): - frame = self.make_frame() - frame.text.insert('1.0', 'test text') - self.assertEqual(frame.text.get('1.0', '1.end'), 'test text') + def setUp(self): + TV.transient.__init__() + TV.grab_set.__init__() + TV.wait_window.__init__() - def test_horiz_scrollbar(self): - # The horizontal scrollbar should be shown/hidden according to - # the 'wrap' setting: It should only be shown when 'wrap' is - # set to NONE. + def test_init_modal(self): + view = TV(self.root, 'Title', 'test text') + self.assertTrue(TV.transient.called) + self.assertTrue(TV.grab_set.called) + self.assertTrue(TV.wait_window.called) + view.Ok() - # wrap = NONE -> with horizontal scrolling - frame = self.make_frame(wrap=NONE) - self.assertEqual(frame.text.cget('wrap'), NONE) - self.assertIsNotNone(frame.xscroll) + def test_init_nonmodal(self): + view = TV(self.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() - # wrap != NONE -> no horizontal scrolling - for wrap in [CHAR, WORD]: - with self.subTest(wrap=wrap): - frame = self.make_frame(wrap=wrap) - self.assertEqual(frame.text.cget('wrap'), wrap) - self.assertIsNone(frame.xscroll) + def test_ok(self): + view = TV(self.root, 'Title', 'test text', modal=False) + view.destroy = Func() + view.Ok() + self.assertTrue(view.destroy.called) + del view.destroy # Unmask the real function. + view.destroy() -class ViewFrameTest(unittest.TestCase): +class ViewFunctionTest(unittest.TestCase): @classmethod def setUpClass(cls): - cls.root = root = Tk() - root.withdraw() - cls.frame = tv.ViewFrame(root, 'test text') + requires('gui') + cls.root = Tk() + cls.root.withdraw() + cls.orig_mbox = tv.tkMessageBox + tv.tkMessageBox = Mbox @classmethod def tearDownClass(cls): - del cls.frame - cls.root.update_idletasks() cls.root.destroy() del cls.root - - def test_line1(self): - get = self.frame.text.get - self.assertEqual(get('1.0', '1.end'), 'test text') - - -# Call ViewWindow with modal=False. -class ViewFunctionTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.orig_error = tv.showerror - tv.showerror = Mbox_func() - - @classmethod - def tearDownClass(cls): - tv.showerror = cls.orig_error - del cls.orig_error + tv.tkMessageBox = cls.orig_mbox + del cls.orig_mbox def test_view_text(self): - view = tv.view_text(root, 'Title', 'test text', modal=False) - self.assertIsInstance(view, tv.ViewWindow) - self.assertIsInstance(view.viewframe, tv.ViewFrame) - view.viewframe.ok() + # If modal True, get tkinter error 'can't invoke "event" command'. + view = tv.view_text(self.root, 'Title', 'test text', modal=False) + self.assertIsInstance(view, tv.TextViewer) + view.Ok() def test_view_file(self): - view = tv.view_file(root, 'Title', __file__, 'ascii', modal=False) - self.assertIsInstance(view, tv.ViewWindow) - self.assertIsInstance(view.viewframe, tv.ViewFrame) - get = view.viewframe.textframe.text.get - self.assertIn('Test', get('1.0', '1.end')) - view.ok() - - def test_bad_file(self): - # Mock showerror will be used; view_file will return None. - view = tv.view_file(root, 'Title', 'abc.xyz', 'ascii', modal=False) - self.assertIsNone(view) - self.assertEqual(tv.showerror.title, 'File Load Error') - - def test_bad_encoding(self): - p = os.path - fn = p.abspath(p.join(p.dirname(__file__), '..', 'CREDITS.txt')) - view = tv.view_file(root, 'Title', fn, 'ascii', modal=False) + test_dir = os.path.dirname(__file__) + testfile = os.path.join(test_dir, 'test_textview.py') + view = tv.view_file(self.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; view_file will return None. + testfile = os.path.join(test_dir, '../notthere.py') + view = tv.view_file(self.root, 'Title', testfile, modal=False) self.assertIsNone(view) - self.assertEqual(tv.showerror.title, 'Unicode Decode Error') - - def test_nowrap(self): - view = tv.view_text(root, 'Title', 'test', modal=False, wrap='none') - text_widget = view.viewframe.textframe.text - self.assertEqual(text_widget.cget('wrap'), 'none') - - -# Call ViewWindow with _utest=True. -class ButtonClickTest(unittest.TestCase): - - def setUp(self): - self.view = None - self.called = False - - def tearDown(self): - if self.view: - self.view.destroy() - - def test_view_text_bind_with_button(self): - def _command(): - self.called = True - self.view = tv.view_text(root, 'TITLE_TEXT', 'COMMAND', _utest=True) - button = Button(root, text='BUTTON', command=_command) - button.invoke() - self.addCleanup(button.destroy) - - self.assertEqual(self.called, True) - self.assertEqual(self.view.title(), 'TITLE_TEXT') - self.assertEqual(self.view.viewframe.textframe.text.get('1.0', '1.end'), - 'COMMAND') - - def test_view_file_bind_with_button(self): - def _command(): - self.called = True - self.view = tv.view_file(root, 'TITLE_FILE', __file__, - encoding='ascii', _utest=True) - button = Button(root, text='BUTTON', command=_command) - button.invoke() - self.addCleanup(button.destroy) - - self.assertEqual(self.called, True) - self.assertEqual(self.view.title(), 'TITLE_FILE') - get = self.view.viewframe.textframe.text.get - with open(__file__) as f: - self.assertEqual(get('1.0', '1.end'), f.readline().strip()) - f.readline() - self.assertEqual(get('3.0', '3.end'), f.readline().strip()) if __name__ == '__main__': diff --git a/Lib/idlelib/idle_test/test_tooltip.py b/Lib/idlelib/idle_test/test_tooltip.py deleted file mode 100644 index c616d4f..0000000 --- a/Lib/idlelib/idle_test/test_tooltip.py +++ /dev/null @@ -1,161 +0,0 @@ -"""Test tooltip, coverage 100%. - -Coverage is 100% after excluding 6 lines with "# pragma: no cover". -They involve TclErrors that either should or should not happen in a -particular situation, and which are 'pass'ed if they do. -""" - -from idlelib.tooltip import TooltipBase, Hovertip -from test.support import requires -requires('gui') - -from functools import wraps -import time -from tkinter import Button, Tk, Toplevel -import unittest - - -def setUpModule(): - global root - root = Tk() - -def tearDownModule(): - global root - root.update_idletasks() - root.destroy() - del root - - -def add_call_counting(func): - @wraps(func) - def wrapped_func(*args, **kwargs): - wrapped_func.call_args_list.append((args, kwargs)) - return func(*args, **kwargs) - wrapped_func.call_args_list = [] - return wrapped_func - - -def _make_top_and_button(testobj): - global root - top = Toplevel(root) - testobj.addCleanup(top.destroy) - top.title("Test tooltip") - button = Button(top, text='ToolTip test button') - button.pack() - testobj.addCleanup(button.destroy) - top.lift() - return top, button - - -class ToolTipBaseTest(unittest.TestCase): - def setUp(self): - self.top, self.button = _make_top_and_button(self) - - def test_base_class_is_unusable(self): - global root - top = Toplevel(root) - self.addCleanup(top.destroy) - - button = Button(top, text='ToolTip test button') - button.pack() - self.addCleanup(button.destroy) - - with self.assertRaises(NotImplementedError): - tooltip = TooltipBase(button) - tooltip.showtip() - - -class HovertipTest(unittest.TestCase): - def setUp(self): - self.top, self.button = _make_top_and_button(self) - - def is_tipwindow_shown(self, tooltip): - return tooltip.tipwindow and tooltip.tipwindow.winfo_viewable() - - def test_showtip(self): - tooltip = Hovertip(self.button, 'ToolTip text') - self.addCleanup(tooltip.hidetip) - self.assertFalse(self.is_tipwindow_shown(tooltip)) - tooltip.showtip() - self.assertTrue(self.is_tipwindow_shown(tooltip)) - - def test_showtip_twice(self): - tooltip = Hovertip(self.button, 'ToolTip text') - self.addCleanup(tooltip.hidetip) - self.assertFalse(self.is_tipwindow_shown(tooltip)) - tooltip.showtip() - self.assertTrue(self.is_tipwindow_shown(tooltip)) - orig_tipwindow = tooltip.tipwindow - tooltip.showtip() - self.assertTrue(self.is_tipwindow_shown(tooltip)) - self.assertIs(tooltip.tipwindow, orig_tipwindow) - - def test_hidetip(self): - tooltip = Hovertip(self.button, 'ToolTip text') - self.addCleanup(tooltip.hidetip) - tooltip.showtip() - tooltip.hidetip() - self.assertFalse(self.is_tipwindow_shown(tooltip)) - - def test_showtip_on_mouse_enter_no_delay(self): - tooltip = Hovertip(self.button, 'ToolTip text', hover_delay=None) - self.addCleanup(tooltip.hidetip) - tooltip.showtip = add_call_counting(tooltip.showtip) - root.update() - self.assertFalse(self.is_tipwindow_shown(tooltip)) - self.button.event_generate('<Enter>', x=0, y=0) - root.update() - self.assertTrue(self.is_tipwindow_shown(tooltip)) - self.assertGreater(len(tooltip.showtip.call_args_list), 0) - - def test_hover_with_delay(self): - # Run multiple tests requiring an actual delay simultaneously. - - # Test #1: A hover tip with a non-zero delay appears after the delay. - tooltip1 = Hovertip(self.button, 'ToolTip text', hover_delay=100) - self.addCleanup(tooltip1.hidetip) - tooltip1.showtip = add_call_counting(tooltip1.showtip) - root.update() - self.assertFalse(self.is_tipwindow_shown(tooltip1)) - self.button.event_generate('<Enter>', x=0, y=0) - root.update() - self.assertFalse(self.is_tipwindow_shown(tooltip1)) - - # Test #2: A hover tip with a non-zero delay doesn't appear when - # the mouse stops hovering over the base widget before the delay - # expires. - tooltip2 = Hovertip(self.button, 'ToolTip text', hover_delay=100) - self.addCleanup(tooltip2.hidetip) - tooltip2.showtip = add_call_counting(tooltip2.showtip) - root.update() - self.button.event_generate('<Enter>', x=0, y=0) - root.update() - self.button.event_generate('<Leave>', x=0, y=0) - root.update() - - time.sleep(0.15) - root.update() - - # Test #1 assertions. - self.assertTrue(self.is_tipwindow_shown(tooltip1)) - self.assertGreater(len(tooltip1.showtip.call_args_list), 0) - - # Test #2 assertions. - self.assertFalse(self.is_tipwindow_shown(tooltip2)) - self.assertEqual(tooltip2.showtip.call_args_list, []) - - def test_hidetip_on_mouse_leave(self): - tooltip = Hovertip(self.button, 'ToolTip text', hover_delay=None) - self.addCleanup(tooltip.hidetip) - tooltip.showtip = add_call_counting(tooltip.showtip) - root.update() - self.button.event_generate('<Enter>', x=0, y=0) - root.update() - self.button.event_generate('<Leave>', x=0, y=0) - root.update() - self.assertFalse(self.is_tipwindow_shown(tooltip)) - self.assertGreater(len(tooltip.showtip.call_args_list), 0) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_tree.py b/Lib/idlelib/idle_test/test_tree.py deleted file mode 100644 index b3e4c10..0000000 --- a/Lib/idlelib/idle_test/test_tree.py +++ /dev/null @@ -1,60 +0,0 @@ -"Test tree. coverage 56%." - -from idlelib import tree -import unittest -from test.support import requires -requires('gui') -from tkinter import Tk, EventType, SCROLL - - -class TreeTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.root = Tk() - cls.root.withdraw() - - @classmethod - def tearDownClass(cls): - cls.root.destroy() - del cls.root - - def test_init(self): - # Start with code slightly adapted from htest. - sc = tree.ScrolledCanvas( - self.root, bg="white", highlightthickness=0, takefocus=1) - sc.frame.pack(expand=1, fill="both", side='left') - item = tree.FileTreeItem(tree.ICONDIR) - node = tree.TreeNode(sc.canvas, None, item) - node.expand() - - -class TestScrollEvent(unittest.TestCase): - - def test_wheel_event(self): - # Fake widget class containing `yview` only. - class _Widget: - def __init__(widget, *expected): - widget.expected = expected - def yview(widget, *args): - self.assertTupleEqual(widget.expected, args) - # Fake event class - class _Event: - pass - # (type, delta, num, amount) - tests = ((EventType.MouseWheel, 120, -1, -5), - (EventType.MouseWheel, -120, -1, 5), - (EventType.ButtonPress, -1, 4, -5), - (EventType.ButtonPress, -1, 5, 5)) - - event = _Event() - for ty, delta, num, amount in tests: - event.type = ty - event.delta = delta - event.num = num - res = tree.wheel_event(event, _Widget(SCROLL, amount, "units")) - self.assertEqual(res, "break") - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_undo.py b/Lib/idlelib/idle_test/test_undo.py deleted file mode 100644 index beb5b58..0000000 --- a/Lib/idlelib/idle_test/test_undo.py +++ /dev/null @@ -1,135 +0,0 @@ -"Test undo, coverage 77%." -# Only test UndoDelegator so far. - -from idlelib.undo import UndoDelegator -import unittest -from test.support import requires -requires('gui') - -from unittest.mock import Mock -from tkinter import Text, Tk -from idlelib.percolator import Percolator - - -class UndoDelegatorTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.root = Tk() - cls.text = Text(cls.root) - cls.percolator = Percolator(cls.text) - - @classmethod - def tearDownClass(cls): - cls.percolator.redir.close() - del cls.percolator, cls.text - cls.root.destroy() - del cls.root - - def setUp(self): - self.delegator = UndoDelegator() - self.delegator.bell = Mock() - self.percolator.insertfilter(self.delegator) - - def tearDown(self): - self.percolator.removefilter(self.delegator) - self.text.delete('1.0', 'end') - self.delegator.resetcache() - - def test_undo_event(self): - text = self.text - - text.insert('insert', 'foobar') - text.insert('insert', 'h') - text.event_generate('<<undo>>') - self.assertEqual(text.get('1.0', 'end'), '\n') - - text.insert('insert', 'foo') - text.insert('insert', 'bar') - text.delete('1.2', '1.4') - text.insert('insert', 'hello') - text.event_generate('<<undo>>') - self.assertEqual(text.get('1.0', '1.4'), 'foar') - text.event_generate('<<undo>>') - self.assertEqual(text.get('1.0', '1.6'), 'foobar') - text.event_generate('<<undo>>') - self.assertEqual(text.get('1.0', '1.3'), 'foo') - text.event_generate('<<undo>>') - self.delegator.undo_event('event') - self.assertTrue(self.delegator.bell.called) - - def test_redo_event(self): - text = self.text - - text.insert('insert', 'foo') - text.insert('insert', 'bar') - text.delete('1.0', '1.3') - text.event_generate('<<undo>>') - text.event_generate('<<redo>>') - self.assertEqual(text.get('1.0', '1.3'), 'bar') - text.event_generate('<<redo>>') - self.assertTrue(self.delegator.bell.called) - - def test_dump_event(self): - """ - Dump_event cannot be tested directly without changing - environment variables. So, test statements in dump_event - indirectly - """ - text = self.text - d = self.delegator - - text.insert('insert', 'foo') - text.insert('insert', 'bar') - text.delete('1.2', '1.4') - self.assertTupleEqual((d.pointer, d.can_merge), (3, True)) - text.event_generate('<<undo>>') - self.assertTupleEqual((d.pointer, d.can_merge), (2, False)) - - def test_get_set_saved(self): - # test the getter method get_saved - # test the setter method set_saved - # indirectly test check_saved - d = self.delegator - - self.assertTrue(d.get_saved()) - self.text.insert('insert', 'a') - self.assertFalse(d.get_saved()) - d.saved_change_hook = Mock() - - d.set_saved(True) - self.assertEqual(d.pointer, d.saved) - self.assertTrue(d.saved_change_hook.called) - - d.set_saved(False) - self.assertEqual(d.saved, -1) - self.assertTrue(d.saved_change_hook.called) - - def test_undo_start_stop(self): - # test the undo_block_start and undo_block_stop methods - text = self.text - - text.insert('insert', 'foo') - self.delegator.undo_block_start() - text.insert('insert', 'bar') - text.insert('insert', 'bar') - self.delegator.undo_block_stop() - self.assertEqual(text.get('1.0', '1.3'), 'foo') - - # test another code path - self.delegator.undo_block_start() - text.insert('insert', 'bar') - self.delegator.undo_block_stop() - self.assertEqual(text.get('1.0', '1.3'), 'foo') - - def test_addcmd(self): - text = self.text - # when number of undo operations exceeds max_undo - self.delegator.max_undo = max_undo = 10 - for i in range(max_undo + 10): - text.insert('insert', 'foo') - self.assertLessEqual(len(self.delegator.undolist), max_undo) - - -if __name__ == '__main__': - unittest.main(verbosity=2, exit=False) diff --git a/Lib/idlelib/idle_test/test_warning.py b/Lib/idlelib/idle_test/test_warning.py index 221068c..da1d8a1 100644 --- a/Lib/idlelib/idle_test/test_warning.py +++ b/Lib/idlelib/idle_test/test_warning.py @@ -1,23 +1,25 @@ -'''Test warnings replacement in pyshell.py and run.py. +'''Test warnings replacement in PyShell.py and run.py. This file could be expanded to include traceback overrides (in same two modules). If so, change name. Revise if output destination changes (http://bugs.python.org/issue18318). Make sure warnings module is left unaltered (http://bugs.python.org/issue18081). ''' -from idlelib import run -from idlelib import pyshell as shell + import unittest -from test.support import captured_stderr -import warnings +from test.test_support import captured_stderr +import warnings # Try to capture default showwarning before Idle modules are imported. showwarning = warnings.showwarning # But if we run this file within idle, we are in the middle of the run.main loop # and default showwarnings has already been replaced. running_in_idle = 'idle' in showwarning.__name__ -# The following was generated from pyshell.idle_formatwarning +from idlelib import run +from idlelib import PyShell as shell + +# The following was generated from PyShell.idle_formatwarning # and checked as matching expectation. idlemsg = ''' Warning (from warnings module): @@ -27,7 +29,6 @@ UserWarning: Test ''' shellmsg = idlemsg + ">>> " - class RunWarnTest(unittest.TestCase): @unittest.skipIf(running_in_idle, "Does not work when run within Idle.") @@ -45,7 +46,6 @@ class RunWarnTest(unittest.TestCase): # The following uses .splitlines to erase line-ending differences self.assertEqual(idlemsg.splitlines(), f.getvalue().splitlines()) - class ShellWarnTest(unittest.TestCase): @unittest.skipIf(running_in_idle, "Does not work when run within Idle.") @@ -70,4 +70,4 @@ class ShellWarnTest(unittest.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, exit=False) diff --git a/Lib/idlelib/idle_test/test_redirector.py b/Lib/idlelib/idle_test/test_widgetredir.py index a97b300..e35ea41 100644 --- a/Lib/idlelib/idle_test/test_redirector.py +++ b/Lib/idlelib/idle_test/test_widgetredir.py @@ -1,10 +1,12 @@ -"Test redirector, coverage 100%." +"""Unittest for idlelib.WidgetRedirector -from idlelib.redirector import WidgetRedirector +100% coverage +""" +from test.test_support import requires import unittest -from test.support import requires -from tkinter import Tk, Text, TclError from idlelib.idle_test.mock_idle import Func +from Tkinter import Tk, Text, TclError +from idlelib.WidgetRedirector import WidgetRedirector class InitCloseTest(unittest.TestCase): @@ -49,7 +51,6 @@ class WidgetRedirectorTest(unittest.TestCase): @classmethod def tearDownClass(cls): del cls.text - cls.root.update_idletasks() cls.root.destroy() del cls.root @@ -118,5 +119,6 @@ class WidgetRedirectorTest(unittest.TestCase): self.assertEqual(self.root.call(self.text._w, 'insert', 'boo'), '') + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_window.py b/Lib/idlelib/idle_test/test_window.py deleted file mode 100644 index 5a2645b..0000000 --- a/Lib/idlelib/idle_test/test_window.py +++ /dev/null @@ -1,45 +0,0 @@ -"Test window, coverage 47%." - -from idlelib import window -import unittest -from test.support import requires -from tkinter import Tk - - -class WindowListTest(unittest.TestCase): - - def test_init(self): - wl = window.WindowList() - self.assertEqual(wl.dict, {}) - self.assertEqual(wl.callbacks, []) - - # Further tests need mock Window. - - -class ListedToplevelTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - window.registry = set() - requires('gui') - cls.root = Tk() - cls.root.withdraw() - - @classmethod - def tearDownClass(cls): - window.registry = window.WindowList() - cls.root.update_idletasks() -## for id in cls.root.tk.call('after', 'info'): -## cls.root.after_cancel(id) # Need for EditorWindow. - cls.root.destroy() - del cls.root - - def test_init(self): - - win = window.ListedToplevel(self.root) - self.assertIn(win, window.registry) - self.assertEqual(win.focused_widget, win) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_zoomheight.py b/Lib/idlelib/idle_test/test_zoomheight.py deleted file mode 100644 index aa5bdfb..0000000 --- a/Lib/idlelib/idle_test/test_zoomheight.py +++ /dev/null @@ -1,39 +0,0 @@ -"Test zoomheight, coverage 66%." -# Some code is system dependent. - -from idlelib import zoomheight -import unittest -from test.support import requires -from tkinter import Tk -from idlelib.editor import EditorWindow - - -class Test(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - cls.root.withdraw() - cls.editwin = EditorWindow(root=cls.root) - - @classmethod - def tearDownClass(cls): - cls.editwin._close() - cls.root.update_idletasks() - for id in cls.root.tk.call('after', 'info'): - cls.root.after_cancel(id) # Need for EditorWindow. - cls.root.destroy() - del cls.root - - def test_init(self): - zoom = zoomheight.ZoomHeight(self.editwin) - self.assertIs(zoom.editwin, self.editwin) - - def test_zoom_height_event(self): - zoom = zoomheight.ZoomHeight(self.editwin) - zoom.zoom_height_event() - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/idlever.py b/Lib/idlelib/idlever.py new file mode 100644 index 0000000..3e9f69a --- /dev/null +++ b/Lib/idlelib/idlever.py @@ -0,0 +1,12 @@ +""" +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 new file mode 100644 index 0000000..9713c79 --- /dev/null +++ b/Lib/idlelib/keybindingDialog.py @@ -0,0 +1,268 @@ +""" +Dialog for building Tkinter accelerator key bindings +""" +from Tkinter import * +import tkMessageBox +import string +import sys + +class GetKeysDialog(Toplevel): + 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) + self.resizable(height=FALSE,width=FALSE) + self.title(title) + self.transient(parent) + self.grab_set() + self.protocol("WM_DELETE_WINDOW", self.Cancel) + self.parent = parent + self.action=action + self.currentKeySequences=currentKeySequences + self.result='' + self.keyString=StringVar(self) + self.keyString.set('') + self.SetModifiersForPlatform() # set self.modifiers, self.modifier_label + self.modifier_vars = [] + for modifier in self.modifiers: + variable = StringVar(self) + variable.set('') + self.modifier_vars.append(variable) + self.advanced = False + self.CreateWidgets() + 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) + if not _htest else 150) + ) ) #centre dialog over parent (or below htest box) + self.deiconify() #geometry set, unhide + self.wait_window() + + def CreateWidgets(self): + frameMain = Frame(self,borderwidth=2,relief=SUNKEN) + frameMain.pack(side=TOP,expand=TRUE,fill=BOTH) + frameButtons=Frame(self) + frameButtons.pack(side=BOTTOM,fill=X) + self.buttonOK = Button(frameButtons,text='OK', + width=8,command=self.OK) + self.buttonOK.grid(row=0,column=0,padx=5,pady=5) + self.buttonCancel = Button(frameButtons,text='Cancel', + width=8,command=self.Cancel) + self.buttonCancel.grid(row=0,column=1,padx=5,pady=5) + self.frameKeySeqBasic = Frame(frameMain) + self.frameKeySeqAdvanced = Frame(frameMain) + self.frameControlsBasic = Frame(frameMain) + self.frameHelpAdvanced = Frame(frameMain) + self.frameKeySeqAdvanced.grid(row=0,column=0,sticky=NSEW,padx=5,pady=5) + self.frameKeySeqBasic.grid(row=0,column=0,sticky=NSEW,padx=5,pady=5) + self.frameKeySeqBasic.lift() + self.frameHelpAdvanced.grid(row=1,column=0,sticky=NSEW,padx=5) + self.frameControlsBasic.grid(row=1,column=0,sticky=NSEW,padx=5) + self.frameControlsBasic.lift() + self.buttonLevel = Button(frameMain,command=self.ToggleLevel, + text='Advanced Key Binding Entry >>') + self.buttonLevel.grid(row=2,column=0,stick=EW,padx=5,pady=5) + labelTitleBasic = Label(self.frameKeySeqBasic, + text="New keys for '"+self.action+"' :") + labelTitleBasic.pack(anchor=W) + labelKeysBasic = Label(self.frameKeySeqBasic,justify=LEFT, + textvariable=self.keyString,relief=GROOVE,borderwidth=2) + labelKeysBasic.pack(ipadx=5,ipady=5,fill=X) + self.modifier_checkbuttons = {} + column = 0 + for modifier, variable in zip(self.modifiers, self.modifier_vars): + label = self.modifier_label.get(modifier, modifier) + check=Checkbutton(self.frameControlsBasic, + command=self.BuildKeyString, + text=label,variable=variable,onvalue=modifier,offvalue='') + check.grid(row=0,column=column,padx=2,sticky=W) + self.modifier_checkbuttons[modifier] = check + column += 1 + labelFnAdvice=Label(self.frameControlsBasic,justify=LEFT, + text=\ + "Select the desired modifier keys\n"+ + "above, and the final key from the\n"+ + "list on the right.\n\n" + + "Use upper case Symbols when using\n" + + "the Shift modifier. (Letters will be\n" + + "converted automatically.)") + labelFnAdvice.grid(row=1,column=0,columnspan=4,padx=2,sticky=W) + self.listKeysFinal=Listbox(self.frameControlsBasic,width=15,height=10, + selectmode=SINGLE) + self.listKeysFinal.bind('<ButtonRelease-1>',self.FinalKeySelected) + self.listKeysFinal.grid(row=0,column=4,rowspan=4,sticky=NS) + scrollKeysFinal=Scrollbar(self.frameControlsBasic,orient=VERTICAL, + command=self.listKeysFinal.yview) + self.listKeysFinal.config(yscrollcommand=scrollKeysFinal.set) + scrollKeysFinal.grid(row=0,column=5,rowspan=4,sticky=NS) + self.buttonClear=Button(self.frameControlsBasic, + text='Clear Keys',command=self.ClearKeySeq) + self.buttonClear.grid(row=2,column=0,columnspan=4) + labelTitleAdvanced = Label(self.frameKeySeqAdvanced,justify=LEFT, + text="Enter new binding(s) for '"+self.action+"' :\n"+ + "(These bindings will not be checked for validity!)") + labelTitleAdvanced.pack(anchor=W) + self.entryKeysAdvanced=Entry(self.frameKeySeqAdvanced, + textvariable=self.keyString) + self.entryKeysAdvanced.pack(fill=X) + labelHelpAdvanced=Label(self.frameHelpAdvanced,justify=LEFT, + text="Key bindings are specified using Tkinter keysyms as\n"+ + "in these samples: <Control-f>, <Shift-F2>, <F12>,\n" + "<Control-space>, <Meta-less>, <Control-Alt-Shift-X>.\n" + "Upper case is used when the Shift modifier is present!\n\n" + + "'Emacs style' multi-keystroke bindings are specified as\n" + + "follows: <Control-x><Control-y>, where the first key\n" + + "is the 'do-nothing' keybinding.\n\n" + + "Multiple separate bindings for one action should be\n"+ + "separated by a space, eg., <Alt-v> <Meta-v>." ) + labelHelpAdvanced.grid(row=0,column=0,sticky=NSEW) + + def SetModifiersForPlatform(self): + """Determine list of names of key modifiers for this platform. + + The names are used to build Tk bindings -- it doesn't matter if the + keyboard has these keys, it matters if Tk understands them. The + order is also important: key binding equality depends on it, so + config-keys.def must use the same ordering. + """ + if sys.platform == "darwin": + self.modifiers = ['Shift', 'Control', 'Option', 'Command'] + else: + self.modifiers = ['Control', 'Alt', 'Shift'] + self.modifier_label = {'Control': 'Ctrl'} # short name + + def ToggleLevel(self): + if self.buttonLevel.cget('text')[:8]=='Advanced': + self.ClearKeySeq() + self.buttonLevel.config(text='<< Basic Key Binding Entry') + self.frameKeySeqAdvanced.lift() + self.frameHelpAdvanced.lift() + self.entryKeysAdvanced.focus_set() + self.advanced = True + else: + self.ClearKeySeq() + self.buttonLevel.config(text='Advanced Key Binding Entry >>') + self.frameKeySeqBasic.lift() + self.frameControlsBasic.lift() + self.advanced = False + + def FinalKeySelected(self,event): + self.BuildKeyString() + + def BuildKeyString(self): + keyList = modifiers = self.GetModifiers() + finalKey = self.listKeysFinal.get(ANCHOR) + if finalKey: + finalKey = self.TranslateKey(finalKey, modifiers) + keyList.append(finalKey) + self.keyString.set('<' + string.join(keyList,'-') + '>') + + def GetModifiers(self): + modList = [variable.get() for variable in self.modifier_vars] + return [mod for mod in modList if mod] + + def ClearKeySeq(self): + self.listKeysFinal.select_clear(0,END) + self.listKeysFinal.yview(MOVETO, '0.0') + for variable in self.modifier_vars: + variable.set('') + self.keyString.set('') + + def LoadFinalKeyList(self): + #these tuples are also available for use in validity checks + self.functionKeys=('F1','F2','F3','F4','F5','F6','F7','F8','F9', + 'F10','F11','F12') + self.alphanumKeys=tuple(string.ascii_lowercase+string.digits) + self.punctuationKeys=tuple('~!@#%^&*()_-+={}[]|;:,.<>/?') + self.whitespaceKeys=('Tab','Space','Return') + self.editKeys=('BackSpace','Delete','Insert') + self.moveKeys=('Home','End','Page Up','Page Down','Left Arrow', + 'Right Arrow','Up Arrow','Down Arrow') + #make a tuple of most of the useful common 'final' keys + keys=(self.alphanumKeys+self.punctuationKeys+self.functionKeys+ + self.whitespaceKeys+self.editKeys+self.moveKeys) + self.listKeysFinal.insert(END, *keys) + + def TranslateKey(self, key, modifiers): + "Translate from keycap symbol to the Tkinter keysym" + translateDict = {'Space':'space', + '~':'asciitilde','!':'exclam','@':'at','#':'numbersign', + '%':'percent','^':'asciicircum','&':'ampersand','*':'asterisk', + '(':'parenleft',')':'parenright','_':'underscore','-':'minus', + '+':'plus','=':'equal','{':'braceleft','}':'braceright', + '[':'bracketleft',']':'bracketright','|':'bar',';':'semicolon', + ':':'colon',',':'comma','.':'period','<':'less','>':'greater', + '/':'slash','?':'question','Page Up':'Prior','Page Down':'Next', + 'Left Arrow':'Left','Right Arrow':'Right','Up Arrow':'Up', + 'Down Arrow': 'Down', 'Tab':'Tab'} + if key in translateDict.keys(): + key = translateDict[key] + if 'Shift' in modifiers and key in string.ascii_lowercase: + key = key.upper() + key = 'Key-' + key + return key + + def OK(self, event=None): + if self.advanced or self.KeysOK(): # doesn't check advanced string yet + self.result=self.keyString.get() + self.grab_release() + self.destroy() + + def Cancel(self, event=None): + self.result='' + self.grab_release() + self.destroy() + + def KeysOK(self): + '''Validity check on user's 'basic' keybinding selection. + + Doesn't check the string produced by the advanced dialog because + 'modifiers' isn't set. + + ''' + keys = self.keyString.get() + keys.strip() + finalKey = self.listKeysFinal.get(ANCHOR) + modifiers = self.GetModifiers() + # create a key sequence list for overlap check: + keySequence = keys.split() + keysOK = False + title = 'Key Sequence Error' + if not keys: + tkMessageBox.showerror(title=title, parent=self, + message='No keys specified.') + elif not keys.endswith('>'): + tkMessageBox.showerror(title=title, parent=self, + message='Missing the final Key') + elif (not modifiers + and finalKey not in self.functionKeys + self.moveKeys): + tkMessageBox.showerror(title=title, parent=self, + message='No modifier key(s) specified.') + elif (modifiers == ['Shift']) \ + and (finalKey not in + self.functionKeys + self.moveKeys + ('Tab', 'Space')): + msg = 'The shift modifier by itself may not be used with'\ + ' this key symbol.' + tkMessageBox.showerror(title=title, parent=self, message=msg) + elif keySequence in self.currentKeySequences: + msg = 'This key combination is already in use.' + tkMessageBox.showerror(title=title, parent=self, message=msg) + else: + keysOK = True + return keysOK + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(GetKeysDialog) diff --git a/Lib/idlelib/macosx.py b/Lib/idlelib/macosxSupport.py index eeaab59..041d700 100644 --- a/Lib/idlelib/macosx.py +++ b/Lib/idlelib/macosxSupport.py @@ -1,26 +1,32 @@ """ -A number of functions that enhance IDLE on macOS. +A number of functions that enhance IDLE on Mac OSX. """ -from os.path import expanduser -import plistlib -from sys import platform # Used in _init_tk_type, changed by test. +import sys +import Tkinter +from os import path -import tkinter +import warnings -## Define functions that query the Mac graphics type. -## _tk_type and its initializer are private to this section. +def runningAsOSXApp(): + warnings.warn("runningAsOSXApp() is deprecated, use isAquaTk()", + DeprecationWarning, stacklevel=2) + return isAquaTk() + +def isCarbonAquaTk(root): + warnings.warn("isCarbonAquaTk(root) is deprecated, use isCarbonTk()", + DeprecationWarning, stacklevel=2) + return isCarbonTk() _tk_type = None -def _init_tk_type(): +def _initializeTkVariantTests(root): """ Initializes OS X Tk variant values for isAquaTk(), isCarbonTk(), isCocoaTk(), and isXQuartz(). """ global _tk_type - if platform == 'darwin': - root = tkinter.Tk() + if sys.platform == 'darwin': ws = root.tk.call('tk', 'windowingsystem') if 'x11' in ws: _tk_type = "xquartz" @@ -30,7 +36,6 @@ def _init_tk_type(): _tk_type = "cocoa" else: _tk_type = "carbon" - root.destroy() else: _tk_type = "other" @@ -38,8 +43,7 @@ def isAquaTk(): """ Returns True if IDLE is using a native OS X Tk (Cocoa or Carbon). """ - if not _tk_type: - _init_tk_type() + assert _tk_type is not None return _tk_type == "cocoa" or _tk_type == "carbon" def isCarbonTk(): @@ -47,27 +51,23 @@ def isCarbonTk(): Returns True if IDLE is using a Carbon Aqua Tk (instead of the newer Cocoa Aqua Tk). """ - if not _tk_type: - _init_tk_type() + assert _tk_type is not None return _tk_type == "carbon" def isCocoaTk(): """ Returns True if IDLE is using a Cocoa Aqua Tk. """ - if not _tk_type: - _init_tk_type() + assert _tk_type is not None return _tk_type == "cocoa" def isXQuartz(): """ Returns True if IDLE is using an OS X X11 Tk. """ - if not _tk_type: - _init_tk_type() + assert _tk_type is not None return _tk_type == "xquartz" - def tkVersionWarning(root): """ Returns a string warning message if the Tk version in use appears to @@ -81,49 +81,13 @@ def tkVersionWarning(root): patchlevel = root.tk.call('info', 'patchlevel') if patchlevel not in ('8.5.7', '8.5.9'): return False - return ("WARNING: The version of Tcl/Tk ({0}) in use may" - " be unstable.\n" - "Visit http://www.python.org/download/mac/tcltk/" - " for current information.".format(patchlevel)) + return (r"WARNING: The version of Tcl/Tk ({0}) in use may" + r" be unstable.\n" + r"Visit http://www.python.org/download/mac/tcltk/" + r" for current information.".format(patchlevel)) else: return False - -def readSystemPreferences(): - """ - Fetch the macOS system preferences. - """ - if platform != 'darwin': - return None - - plist_path = expanduser('~/Library/Preferences/.GlobalPreferences.plist') - try: - with open(plist_path, 'rb') as plist_file: - return plistlib.load(plist_file) - except OSError: - return None - - -def preferTabsPreferenceWarning(): - """ - Warn if "Prefer tabs when opening documents" is set to "Always". - """ - if platform != 'darwin': - return None - - prefs = readSystemPreferences() - if prefs and prefs.get('AppleWindowTabbingMode') == 'always': - return ( - 'WARNING: The system preference "Prefer tabs when opening' - ' documents" is set to "Always". This will cause various problems' - ' with IDLE. For the best experience, change this setting when' - ' running IDLE (via System Preferences -> Dock).' - ) - return None - - -## Fix the menu and related functions. - def addOpenEventSupport(root, flist): """ This ensures that the application will respond to open AppleEvents, which @@ -141,7 +105,7 @@ def addOpenEventSupport(root, flist): def hideTkConsole(root): try: root.tk.call('console', 'hide') - except tkinter.TclError: + except Tkinter.TclError: # Some versions of the Tk framework don't have a console object pass @@ -161,29 +125,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 - from idlelib import mainmenu - from idlelib import window + from Tkinter import Menu + from idlelib import Bindings + from idlelib import WindowList - closeItem = mainmenu.menudefs[0][1][-2] + 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 mainmenu.menudefs[0][1][-3:] - mainmenu.menudefs[0][1].insert(6, closeItem) + 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 mainmenu.menudefs[-1][1][0:2] + 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 mainmenu.menudefs[-3][1][0:2] + del Bindings.menudefs[-2][1][0] menubar = Menu(root) root.configure(menu=menubar) menudict = {} - menudict['window'] = menu = Menu(menubar, name='window', tearoff=0) + menudict['windows'] = menu = Menu(menubar, name='windows', tearoff=0) menubar.add_cascade(label='Window', menu=menu, underline=0) def postwindowsmenu(menu=menu): @@ -193,30 +157,25 @@ def overrideRootMenu(root, flist): if end > 0: menu.delete(0, end) - window.add_windows_to_menu(menu) - window.register_callback(postwindowsmenu) + WindowList.add_windows_to_menu(menu) + WindowList.register_callback(postwindowsmenu) def about_dialog(event=None): "Handle Help 'About IDLE' event." - # Synchronize with editor.EditorWindow.about_dialog. - from idlelib import help_about - help_about.AboutDialog(root) + # 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 editor.EditorWindow.config_dialog. - from idlelib import configdialog - - # Ensure that the root object has an instance_dict attribute, - # mirrors code in EditorWindow (although that sets the attribute - # on an EditorWindow instance that is then passed as the first - # argument to ConfigDialog) + # Synchronize with EditorWindow.EditorWindow.config_dialog. + from idlelib import configDialog root.instance_dict = flist.inversedict - configdialog.ConfigDialog(root, 'Settings') + configDialog.ConfigDialog(root, 'Settings') def help_dialog(event=None): "Handle Help 'IDLE Help' event." - # Synchronize with editor.EditorWindow.help_dialog. + # Synchronize with EditorWindow.EditorWindow.help_dialog. from idlelib import help help.show_idlehelp(root) @@ -227,7 +186,7 @@ def overrideRootMenu(root, flist): root.bind('<<close-all-windows>>', flist.close_all_callback) # The binding above doesn't reliably work on all versions of Tk - # on macOS. Adding command definition below does seem to do the + # on MacOSX. Adding command definition below does seem to do the # right thing for now. root.createcommand('exit', flist.close_all_callback) @@ -236,33 +195,29 @@ def overrideRootMenu(root, flist): menudict['application'] = menu = Menu(menubar, name='apple', tearoff=0) menubar.add_cascade(label='IDLE', menu=menu) - mainmenu.menudefs.insert(0, + Bindings.menudefs.insert(0, ('application', [ ('About IDLE', '<<about-idle>>'), None, ])) + tkversion = root.tk.eval('info patchlevel') + if tuple(map(int, tkversion.split('.'))) < (8, 4, 14): + # for earlier AquaTk versions, supply a Preferences menu item + Bindings.menudefs[0][1].append( + ('_Preferences....', '<<open-config-dialog>>'), + ) if isCocoaTk(): # replace default About dialog with About IDLE one root.createcommand('tkAboutDialog', about_dialog) # replace default "Help" item in Help menu root.createcommand('::tk::mac::ShowHelp', help_dialog) # remove redundant "IDLE Help" from menu - del mainmenu.menudefs[-1][1][0] - -def fixb2context(root): - '''Removed bad AquaTk Button-2 (right) and Paste bindings. - - They prevent context menu access and seem to be gone in AquaTk8.6. - See issue #24801. - ''' - root.unbind_class('Text', '<B2>') - root.unbind_class('Text', '<B2-Motion>') - root.unbind_class('Text', '<<PasteSelection>>') + del Bindings.menudefs[-1][1][0] def setupApp(root, flist): """ Perform initial OS X customizations if needed. - Called from pyshell.main() after initial calls to Tk() + 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) @@ -275,13 +230,8 @@ def setupApp(root, flist): isAquaTk(), isCarbonTk(), isCocoaTk(), isXQuartz() functions which are initialized here as well. """ + _initializeTkVariantTests(root) if isAquaTk(): hideTkConsole(root) overrideRootMenu(root, flist) addOpenEventSupport(root, flist) - fixb2context(root) - - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_macosx', verbosity=2) diff --git a/Lib/idlelib/outwin.py b/Lib/idlelib/outwin.py deleted file mode 100644 index 90272b6..0000000 --- a/Lib/idlelib/outwin.py +++ /dev/null @@ -1,189 +0,0 @@ -"""Editor window that can serve as an output file. -""" - -import re - -from tkinter import messagebox - -from idlelib.editor import EditorWindow -from idlelib import iomenu - - -file_line_pats = [ - # order of patterns matters - r'file "([^"]*)", line (\d+)', - r'([^\s]+)\((\d+)\)', - r'^(\s*\S.*?):\s*(\d+):', # Win filename, maybe starting with spaces - r'([^\s]+):\s*(\d+):', # filename or path, ltrim - r'^\s*(\S.*?):\s*(\d+):', # Win abs path with embedded spaces, ltrim -] - -file_line_progs = None - - -def compile_progs(): - "Compile the patterns for matching to file name and line number." - global file_line_progs - file_line_progs = [re.compile(pat, re.IGNORECASE) - for pat in file_line_pats] - - -def file_line_helper(line): - """Extract file name and line number from line of text. - - Check if line of text contains one of the file/line patterns. - If it does and if the file and line are valid, return - a tuple of the file name and line number. If it doesn't match - or if the file or line is invalid, return None. - """ - if not file_line_progs: - compile_progs() - for prog in file_line_progs: - match = prog.search(line) - if match: - filename, lineno = match.group(1, 2) - try: - f = open(filename, "r") - f.close() - break - except OSError: - continue - else: - return None - try: - return filename, int(lineno) - except TypeError: - return None - - -class OutputWindow(EditorWindow): - """An editor window that can serve as an output file. - - Also the future base class for the Python shell window. - This class has no input facilities. - - Adds binding to open a file at a line to the text widget. - """ - - # Our own right-button menu - rmenu_specs = [ - ("Cut", "<<cut>>", "rmenu_check_cut"), - ("Copy", "<<copy>>", "rmenu_check_copy"), - ("Paste", "<<paste>>", "rmenu_check_paste"), - (None, None, None), - ("Go to file/line", "<<goto-file-line>>", None), - ] - - allow_code_context = False - - def __init__(self, *args): - EditorWindow.__init__(self, *args) - self.text.bind("<<goto-file-line>>", self.goto_file_line) - - # Customize EditorWindow - def ispythonsource(self, filename): - "Python source is only part of output: do not colorize." - return False - - def short_title(self): - "Customize EditorWindow title." - return "Output" - - def maybesave(self): - "Customize EditorWindow to not display save file messagebox." - return 'yes' if self.get_saved() else 'no' - - # Act as output file - def write(self, s, tags=(), mark="insert"): - """Write text to text widget. - - The text is inserted at the given index with the provided - tags. The text widget is then scrolled to make it visible - and updated to display it, giving the effect of seeing each - line as it is added. - - Args: - s: Text to insert into text widget. - tags: Tuple of tag strings to apply on the insert. - mark: Index for the insert. - - Return: - Length of text inserted. - """ - if isinstance(s, bytes): - s = s.decode(iomenu.encoding, "replace") - self.text.insert(mark, s, tags) - self.text.see(mark) - self.text.update() - return len(s) - - def writelines(self, lines): - "Write each item in lines iterable." - for line in lines: - self.write(line) - - def flush(self): - "No flushing needed as write() directly writes to widget." - pass - - def showerror(self, *args, **kwargs): - messagebox.showerror(*args, **kwargs) - - def goto_file_line(self, event=None): - """Handle request to open file/line. - - If the selected or previous line in the output window - contains a file name and line number, then open that file - name in a new window and position on the line number. - - Otherwise, display an error messagebox. - """ - line = self.text.get("insert linestart", "insert lineend") - result = file_line_helper(line) - if not result: - # Try the previous line. This is handy e.g. in tracebacks, - # where you tend to right-click on the displayed source line - line = self.text.get("insert -1line linestart", - "insert -1line lineend") - result = file_line_helper(line) - if not result: - self.showerror( - "No special line", - "The line you point at doesn't look like " - "a valid file name followed by a line number.", - parent=self.text) - return - filename, lineno = result - self.flist.gotofileline(filename, lineno) - - -# These classes are currently not used but might come in handy -class OnDemandOutputWindow: - - tagdefs = { - # XXX Should use IdlePrefs.ColorPrefs - "stdout": {"foreground": "blue"}, - "stderr": {"foreground": "#007700"}, - } - - def __init__(self, flist): - self.flist = flist - self.owin = None - - def write(self, s, tags, mark): - if not self.owin: - self.setup() - self.owin.write(s, tags, mark) - - def setup(self): - self.owin = owin = OutputWindow(self.flist) - text = owin.text - for tag, cnf in self.tagdefs.items(): - if cnf: - text.tag_configure(tag, **cnf) - text.tag_raise('sel') - self.write = self.owin.write - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_outwin', verbosity=2, exit=False) diff --git a/Lib/idlelib/query.py b/Lib/idlelib/query.py deleted file mode 100644 index 097e6e6..0000000 --- a/Lib/idlelib/query.py +++ /dev/null @@ -1,376 +0,0 @@ -""" -Dialogs that query users and verify the answer before accepting. - -Query is the generic base class for a popup dialog. -The user must either enter a valid answer or close the dialog. -Entries are validated when <Return> is entered or [Ok] is clicked. -Entries are ignored when [Cancel] or [X] are clicked. -The 'return value' is .result set to either a valid answer or None. - -Subclass SectionName gets a name for a new config file section. -Configdialog uses it for new highlight theme and keybinding set names. -Subclass ModuleName gets a name for File => Open Module. -Subclass HelpSource gets menu item and path for additions to Help menu. -""" -# Query and Section name result from splitting GetCfgSectionNameDialog -# of configSectionNameDialog.py (temporarily config_sec.py) into -# generic and specific parts. 3.6 only, July 2016. -# ModuleName.entry_ok came from editor.EditorWindow.load_module. -# HelpSource was extracted from configHelpSourceEdit.py (temporarily -# config_help.py), with darwin code moved from ok to path_ok. - -import importlib -import os -import shlex -from sys import executable, platform # Platform is set for one test. - -from tkinter import Toplevel, StringVar, BooleanVar, W, E, S -from tkinter.ttk import Frame, Button, Entry, Label, Checkbutton -from tkinter import filedialog -from tkinter.font import Font - -class Query(Toplevel): - """Base class for getting verified answer from a user. - - For this base class, accept any non-blank string. - """ - def __init__(self, parent, title, message, *, text0='', used_names={}, - _htest=False, _utest=False): - """Create modal popup, return when destroyed. - - Additional subclass init must be done before this unless - _utest=True is passed to suppress wait_window(). - - title - string, title of popup dialog - message - string, informational message to display - text0 - initial value for entry - used_names - names already in use - _htest - bool, change box location when running htest - _utest - bool, leave window hidden and not modal - """ - self.parent = parent # Needed for Font call. - self.message = message - self.text0 = text0 - self.used_names = used_names - - Toplevel.__init__(self, parent) - self.withdraw() # Hide while configuring, especially geometry. - self.title(title) - self.transient(parent) - self.grab_set() - - windowingsystem = self.tk.call('tk', 'windowingsystem') - if windowingsystem == 'aqua': - try: - self.tk.call('::tk::unsupported::MacWindowStyle', 'style', - self._w, 'moveableModal', '') - except: - pass - self.bind("<Command-.>", self.cancel) - self.bind('<Key-Escape>', self.cancel) - self.protocol("WM_DELETE_WINDOW", self.cancel) - self.bind('<Key-Return>', self.ok) - self.bind("<KP_Enter>", self.ok) - - self.create_widgets() - self.update_idletasks() # Need here for winfo_reqwidth below. - self.geometry( # Center dialog over parent (or below htest box). - "+%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.resizable(height=False, width=False) - - if not _utest: - self.deiconify() # Unhide now that geometry set. - self.wait_window() - - def create_widgets(self, ok_text='OK'): # Do not replace. - """Create entry (rows, extras, buttons. - - Entry stuff on rows 0-2, spanning cols 0-2. - Buttons on row 99, cols 1, 2. - """ - # Bind to self the widgets needed for entry_ok or unittest. - self.frame = frame = Frame(self, padding=10) - frame.grid(column=0, row=0, sticky='news') - frame.grid_columnconfigure(0, weight=1) - - entrylabel = Label(frame, anchor='w', justify='left', - text=self.message) - self.entryvar = StringVar(self, self.text0) - self.entry = Entry(frame, width=30, textvariable=self.entryvar) - self.entry.focus_set() - self.error_font = Font(name='TkCaptionFont', - exists=True, root=self.parent) - self.entry_error = Label(frame, text=' ', foreground='red', - font=self.error_font) - entrylabel.grid(column=0, row=0, columnspan=3, padx=5, sticky=W) - self.entry.grid(column=0, row=1, columnspan=3, padx=5, sticky=W+E, - pady=[10,0]) - self.entry_error.grid(column=0, row=2, columnspan=3, padx=5, - sticky=W+E) - - self.create_extra() - - self.button_ok = Button( - frame, text=ok_text, default='active', command=self.ok) - self.button_cancel = Button( - frame, text='Cancel', command=self.cancel) - - self.button_ok.grid(column=1, row=99, padx=5) - self.button_cancel.grid(column=2, row=99, padx=5) - - def create_extra(self): pass # Override to add widgets. - - def showerror(self, message, widget=None): - #self.bell(displayof=self) - (widget or self.entry_error)['text'] = 'ERROR: ' + message - - def entry_ok(self): # Example: usually replace. - "Return non-blank entry or None." - self.entry_error['text'] = '' - entry = self.entry.get().strip() - if not entry: - self.showerror('blank line.') - return None - return entry - - def ok(self, event=None): # Do not replace. - '''If entry is valid, bind it to 'result' and destroy tk widget. - - Otherwise leave dialog open for user to correct entry or cancel. - ''' - entry = self.entry_ok() - if entry is not None: - self.result = entry - self.destroy() - else: - # [Ok] moves focus. (<Return> does not.) Move it back. - self.entry.focus_set() - - def cancel(self, event=None): # Do not replace. - "Set dialog result to None and destroy tk widget." - self.result = None - self.destroy() - - def destroy(self): - self.grab_release() - super().destroy() - - -class SectionName(Query): - "Get a name for a config file section name." - # Used in ConfigDialog.GetNewKeysName, .GetNewThemeName (837) - - def __init__(self, parent, title, message, used_names, - *, _htest=False, _utest=False): - super().__init__(parent, title, message, used_names=used_names, - _htest=_htest, _utest=_utest) - - def entry_ok(self): - "Return sensible ConfigParser section name or None." - self.entry_error['text'] = '' - name = self.entry.get().strip() - if not name: - self.showerror('no name specified.') - return None - elif len(name)>30: - self.showerror('name is longer than 30 characters.') - return None - elif name in self.used_names: - self.showerror('name is already in use.') - return None - return name - - -class ModuleName(Query): - "Get a module name for Open Module menu entry." - # Used in open_module (editor.EditorWindow until move to iobinding). - - def __init__(self, parent, title, message, text0, - *, _htest=False, _utest=False): - super().__init__(parent, title, message, text0=text0, - _htest=_htest, _utest=_utest) - - def entry_ok(self): - "Return entered module name as file path or None." - self.entry_error['text'] = '' - name = self.entry.get().strip() - if not name: - self.showerror('no name specified.') - return None - # XXX Ought to insert current file's directory in front of path. - try: - spec = importlib.util.find_spec(name) - except (ValueError, ImportError) as msg: - self.showerror(str(msg)) - return None - if spec is None: - self.showerror("module not found") - return None - if not isinstance(spec.loader, importlib.abc.SourceLoader): - self.showerror("not a source-based module") - return None - try: - file_path = spec.loader.get_filename(name) - except AttributeError: - self.showerror("loader does not support get_filename", - parent=self) - return None - return file_path - - -class HelpSource(Query): - "Get menu name and help source for Help menu." - # Used in ConfigDialog.HelpListItemAdd/Edit, (941/9) - - def __init__(self, parent, title, *, menuitem='', filepath='', - used_names={}, _htest=False, _utest=False): - """Get menu entry and url/local file for Additional Help. - - User enters a name for the Help resource and a web url or file - name. The user can browse for the file. - """ - self.filepath = filepath - message = 'Name for item on Help menu:' - super().__init__( - parent, title, message, text0=menuitem, - used_names=used_names, _htest=_htest, _utest=_utest) - - def create_extra(self): - "Add path widjets to rows 10-12." - frame = self.frame - pathlabel = Label(frame, anchor='w', justify='left', - text='Help File Path: Enter URL or browse for file') - self.pathvar = StringVar(self, self.filepath) - self.path = Entry(frame, textvariable=self.pathvar, width=40) - browse = Button(frame, text='Browse', width=8, - command=self.browse_file) - self.path_error = Label(frame, text=' ', foreground='red', - font=self.error_font) - - pathlabel.grid(column=0, row=10, columnspan=3, padx=5, pady=[10,0], - sticky=W) - self.path.grid(column=0, row=11, columnspan=2, padx=5, sticky=W+E, - pady=[10,0]) - browse.grid(column=2, row=11, padx=5, sticky=W+S) - self.path_error.grid(column=0, row=12, columnspan=3, padx=5, - sticky=W+E) - - def askfilename(self, filetypes, initdir, initfile): # htest # - # Extracted from browse_file so can mock for unittests. - # Cannot unittest as cannot simulate button clicks. - # Test by running htest, such as by running this file. - return filedialog.Open(parent=self, filetypes=filetypes)\ - .show(initialdir=initdir, initialfile=initfile) - - def browse_file(self): - filetypes = [ - ("HTML Files", "*.htm *.html", "TEXT"), - ("PDF Files", "*.pdf", "TEXT"), - ("Windows Help Files", "*.chm"), - ("Text Files", "*.txt", "TEXT"), - ("All Files", "*")] - path = self.pathvar.get() - if path: - dir, base = os.path.split(path) - else: - base = None - if platform[:3] == 'win': - dir = os.path.join(os.path.dirname(executable), 'Doc') - if not os.path.isdir(dir): - dir = os.getcwd() - else: - dir = os.getcwd() - file = self.askfilename(filetypes, dir, base) - if file: - self.pathvar.set(file) - - item_ok = SectionName.entry_ok # localize for test override - - def path_ok(self): - "Simple validity check for menu file path" - path = self.path.get().strip() - if not path: #no path specified - self.showerror('no help file path specified.', self.path_error) - return None - elif not path.startswith(('www.', 'http')): - if path[:5] == 'file:': - path = path[5:] - if not os.path.exists(path): - self.showerror('help file path does not exist.', - self.path_error) - return None - if platform == 'darwin': # for Mac Safari - path = "file://" + path - return path - - def entry_ok(self): - "Return apparently valid (name, path) or None" - self.entry_error['text'] = '' - self.path_error['text'] = '' - name = self.item_ok() - path = self.path_ok() - return None if name is None or path is None else (name, path) - -class CustomRun(Query): - """Get settings for custom run of module. - - 1. Command line arguments to extend sys.argv. - 2. Whether to restart Shell or not. - """ - # Used in runscript.run_custom_event - - def __init__(self, parent, title, *, cli_args=[], - _htest=False, _utest=False): - """cli_args is a list of strings. - - The list is assigned to the default Entry StringVar. - The strings are displayed joined by ' ' for display. - """ - message = 'Command Line Arguments for sys.argv:' - super().__init__( - parent, title, message, text0=cli_args, - _htest=_htest, _utest=_utest) - - def create_extra(self): - "Add run mode on rows 10-12." - frame = self.frame - self.restartvar = BooleanVar(self, value=True) - restart = Checkbutton(frame, variable=self.restartvar, onvalue=True, - offvalue=False, text='Restart shell') - self.args_error = Label(frame, text=' ', foreground='red', - font=self.error_font) - - restart.grid(column=0, row=10, columnspan=3, padx=5, sticky='w') - self.args_error.grid(column=0, row=12, columnspan=3, padx=5, - sticky='we') - - def cli_args_ok(self): - "Validity check and parsing for command line arguments." - cli_string = self.entry.get().strip() - try: - cli_args = shlex.split(cli_string, posix=True) - except ValueError as err: - self.showerror(str(err)) - return None - return cli_args - - def entry_ok(self): - "Return apparently valid (cli_args, restart) or None" - self.entry_error['text'] = '' - cli_args = self.cli_args_ok() - restart = self.restartvar.get() - return None if cli_args is None else (cli_args, restart) - - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_query', verbosity=2, exit=False) - - from idlelib.idle_test.htest import run - run(Query, HelpSource, CustomRun) diff --git a/Lib/idlelib/rpc.py b/Lib/idlelib/rpc.py index aa8cbd3..43328e7 100644 --- a/Lib/idlelib/rpc.py +++ b/Lib/idlelib/rpc.py @@ -5,7 +5,7 @@ connect to the Idle process, which listens for the connection. Since Idle has only one client per server, this was not a limitation. +---------------------------------+ +-------------+ - | socketserver.BaseRequestHandler | | SocketIO | + | SocketServer.BaseRequestHandler | | SocketIO | +---------------------------------+ +-------------+ ^ | register() | | | unregister()| @@ -26,56 +26,52 @@ See the Idle run.main() docstring for further information on how this was accomplished in Idle. """ -import builtins -import copyreg -import io -import marshal + +import sys import os -import pickle -import queue -import select import socket -import socketserver +import select +import SocketServer import struct -import sys +import cPickle as pickle import threading +import Queue import traceback +import copy_reg import types +import marshal + def unpickle_code(ms): - "Return code object from marshal string ms." co = marshal.loads(ms) assert isinstance(co, types.CodeType) return co def pickle_code(co): - "Return unpickle function and tuple with marshalled co code object." assert isinstance(co, types.CodeType) ms = marshal.dumps(co) return unpickle_code, (ms,) -def dumps(obj, protocol=None): - "Return pickled (or marshalled) string for obj." - # IDLE passes 'None' to select pickle.DEFAULT_PROTOCOL. - f = io.BytesIO() - p = CodePickler(f, protocol) - p.dump(obj) - return f.getvalue() - +# XXX KBK 24Aug02 function pickling capability not used in Idle +# def unpickle_function(ms): +# return ms -class CodePickler(pickle.Pickler): - dispatch_table = {types.CodeType: pickle_code, **copyreg.dispatch_table} +# def pickle_function(fn): +# assert isinstance(fn, type.FunctionType) +# return repr(fn) +copy_reg.pickle(types.CodeType, pickle_code, unpickle_code) +# copy_reg.pickle(types.FunctionType, pickle_function, unpickle_function) BUFSIZE = 8*1024 LOCALHOST = '127.0.0.1' -class RPCServer(socketserver.TCPServer): +class RPCServer(SocketServer.TCPServer): def __init__(self, addr, handlerclass=None): if handlerclass is None: handlerclass = RPCHandler - socketserver.TCPServer.__init__(self, addr, handlerclass) + SocketServer.TCPServer.__init__(self, addr, handlerclass) def server_bind(self): "Override TCPServer method, no bind() phase for connecting entity" @@ -108,21 +104,21 @@ class RPCServer(socketserver.TCPServer): raise except: erf = sys.__stderr__ - print('\n' + '-'*40, file=erf) - print('Unhandled server exception!', file=erf) - print('Thread: %s' % threading.current_thread().name, file=erf) - print('Client Address: ', client_address, file=erf) - print('Request: ', repr(request), file=erf) + print>>erf, '\n' + '-'*40 + print>>erf, 'Unhandled server exception!' + print>>erf, 'Thread: %s' % threading.currentThread().getName() + print>>erf, 'Client Address: ', client_address + print>>erf, 'Request: ', repr(request) traceback.print_exc(file=erf) - print('\n*** Unrecoverable, server exiting!', file=erf) - print('-'*40, file=erf) + print>>erf, '\n*** Unrecoverable, server exiting!' + print>>erf, '-'*40 os._exit(0) #----------------- end class RPCServer -------------------- objecttable = {} -request_queue = queue.Queue(0) -response_queue = queue.Queue(0) +request_queue = Queue.Queue(0) +response_queue = Queue.Queue(0) class SocketIO(object): @@ -130,7 +126,7 @@ class SocketIO(object): nextseq = 0 def __init__(self, sock, objtable=None, debugging=None): - self.sockthread = threading.current_thread() + self.sockthread = threading.currentThread() if debugging is not None: self.debugging = debugging self.sock = sock @@ -153,10 +149,10 @@ class SocketIO(object): def debug(self, *args): if not self.debugging: return - s = self.location + " " + str(threading.current_thread().name) + s = self.location + " " + str(threading.currentThread().getName()) for a in args: s = s + " " + str(a) - print(s, file=sys.__stderr__) + print>>sys.__stderr__, s def register(self, oid, object): self.objtable[oid] = object @@ -200,16 +196,12 @@ class SocketIO(object): return ("ERROR", "Unsupported message type: %s" % how) except SystemExit: raise - except KeyboardInterrupt: + except socket.error: raise - except OSError: - raise - except Exception as ex: - return ("CALLEXC", ex) except: msg = "*** Internal Error: rpc.py:SocketIO.localcall()\n\n"\ " Object: %s \n Method: %s \n Args: %s\n" - print(msg % (oid, method, args), file=sys.__stderr__) + print>>sys.__stderr__, msg % (oid, method, args) traceback.print_exc(file=sys.__stderr__) return ("EXCEPTION", None) @@ -226,7 +218,7 @@ class SocketIO(object): def asynccall(self, oid, methodname, args, kwargs): request = ("CALL", (oid, methodname, args, kwargs)) seq = self.newseq() - if threading.current_thread() != self.sockthread: + if threading.currentThread() != self.sockthread: cvar = threading.Condition() self.cvars[seq] = cvar self.debug(("asynccall:%d:" % seq), oid, methodname, args, kwargs) @@ -236,7 +228,7 @@ class SocketIO(object): def asyncqueue(self, oid, methodname, args, kwargs): request = ("QUEUE", (oid, methodname, args, kwargs)) seq = self.newseq() - if threading.current_thread() != self.sockthread: + if threading.currentThread() != self.sockthread: cvar = threading.Condition() self.cvars[seq] = cvar self.debug(("asyncqueue:%d:" % seq), oid, methodname, args, kwargs) @@ -264,11 +256,8 @@ class SocketIO(object): return None if how == "ERROR": self.debug("decoderesponse: Internal ERROR:", what) - raise RuntimeError(what) - if how == "CALLEXC": - self.debug("decoderesponse: Call Exception:", what) - raise what - raise SystemError(how, what) + raise RuntimeError, what + raise SystemError, (how, what) def decode_interrupthook(self): "" @@ -298,14 +287,14 @@ class SocketIO(object): def _proxify(self, obj): if isinstance(obj, RemoteProxy): return RPCProxy(self, obj.oid) - if isinstance(obj, list): - return list(map(self._proxify, obj)) + if isinstance(obj, types.ListType): + return map(self._proxify, obj) # XXX Check for other types -- not currently needed return obj def _getresponse(self, myseq, wait): self.debug("_getresponse:myseq:", myseq) - if threading.current_thread() is self.sockthread: + if threading.currentThread() is self.sockthread: # this thread does all reading of requests or responses while 1: response = self.pollresponse(myseq, wait) @@ -332,9 +321,9 @@ class SocketIO(object): def putmessage(self, message): self.debug("putmessage:%d:" % message[0]) try: - s = dumps(message) + s = pickle.dumps(message) except pickle.PicklingError: - print("Cannot pickle:", repr(message), file=sys.__stderr__) + print >>sys.__stderr__, "Cannot pickle:", repr(message) raise s = struct.pack("<i", len(s)) + s while len(s) > 0: @@ -342,40 +331,40 @@ class SocketIO(object): r, w, x = select.select([], [self.sock], []) n = self.sock.send(s[:BUFSIZE]) except (AttributeError, TypeError): - raise OSError("socket no longer exists") + raise IOError, "socket no longer exists" s = s[n:] - buff = b'' + buffer = "" bufneed = 4 bufstate = 0 # meaning: 0 => reading count; 1 => reading data def pollpacket(self, wait): self._stage0() - if len(self.buff) < self.bufneed: + if len(self.buffer) < self.bufneed: r, w, x = select.select([self.sock.fileno()], [], [], wait) if len(r) == 0: return None try: s = self.sock.recv(BUFSIZE) - except OSError: + except socket.error: raise EOFError if len(s) == 0: raise EOFError - self.buff += s + self.buffer += s self._stage0() return self._stage1() def _stage0(self): - if self.bufstate == 0 and len(self.buff) >= 4: - s = self.buff[:4] - self.buff = self.buff[4:] + if self.bufstate == 0 and len(self.buffer) >= 4: + s = self.buffer[:4] + self.buffer = self.buffer[4:] self.bufneed = struct.unpack("<i", s)[0] self.bufstate = 1 def _stage1(self): - if self.bufstate == 1 and len(self.buff) >= self.bufneed: - packet = self.buff[:self.bufneed] - self.buff = self.buff[self.bufneed:] + if self.bufstate == 1 and len(self.buffer) >= self.bufneed: + packet = self.buffer[:self.bufneed] + self.buffer = self.buffer[self.bufneed:] self.bufneed = 4 self.bufstate = 0 return packet @@ -387,10 +376,10 @@ class SocketIO(object): try: message = pickle.loads(packet) except pickle.UnpicklingError: - print("-----------------------", file=sys.__stderr__) - print("cannot unpickle packet:", repr(packet), file=sys.__stderr__) + print >>sys.__stderr__, "-----------------------" + print >>sys.__stderr__, "cannot unpickle packet:", repr(packet) traceback.print_stack(file=sys.__stderr__) - print("-----------------------", file=sys.__stderr__) + print >>sys.__stderr__, "-----------------------" raise return message @@ -421,7 +410,7 @@ class SocketIO(object): # send queued response if there is one available try: qmsg = response_queue.get(0) - except queue.Empty: + except Queue.Empty: pass else: seq, response = qmsg @@ -490,20 +479,17 @@ class RemoteObject(object): # Token mix-in class pass - def remoteref(obj): oid = id(obj) objecttable[oid] = obj return RemoteProxy(oid) - class RemoteProxy(object): def __init__(self, oid): self.oid = oid - -class RPCHandler(socketserver.BaseRequestHandler, SocketIO): +class RPCHandler(SocketServer.BaseRequestHandler, SocketIO): debugging = False location = "#S" # Server @@ -511,16 +497,15 @@ class RPCHandler(socketserver.BaseRequestHandler, SocketIO): def __init__(self, sock, addr, svr): svr.current_handler = self ## cgt xxx SocketIO.__init__(self, sock) - socketserver.BaseRequestHandler.__init__(self, sock, addr, svr) + SocketServer.BaseRequestHandler.__init__(self, sock, addr, svr) def handle(self): - "handle() method required by socketserver" + "handle() method required by SocketServer" self.mainloop() def get_remote_proxy(self, oid): return RPCProxy(self, oid) - class RPCClient(SocketIO): debugging = False @@ -536,17 +521,16 @@ class RPCClient(SocketIO): def accept(self): working_sock, address = self.listening_sock.accept() if self.debugging: - print("****** Connection request from ", address, file=sys.__stderr__) + print>>sys.__stderr__, "****** Connection request from ", address if address[0] == LOCALHOST: SocketIO.__init__(self, working_sock) else: - print("** Invalid host: ", address, file=sys.__stderr__) - raise OSError + print>>sys.__stderr__, "** Invalid host: ", address + raise socket.error def get_remote_proxy(self, oid): return RPCProxy(self, oid) - class RPCProxy(object): __methods = None @@ -568,7 +552,7 @@ class RPCProxy(object): (name,), {}) return value else: - raise AttributeError(name) + raise AttributeError, name def __getattributes(self): self.__attributes = self.sockio.remotecall(self.oid, @@ -583,19 +567,20 @@ def _getmethods(obj, methods): # Adds names to dictionary argument 'methods' for name in dir(obj): attr = getattr(obj, name) - if callable(attr): + if hasattr(attr, '__call__'): methods[name] = 1 - if isinstance(obj, type): + if type(obj) == types.InstanceType: + _getmethods(obj.__class__, methods) + if type(obj) == types.ClassType: for super in obj.__bases__: _getmethods(super, methods) def _getattributes(obj, attributes): for name in dir(obj): attr = getattr(obj, name) - if not callable(attr): + if not hasattr(attr, '__call__'): attributes[name] = 1 - class MethodProxy(object): def __init__(self, sockio, oid, name): @@ -603,33 +588,10 @@ class MethodProxy(object): self.oid = oid self.name = name - def __call__(self, /, *args, **kwargs): + def __call__(self, *args, **kwargs): value = self.sockio.remotecall(self.oid, self.name, args, kwargs) return value # XXX KBK 09Sep03 We need a proper unit test for this module. Previously # existing test code was removed at Rev 1.27 (r34098). - -def displayhook(value): - """Override standard display hook to use non-locale encoding""" - if value is None: - return - # Set '_' to None to avoid recursion - builtins._ = None - text = repr(value) - try: - sys.stdout.write(text) - except UnicodeEncodeError: - # let's use ascii while utf8-bmp codec doesn't present - encoding = 'ascii' - bytes = text.encode(encoding, 'backslashreplace') - text = bytes.decode(encoding, 'strict') - sys.stdout.write(text) - sys.stdout.write("\n") - builtins._ = value - - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_rpc', verbosity=2,) diff --git a/Lib/idlelib/run.py b/Lib/idlelib/run.py index 5bd84aa..518afab 100644 --- a/Lib/idlelib/run.py +++ b/Lib/idlelib/run.py @@ -1,56 +1,27 @@ -""" idlelib.run - -Simplified, pyshell.ModifiedInterpreter spawns a subprocess with -f'''{sys.executable} -c "__import__('idlelib.run').run.main()"''' -'.run' is needed because __import__ returns idlelib, not idlelib.run. -""" -import functools -import io -import linecache -import queue import sys -import textwrap +import linecache import time +import socket import traceback -import _thread as thread +import thread import threading -import warnings +import Queue -from idlelib import autocomplete # AutoComplete, fetch_encodings -from idlelib import calltip # Calltip -from idlelib import debugger_r # start_debugger -from idlelib import debugobj_r # remote_object_tree_item -from idlelib import iomenu # encoding -from idlelib import rpc # multiple objects -from idlelib import stackviewer # StackTreeItem -import __main__ +from idlelib import CallTips +from idlelib import AutoComplete -import tkinter # Use tcl and, if startup fails, messagebox. -if not hasattr(sys.modules['idlelib.run'], 'firstrun'): - # Undo modifications of tkinter by idlelib imports; see bpo-25507. - for mod in ('simpledialog', 'messagebox', 'font', - 'dialog', 'filedialog', 'commondialog', - 'ttk'): - delattr(tkinter, mod) - del sys.modules['tkinter.' + mod] - # Avoid AttributeError if run again; see bpo-37038. - sys.modules['idlelib.run'].firstrun = False - -LOCALHOST = '127.0.0.1' +from idlelib import RemoteDebugger +from idlelib import RemoteObjectBrowser +from idlelib import StackViewer +from idlelib import rpc +from idlelib import PyShell +from idlelib import IOBinding +import __main__ -def idle_formatwarning(message, category, filename, lineno, line=None): - """Format warnings the IDLE way.""" +LOCALHOST = '127.0.0.1' - s = "\nWarning (from warnings module):\n" - s += ' File \"%s\", line %s\n' % (filename, lineno) - if line is None: - line = linecache.getline(filename, lineno) - line = line.strip() - if line: - s += " %s\n" % line - s += "%s: %s\n" % (category.__name__, message) - return s +import warnings def idle_showwarning_subproc( message, category, filename, lineno, file=None, line=None): @@ -61,9 +32,9 @@ def idle_showwarning_subproc( if file is None: file = sys.stderr try: - file.write(idle_formatwarning( + file.write(PyShell.idle_formatwarning( message, category, filename, lineno, line)) - except OSError: + except IOError: pass # the file (probably stderr) is invalid - this warning gets lost. _warnings_showwarning = None @@ -82,13 +53,6 @@ def capture_warnings(capture): _warnings_showwarning = None capture_warnings(True) -tcl = tkinter.Tcl() - -def handle_tk_events(tcl=tcl): - """Process any tk events that are ready to be dispatched if tkinter - has been imported, a tcl interpreter has been created and tk has been - loaded.""" - tcl.eval("update") # Thread shared globals: Establish a queue between a subthread (which handles # the socket) and the main thread (which runs user code), plus global @@ -111,7 +75,7 @@ def main(del_exitfunc=False): MyHandler object. That reference is saved as attribute rpchandler of the Executive instance. The Executive methods have access to the reference and can pass it on to entities that they command - (e.g. debugger_r.Debugger.start_debugger()). The latter, in turn, can + (e.g. RemoteDebugger.Debugger.start_debugger()). The latter, in turn, can call MyHandler(SocketIO) register/unregister methods via the reference to register and unregister themselves. @@ -125,8 +89,7 @@ def main(del_exitfunc=False): assert(len(sys.argv) > 1) port = int(sys.argv[-1]) except: - print("IDLE Subprocess: no IP port passed in sys.argv.", - file=sys.__stderr__) + print>>sys.stderr, "IDLE Subprocess: no IP port passed in sys.argv." return capture_warnings(True) @@ -134,7 +97,7 @@ def main(del_exitfunc=False): sockthread = threading.Thread(target=manage_socket, name='SockThread', args=((LOCALHOST, port),)) - sockthread.daemon = True + sockthread.setDaemon(True) sockthread.start() while 1: try: @@ -145,17 +108,12 @@ def main(del_exitfunc=False): # exiting but got an extra KBI? Try again! continue try: - request = rpc.request_queue.get(block=True, timeout=0.05) - except queue.Empty: - request = None - # Issue 32207: calling handle_tk_events here adds spurious - # queue.Empty traceback to event handling exceptions. - if request: - seq, (method, args, kwargs) = request - ret = method(*args, **kwargs) - rpc.response_queue.put((seq, ret)) - else: - handle_tk_events() + seq, request = rpc.request_queue.get(block=True, timeout=0.05) + except Queue.Empty: + continue + method, args, kwargs = request + ret = method(*args, **kwargs) + rpc.response_queue.put((seq, ret)) except KeyboardInterrupt: if quitting: exit_now = True @@ -181,33 +139,33 @@ def manage_socket(address): try: server = MyRPCServer(address, MyHandler) break - except OSError as err: - print("IDLE Subprocess: OSError: " + err.args[1] + - ", retrying....", file=sys.__stderr__) - socket_error = err + except socket.error as err: + print>>sys.__stderr__,"IDLE Subprocess: socket error: "\ + + err.args[1] + ", retrying...." else: - print("IDLE Subprocess: Connection to " - "IDLE GUI failed, exiting.", file=sys.__stderr__) - show_socket_error(socket_error, address) + print>>sys.__stderr__, "IDLE Subprocess: Connection to "\ + "IDLE GUI failed, exiting." + show_socket_error(err, address) global exit_now exit_now = True return server.handle_request() # A single request only def show_socket_error(err, address): - "Display socket error from manage_socket." - import tkinter - from tkinter.messagebox import showerror - root = tkinter.Tk() + import Tkinter + import tkMessageBox + root = Tkinter.Tk() fix_scaling(root) root.withdraw() - showerror( - "Subprocess Connection Error", - f"IDLE's subprocess can't connect to {address[0]}:{address[1]}.\n" - f"Fatal OSError #{err.errno}: {err.strerror}.\n" - "See the 'Startup failure' section of the IDLE doc, online at\n" - "https://docs.python.org/3/library/idle.html#startup-failure", - parent=root) + if err.args[0] == 61: # connection refused + msg = "IDLE's subprocess can't connect to %s:%d. This may be due "\ + "to your personal firewall configuration. It is safe to "\ + "allow this internal connection because no data is visible on "\ + "external ports." % address + tkMessageBox.showerror("IDLE Subprocess Error", msg, parent=root) + else: + tkMessageBox.showerror("IDLE Subprocess Error", + "Socket Error: %s" % err.args[1], parent=root) root.destroy() def print_exception(): @@ -217,34 +175,15 @@ def print_exception(): efile = sys.stderr typ, val, tb = excinfo = sys.exc_info() sys.last_type, sys.last_value, sys.last_traceback = excinfo - seen = set() - - def print_exc(typ, exc, tb): - seen.add(id(exc)) - context = exc.__context__ - cause = exc.__cause__ - if cause is not None and id(cause) not in seen: - print_exc(type(cause), cause, cause.__traceback__) - print("\nThe above exception was the direct cause " - "of the following exception:\n", file=efile) - elif (context is not None and - not exc.__suppress_context__ and - id(context) not in seen): - print_exc(type(context), context, context.__traceback__) - print("\nDuring handling of the above exception, " - "another exception occurred:\n", file=efile) - if tb: - tbe = traceback.extract_tb(tb) - print('Traceback (most recent call last):', file=efile) - exclude = ("run.py", "rpc.py", "threading.py", "queue.py", - "debugger_r.py", "bdb.py") - cleanup_traceback(tbe, exclude) - traceback.print_list(tbe, file=efile) - lines = traceback.format_exception_only(typ, exc) - for line in lines: - print(line, end='', file=efile) - - print_exc(typ, val, tb) + tbe = traceback.extract_tb(tb) + print>>efile, '\nTraceback (most recent call last):' + exclude = ("run.py", "rpc.py", "threading.py", "Queue.py", + "RemoteDebugger.py", "bdb.py") + cleanup_traceback(tbe, exclude) + traceback.print_list(tbe, file=efile) + lines = traceback.format_exception_only(typ, val) + for line in lines: + print>>efile, line, def cleanup_traceback(tb, exclude): "Remove excluded traces from beginning/end of tb; get cached lines" @@ -266,106 +205,53 @@ def cleanup_traceback(tb, exclude): if len(tb) == 0: # exception was in IDLE internals, don't prune! tb[:] = orig_tb[:] - print("** IDLE Internal Exception: ", file=sys.stderr) + print>>sys.stderr, "** IDLE Internal Exception: " rpchandler = rpc.objecttable['exec'].rpchandler for i in range(len(tb)): fn, ln, nm, line = tb[i] if nm == '?': nm = "-toplevel-" + if fn.startswith("<pyshell#") and IOBinding.encoding != 'utf-8': + ln -= 1 # correction for coding cookie if not line and fn.startswith("<pyshell#"): line = rpchandler.remotecall('linecache', 'getline', (fn, ln), {}) tb[i] = fn, ln, nm, line def flush_stdout(): - """XXX How to do this now?""" + try: + if sys.stdout.softspace: + sys.stdout.softspace = 0 + sys.stdout.write("\n") + except (AttributeError, EOFError): + pass def exit(): - """Exit subprocess, possibly after first clearing exit functions. + """Exit subprocess, possibly after first deleting sys.exitfunc If config-main.cfg/.def 'General' 'delete-exitfunc' is True, then any - functions registered with atexit will be removed before exiting. - (VPython support) + sys.exitfunc will be removed before exiting. (VPython support) """ if no_exitfunc: - import atexit - atexit._clear() + try: + del sys.exitfunc + except AttributeError: + pass capture_warnings(False) sys.exit(0) def fix_scaling(root): """Scale fonts on HiDPI displays.""" - import tkinter.font + import tkFont scaling = float(root.tk.call('tk', 'scaling')) if scaling > 1.4: - for name in tkinter.font.names(root): - font = tkinter.font.Font(root=root, name=name, exists=True) + for name in tkFont.names(root): + font = tkFont.Font(root=root, name=name, exists=True) size = int(font['size']) if size < 0: - font['size'] = round(-0.75*size) - - -def fixdoc(fun, text): - tem = (fun.__doc__ + '\n\n') if fun.__doc__ is not None else '' - fun.__doc__ = tem + textwrap.fill(textwrap.dedent(text)) - -RECURSIONLIMIT_DELTA = 30 - -def install_recursionlimit_wrappers(): - """Install wrappers to always add 30 to the recursion limit.""" - # see: bpo-26806 - - @functools.wraps(sys.setrecursionlimit) - def setrecursionlimit(*args, **kwargs): - # mimic the original sys.setrecursionlimit()'s input handling - if kwargs: - raise TypeError( - "setrecursionlimit() takes no keyword arguments") - try: - limit, = args - except ValueError: - raise TypeError(f"setrecursionlimit() takes exactly one " - f"argument ({len(args)} given)") - if not limit > 0: - raise ValueError( - "recursion limit must be greater or equal than 1") - - return setrecursionlimit.__wrapped__(limit + RECURSIONLIMIT_DELTA) - - fixdoc(setrecursionlimit, f"""\ - This IDLE wrapper adds {RECURSIONLIMIT_DELTA} to prevent possible - uninterruptible loops.""") - - @functools.wraps(sys.getrecursionlimit) - def getrecursionlimit(): - return getrecursionlimit.__wrapped__() - RECURSIONLIMIT_DELTA - - fixdoc(getrecursionlimit, f"""\ - This IDLE wrapper subtracts {RECURSIONLIMIT_DELTA} to compensate - for the {RECURSIONLIMIT_DELTA} IDLE adds when setting the limit.""") - - # add the delta to the default recursion limit, to compensate - sys.setrecursionlimit(sys.getrecursionlimit() + RECURSIONLIMIT_DELTA) - - sys.setrecursionlimit = setrecursionlimit - sys.getrecursionlimit = getrecursionlimit - - -def uninstall_recursionlimit_wrappers(): - """Uninstall the recursion limit wrappers from the sys module. - - IDLE only uses this for tests. Users can import run and call - this to remove the wrapping. - """ - if ( - getattr(sys.setrecursionlimit, '__wrapped__', None) and - getattr(sys.getrecursionlimit, '__wrapped__', None) - ): - sys.setrecursionlimit = sys.setrecursionlimit.__wrapped__ - sys.getrecursionlimit = sys.getrecursionlimit.__wrapped__ - sys.setrecursionlimit(sys.getrecursionlimit() - RECURSIONLIMIT_DELTA) + font['size'] = int(round(-0.75*size)) class MyRPCServer(rpc.RPCServer): @@ -387,105 +273,17 @@ class MyRPCServer(rpc.RPCServer): thread.interrupt_main() except: erf = sys.__stderr__ - print('\n' + '-'*40, file=erf) - print('Unhandled server exception!', file=erf) - print('Thread: %s' % threading.current_thread().name, file=erf) - print('Client Address: ', client_address, file=erf) - print('Request: ', repr(request), file=erf) + print>>erf, '\n' + '-'*40 + print>>erf, 'Unhandled server exception!' + print>>erf, 'Thread: %s' % threading.currentThread().getName() + print>>erf, 'Client Address: ', client_address + print>>erf, 'Request: ', repr(request) traceback.print_exc(file=erf) - print('\n*** Unrecoverable, server exiting!', file=erf) - print('-'*40, file=erf) + print>>erf, '\n*** Unrecoverable, server exiting!' + print>>erf, '-'*40 quitting = True thread.interrupt_main() - -# Pseudofiles for shell-remote communication (also used in pyshell) - -class StdioFile(io.TextIOBase): - - def __init__(self, shell, tags, encoding='utf-8', errors='strict'): - self.shell = shell - self.tags = tags - self._encoding = encoding - self._errors = errors - - @property - def encoding(self): - return self._encoding - - @property - def errors(self): - return self._errors - - @property - def name(self): - return '<%s>' % self.tags - - def isatty(self): - return True - - -class StdOutputFile(StdioFile): - - def writable(self): - return True - - def write(self, s): - if self.closed: - raise ValueError("write to closed file") - s = str.encode(s, self.encoding, self.errors).decode(self.encoding, self.errors) - return self.shell.write(s, self.tags) - - -class StdInputFile(StdioFile): - _line_buffer = '' - - def readable(self): - return True - - def read(self, size=-1): - if self.closed: - raise ValueError("read from closed file") - if size is None: - size = -1 - elif not isinstance(size, int): - raise TypeError('must be int, not ' + type(size).__name__) - result = self._line_buffer - self._line_buffer = '' - if size < 0: - while True: - line = self.shell.readline() - if not line: break - result += line - else: - while len(result) < size: - line = self.shell.readline() - if not line: break - result += line - self._line_buffer = result[size:] - result = result[:size] - return result - - def readline(self, size=-1): - if self.closed: - raise ValueError("read from closed file") - if size is None: - size = -1 - elif not isinstance(size, int): - raise TypeError('must be int, not ' + type(size).__name__) - line = self._line_buffer or self.shell.readline() - if size < 0: - size = len(line) - eol = line.find('\n', 0, size) - if eol >= 0: - size = eol + 1 - self._line_buffer = line[size:] - return line[:size] - - def close(self): - self.shell.close() - - class MyHandler(rpc.RPCHandler): def handle(self): @@ -493,24 +291,17 @@ class MyHandler(rpc.RPCHandler): executive = Executive(self) self.register("exec", executive) self.console = self.get_remote_proxy("console") - sys.stdin = StdInputFile(self.console, "stdin", - iomenu.encoding, iomenu.errors) - sys.stdout = StdOutputFile(self.console, "stdout", - iomenu.encoding, iomenu.errors) - sys.stderr = StdOutputFile(self.console, "stderr", - iomenu.encoding, "backslashreplace") - - sys.displayhook = rpc.displayhook - # page help() text to shell. - import pydoc # import must be done here to capture i/o binding - pydoc.pager = pydoc.plainpager + sys.stdin = PyShell.PseudoInputFile(self.console, "stdin", + IOBinding.encoding) + sys.stdout = PyShell.PseudoOutputFile(self.console, "stdout", + IOBinding.encoding) + sys.stderr = PyShell.PseudoOutputFile(self.console, "stderr", + IOBinding.encoding) # Keep a reference to stdin so that it won't try to exit IDLE if # sys.stdin gets changed from within IDLE's shell. See issue17838. self._keep_stdin = sys.stdin - install_recursionlimit_wrappers() - self.interp = self.get_remote_proxy("interp") rpc.RPCHandler.getresponse(self, myseq=None, wait=0.05) @@ -536,8 +327,8 @@ class Executive(object): def __init__(self, rpchandler): self.rpchandler = rpchandler self.locals = __main__.__dict__ - self.calltip = calltip.Calltip() - self.autocomplete = autocomplete.AutoComplete() + self.calltip = CallTips.CallTips() + self.autocomplete = AutoComplete.AutoComplete() def runcode(self, code): global interruptable @@ -545,15 +336,13 @@ class Executive(object): self.usr_exc_info = None interruptable = True try: - exec(code, self.locals) + exec code in self.locals finally: interruptable = False - except SystemExit as e: - if e.args: # SystemExit called with an argument. - ob = e.args[0] - if not isinstance(ob, (type(None), int)): - print('SystemExit: ' + str(ob), file=sys.stderr) - # Return to the interactive prompt. + except SystemExit: + # Scripts that raise SystemExit should just + # return to the interactive prompt + pass except: self.usr_exc_info = sys.exc_info() if quitting: @@ -570,7 +359,7 @@ class Executive(object): thread.interrupt_main() def start_the_debugger(self, gui_adap_oid): - return debugger_r.start_debugger(self.rpchandler, gui_adap_oid) + return RemoteDebugger.start_debugger(self.rpchandler, gui_adap_oid) def stop_the_debugger(self, idb_adap_oid): "Unregister the Idb Adapter. Link objects and Idb then subject to GC" @@ -594,12 +383,7 @@ class Executive(object): tb = tb.tb_next sys.last_type = typ sys.last_value = val - item = stackviewer.StackTreeItem(flist, tb) - return debugobj_r.remote_object_tree_item(item) - - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_run', verbosity=2) + item = StackViewer.StackTreeItem(flist, tb) + return RemoteObjectBrowser.remote_object_tree_item(item) -capture_warnings(False) # Make sure turned off; see bpo-18081. +capture_warnings(False) # Make sure turned off; see issue 18081 diff --git a/Lib/idlelib/runscript.py b/Lib/idlelib/runscript.py deleted file mode 100644 index a541087..0000000 --- a/Lib/idlelib/runscript.py +++ /dev/null @@ -1,225 +0,0 @@ -"""Execute code from an editor. - -Check module: do a full syntax check of the current module. -Also run the tabnanny to catch any inconsistent tabs. - -Run module: also execute the module's code in the __main__ namespace. -The window must have been saved previously. The module is added to -sys.modules, and is also added to the __main__ namespace. - -TODO: Specify command line arguments in a dialog box. -""" -import os -import tabnanny -import tokenize - -import tkinter.messagebox as tkMessageBox - -from idlelib.config import idleConf -from idlelib import macosx -from idlelib import pyshell -from idlelib.query import CustomRun -from idlelib import outwin - -indent_message = """Error: Inconsistent indentation detected! - -1) Your indentation is outright incorrect (easy to fix), OR - -2) Your indentation mixes tabs and spaces. - -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: - - def __init__(self, editwin): - self.editwin = editwin - # Provide instance variables referenced by debugger - # XXX This should be done differently - self.flist = self.editwin.flist - self.root = self.editwin.root - # cli_args is list of strings that extends sys.argv - self.cli_args = [] - - if macosx.isCocoaTk(): - self.editwin.text_frame.bind('<<run-module-event-2>>', self._run_module_event) - - def check_module_event(self, event): - if isinstance(self.editwin, outwin.OutputWindow): - self.editwin.text.bell() - return 'break' - filename = self.getfilename() - if not filename: - return 'break' - if not self.checksyntax(filename): - return 'break' - if not self.tabnanny(filename): - return 'break' - return "break" - - def tabnanny(self, filename): - # XXX: tabnanny should work on binary files as well - with tokenize.open(filename) as f: - try: - tabnanny.process_tokens(tokenize.generate_tokens(f.readline)) - except tokenize.TokenError as msg: - msgtxt, (lineno, start) = msg.args - self.editwin.gotoline(lineno) - self.errorbox("Tabnanny Tokenizing Error", - "Token Error: %s" % msgtxt) - return False - except tabnanny.NannyNag as nag: - # The error messages from tabnanny are too confusing... - self.editwin.gotoline(nag.get_lineno()) - self.errorbox("Tab/space error", indent_message) - return False - return True - - def checksyntax(self, filename): - self.shell = shell = self.flist.open_shell() - saved_stream = shell.get_warning_stream() - shell.set_warning_stream(shell.stderr) - with open(filename, 'rb') as f: - source = f.read() - if b'\r' in source: - source = source.replace(b'\r\n', b'\n') - source = source.replace(b'\r', b'\n') - if source and source[-1] != ord(b'\n'): - source = source + b'\n' - editwin = self.editwin - text = editwin.text - text.tag_remove("ERROR", "1.0", "end") - try: - # If successful, return the compiled code - return compile(source, filename, "exec") - except (SyntaxError, OverflowError, ValueError) as value: - msg = getattr(value, 'msg', '') or value or "<no detail available>" - lineno = getattr(value, 'lineno', '') or 1 - offset = getattr(value, 'offset', '') or 0 - if offset == 0: - lineno += 1 #mark end of offending line - pos = "0.0 + %d lines + %d chars" % (lineno-1, offset-1) - editwin.colorize_syntax_error(text, pos) - self.errorbox("SyntaxError", "%-20s" % msg) - return False - finally: - shell.set_warning_stream(saved_stream) - - def run_module_event(self, event): - if macosx.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 - # tries to run a module using the keyboard shortcut - # (the menu item works fine). - self.editwin.text_frame.after(200, - lambda: self.editwin.text_frame.event_generate( - '<<run-module-event-2>>')) - return 'break' - else: - return self._run_module_event(event) - - def run_custom_event(self, event): - return self._run_module_event(event, customize=True) - - def _run_module_event(self, event, *, customize=False): - """Run the module after setting up the environment. - - First check the syntax. Next get customization. If OK, make - sure the shell is active and then transfer the arguments, set - the run environment's working directory to the directory of the - module being executed and also add that directory to its - sys.path if not already included. - """ - if isinstance(self.editwin, outwin.OutputWindow): - self.editwin.text.bell() - return 'break' - filename = self.getfilename() - if not filename: - return 'break' - code = self.checksyntax(filename) - if not code: - return 'break' - if not self.tabnanny(filename): - return 'break' - if customize: - title = f"Customize {self.editwin.short_title()} Run" - run_args = CustomRun(self.shell.text, title, - cli_args=self.cli_args).result - if not run_args: # User cancelled. - return 'break' - self.cli_args, restart = run_args if customize else ([], True) - interp = self.shell.interp - if pyshell.use_subprocess and restart: - interp.restart_subprocess( - with_cwd=False, filename=filename) - dirname = os.path.dirname(filename) - argv = [filename] - if self.cli_args: - argv += self.cli_args - interp.runcommand(f"""if 1: - __file__ = {filename!r} - import sys as _sys - from os.path import basename as _basename - argv = {argv!r} - if (not _sys.argv or - _basename(_sys.argv[0]) != _basename(__file__) or - len(argv) > 1): - _sys.argv = argv - import os as _os - _os.chdir({dirname!r}) - del _sys, argv, _basename, _os - \n""") - interp.prepend_syspath(filename) - # XXX KBK 03Jul04 When run w/o subprocess, runtime warnings still - # go to __stderr__. With subprocess, they go to the shell. - # Need to change streams in pyshell.ModifiedInterpreter. - interp.runcode(code) - return 'break' - - def getfilename(self): - """Get source filename. If not saved, offer to save (or create) file - - The debugger requires a source file. Make sure there is one, and that - the current version of the source buffer has been saved. If the user - declines to save or cancels the Save As dialog, return None. - - If the user has configured IDLE for Autosave, the file will be - silently saved if it already exists and is dirty. - - """ - filename = self.editwin.io.filename - if not self.editwin.get_saved(): - autosave = idleConf.GetOption('main', 'General', - 'autosave', type='bool') - if autosave and filename: - self.editwin.io.save(None) - else: - confirm = self.ask_save_dialog() - self.editwin.text.focus_set() - if confirm: - self.editwin.io.save(None) - filename = self.editwin.io.filename - else: - filename = None - return filename - - def ask_save_dialog(self): - msg = "Source Must Be Saved\n" + 5*' ' + "OK to Save?" - confirm = tkMessageBox.askokcancel(title="Save Before Run or Check", - message=msg, - default=tkMessageBox.OK, - parent=self.editwin.text) - return confirm - - def errorbox(self, title, message): - # XXX This should really be a function of EditorWindow... - tkMessageBox.showerror(title, message, parent=self.editwin.text) - self.editwin.text.focus_set() - - -if __name__ == "__main__": - from unittest import main - main('idlelib.idle_test.test_runscript', verbosity=2,) diff --git a/Lib/idlelib/search.py b/Lib/idlelib/search.py deleted file mode 100644 index b35f3b5..0000000 --- a/Lib/idlelib/search.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Search dialog for Find, Find Again, and Find Selection - functionality. - - Inherits from SearchDialogBase for GUI and uses searchengine - to prepare search pattern. -""" -from tkinter import TclError - -from idlelib import searchengine -from idlelib.searchbase import SearchDialogBase - -def _setup(text): - """Return the new or existing singleton SearchDialog instance. - - The singleton dialog saves user entries and preferences - across instances. - - Args: - text: Text widget containing the text to be searched. - """ - root = text._root() - engine = searchengine.get(root) - if not hasattr(engine, "_searchdialog"): - engine._searchdialog = SearchDialog(root, engine) - return engine._searchdialog - -def find(text): - """Open the search dialog. - - Module-level function to access the singleton SearchDialog - instance and open the dialog. If text is selected, it is - used as the search phrase; otherwise, the previous entry - is used. No search is done with this command. - """ - pat = text.get("sel.first", "sel.last") - return _setup(text).open(text, pat) # Open is inherited from SDBase. - -def find_again(text): - """Repeat the search for the last pattern and preferences. - - Module-level function to access the singleton SearchDialog - instance to search again using the user entries and preferences - from the last dialog. If there was no prior search, open the - search dialog; otherwise, perform the search without showing the - dialog. - """ - return _setup(text).find_again(text) - -def find_selection(text): - """Search for the selected pattern in the text. - - Module-level function to access the singleton SearchDialog - instance to search using the selected text. With a text - selection, perform the search without displaying the dialog. - Without a selection, use the prior entry as the search phrase - and don't display the dialog. If there has been no prior - search, open the search dialog. - """ - return _setup(text).find_selection(text) - - -class SearchDialog(SearchDialogBase): - "Dialog for finding a pattern in text." - - def create_widgets(self): - "Create the base search dialog and add a button for Find Next." - SearchDialogBase.create_widgets(self) - # TODO - why is this here and not in a create_command_buttons? - self.make_button("Find Next", self.default_command, isdef=True) - - def default_command(self, event=None): - "Handle the Find Next button as the default command." - if not self.engine.getprog(): - return - self.find_again(self.text) - - def find_again(self, text): - """Repeat the last search. - - If no search was previously run, open a new search dialog. In - this case, no search is done. - - If a search was previously run, the search dialog won't be - shown and the options from the previous search (including the - search pattern) will be used to find the next occurrence - of the pattern. Next is relative based on direction. - - Position the window to display the located occurrence in the - text. - - Return True if the search was successful and False otherwise. - """ - if not self.engine.getpat(): - self.open(text) - return False - if not self.engine.getprog(): - return False - res = self.engine.search_text(text) - if res: - line, m = res - i, j = m.span() - first = "%d.%d" % (line, i) - last = "%d.%d" % (line, j) - try: - selfirst = text.index("sel.first") - sellast = text.index("sel.last") - if selfirst == first and sellast == last: - self.bell() - return False - except TclError: - pass - text.tag_remove("sel", "1.0", "end") - text.tag_add("sel", first, last) - text.mark_set("insert", self.engine.isback() and first or last) - text.see("insert") - return True - else: - self.bell() - return False - - def find_selection(self, text): - """Search for selected text with previous dialog preferences. - - Instead of using the same pattern for searching (as Find - Again does), this first resets the pattern to the currently - selected text. If the selected text isn't changed, then use - the prior search phrase. - """ - pat = text.get("sel.first", "sel.last") - if pat: - self.engine.setcookedpat(pat) - return self.find_again(text) - - -def _search_dialog(parent): # htest # - "Display search test box." - from tkinter import Toplevel, Text - from tkinter.ttk import Frame, Button - - top = Toplevel(parent) - top.title("Test SearchDialog") - x, y = map(int, parent.geometry().split('+')[1:]) - top.geometry("+%d+%d" % (x, y + 175)) - - frame = Frame(top) - frame.pack() - text = Text(frame, inactiveselectbackground='gray') - text.pack() - text.insert("insert","This is a sample string.\n"*5) - - def show_find(): - text.tag_add('sel', '1.0', 'end') - _setup(text).open(text) - text.tag_remove('sel', '1.0', 'end') - - button = Button(frame, text="Search (selection ignored)", command=show_find) - button.pack() - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_search', verbosity=2, exit=False) - - from idlelib.idle_test.htest import run - run(_search_dialog) diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py deleted file mode 100644 index 41c0968..0000000 --- a/Lib/idlelib/sidebar.py +++ /dev/null @@ -1,341 +0,0 @@ -"""Line numbering implementation for IDLE as an extension. -Includes BaseSideBar which can be extended for other sidebar based extensions -""" -import functools -import itertools - -import tkinter as tk -from idlelib.config import idleConf -from idlelib.delegator import Delegator - - -def get_end_linenumber(text): - """Utility to get the last line's number in a Tk text widget.""" - return int(float(text.index('end-1c'))) - - -def get_widget_padding(widget): - """Get the total padding of a Tk widget, including its border.""" - # TODO: use also in codecontext.py - manager = widget.winfo_manager() - if manager == 'pack': - info = widget.pack_info() - elif manager == 'grid': - info = widget.grid_info() - else: - raise ValueError(f"Unsupported geometry manager: {manager}") - - # All values are passed through getint(), since some - # values may be pixel objects, which can't simply be added to ints. - padx = sum(map(widget.tk.getint, [ - info['padx'], - widget.cget('padx'), - widget.cget('border'), - ])) - pady = sum(map(widget.tk.getint, [ - info['pady'], - widget.cget('pady'), - widget.cget('border'), - ])) - return padx, pady - - -class BaseSideBar: - """ - The base class for extensions which require a sidebar. - """ - def __init__(self, editwin): - self.editwin = editwin - self.parent = editwin.text_frame - self.text = editwin.text - - _padx, pady = get_widget_padding(self.text) - self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE, - padx=2, pady=pady, - borderwidth=0, highlightthickness=0) - self.sidebar_text.config(state=tk.DISABLED) - self.text['yscrollcommand'] = self.redirect_yscroll_event - self.update_font() - self.update_colors() - - self.is_shown = False - - def update_font(self): - """Update the sidebar text font, usually after config changes.""" - font = idleConf.GetFont(self.text, 'main', 'EditorWindow') - self._update_font(font) - - def _update_font(self, font): - self.sidebar_text['font'] = font - - def update_colors(self): - """Update the sidebar text colors, usually after config changes.""" - colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'normal') - self._update_colors(foreground=colors['foreground'], - background=colors['background']) - - def _update_colors(self, foreground, background): - self.sidebar_text.config( - fg=foreground, bg=background, - selectforeground=foreground, selectbackground=background, - inactiveselectbackground=background, - ) - - def show_sidebar(self): - if not self.is_shown: - self.sidebar_text.grid(row=1, column=0, sticky=tk.NSEW) - self.is_shown = True - - def hide_sidebar(self): - if self.is_shown: - self.sidebar_text.grid_forget() - self.is_shown = False - - def redirect_yscroll_event(self, *args, **kwargs): - """Redirect vertical scrolling to the main editor text widget. - - The scroll bar is also updated. - """ - self.editwin.vbar.set(*args) - self.sidebar_text.yview_moveto(args[0]) - return 'break' - - def redirect_focusin_event(self, event): - """Redirect focus-in events to the main editor text widget.""" - self.text.focus_set() - return 'break' - - def redirect_mousebutton_event(self, event, event_name): - """Redirect mouse button events to the main editor text widget.""" - self.text.focus_set() - self.text.event_generate(event_name, x=0, y=event.y) - return 'break' - - def redirect_mousewheel_event(self, event): - """Redirect mouse wheel events to the editwin text widget.""" - self.text.event_generate('<MouseWheel>', - x=0, y=event.y, delta=event.delta) - return 'break' - - -class EndLineDelegator(Delegator): - """Generate callbacks with the current end line number after - insert or delete operations""" - def __init__(self, changed_callback): - """ - changed_callback - Callable, will be called after insert - or delete operations with the current - end line number. - """ - Delegator.__init__(self) - self.changed_callback = changed_callback - - def insert(self, index, chars, tags=None): - self.delegate.insert(index, chars, tags) - self.changed_callback(get_end_linenumber(self.delegate)) - - def delete(self, index1, index2=None): - self.delegate.delete(index1, index2) - self.changed_callback(get_end_linenumber(self.delegate)) - - -class LineNumbers(BaseSideBar): - """Line numbers support for editor windows.""" - def __init__(self, editwin): - BaseSideBar.__init__(self, editwin) - self.prev_end = 1 - self._sidebar_width_type = type(self.sidebar_text['width']) - self.sidebar_text.config(state=tk.NORMAL) - self.sidebar_text.insert('insert', '1', 'linenumber') - self.sidebar_text.config(state=tk.DISABLED) - self.sidebar_text.config(takefocus=False, exportselection=False) - self.sidebar_text.tag_config('linenumber', justify=tk.RIGHT) - - self.bind_events() - - end = get_end_linenumber(self.text) - self.update_sidebar_text(end) - - end_line_delegator = EndLineDelegator(self.update_sidebar_text) - # Insert the delegator after the undo delegator, so that line numbers - # are properly updated after undo and redo actions. - end_line_delegator.setdelegate(self.editwin.undo.delegate) - self.editwin.undo.setdelegate(end_line_delegator) - # Reset the delegator caches of the delegators "above" the - # end line delegator we just inserted. - delegator = self.editwin.per.top - while delegator is not end_line_delegator: - delegator.resetcache() - delegator = delegator.delegate - - self.is_shown = False - - def bind_events(self): - # Ensure focus is always redirected to the main editor text widget. - self.sidebar_text.bind('<FocusIn>', self.redirect_focusin_event) - - # Redirect mouse scrolling to the main editor text widget. - # - # Note that without this, scrolling with the mouse only scrolls - # the line numbers. - self.sidebar_text.bind('<MouseWheel>', self.redirect_mousewheel_event) - - # Redirect mouse button events to the main editor text widget, - # except for the left mouse button (1). - # - # Note: X-11 sends Button-4 and Button-5 events for the scroll wheel. - def bind_mouse_event(event_name, target_event_name): - handler = functools.partial(self.redirect_mousebutton_event, - event_name=target_event_name) - self.sidebar_text.bind(event_name, handler) - - for button in [2, 3, 4, 5]: - for event_name in (f'<Button-{button}>', - f'<ButtonRelease-{button}>', - f'<B{button}-Motion>', - ): - bind_mouse_event(event_name, target_event_name=event_name) - - # Convert double- and triple-click events to normal click events, - # since event_generate() doesn't allow generating such events. - for event_name in (f'<Double-Button-{button}>', - f'<Triple-Button-{button}>', - ): - bind_mouse_event(event_name, - target_event_name=f'<Button-{button}>') - - # This is set by b1_mousedown_handler() and read by - # drag_update_selection_and_insert_mark(), to know where dragging - # began. - start_line = None - # These are set by b1_motion_handler() and read by selection_handler(). - # last_y is passed this way since the mouse Y-coordinate is not - # available on selection event objects. last_yview is passed this way - # to recognize scrolling while the mouse isn't moving. - last_y = last_yview = None - - def b1_mousedown_handler(event): - # select the entire line - lineno = int(float(self.sidebar_text.index(f"@0,{event.y}"))) - self.text.tag_remove("sel", "1.0", "end") - self.text.tag_add("sel", f"{lineno}.0", f"{lineno+1}.0") - self.text.mark_set("insert", f"{lineno+1}.0") - - # remember this line in case this is the beginning of dragging - nonlocal start_line - start_line = lineno - self.sidebar_text.bind('<Button-1>', b1_mousedown_handler) - - def b1_mouseup_handler(event): - # On mouse up, we're no longer dragging. Set the shared persistent - # variables to None to represent this. - nonlocal start_line - nonlocal last_y - nonlocal last_yview - start_line = None - last_y = None - last_yview = None - self.sidebar_text.bind('<ButtonRelease-1>', b1_mouseup_handler) - - def drag_update_selection_and_insert_mark(y_coord): - """Helper function for drag and selection event handlers.""" - lineno = int(float(self.sidebar_text.index(f"@0,{y_coord}"))) - a, b = sorted([start_line, lineno]) - self.text.tag_remove("sel", "1.0", "end") - self.text.tag_add("sel", f"{a}.0", f"{b+1}.0") - self.text.mark_set("insert", - f"{lineno if lineno == a else lineno + 1}.0") - - # Special handling of dragging with mouse button 1. In "normal" text - # widgets this selects text, but the line numbers text widget has - # selection disabled. Still, dragging triggers some selection-related - # functionality under the hood. Specifically, dragging to above or - # below the text widget triggers scrolling, in a way that bypasses the - # other scrolling synchronization mechanisms.i - def b1_drag_handler(event, *args): - nonlocal last_y - nonlocal last_yview - last_y = event.y - last_yview = self.sidebar_text.yview() - if not 0 <= last_y <= self.sidebar_text.winfo_height(): - self.text.yview_moveto(last_yview[0]) - drag_update_selection_and_insert_mark(event.y) - self.sidebar_text.bind('<B1-Motion>', b1_drag_handler) - - # With mouse-drag scrolling fixed by the above, there is still an edge- - # case we need to handle: When drag-scrolling, scrolling can continue - # while the mouse isn't moving, leading to the above fix not scrolling - # properly. - def selection_handler(event): - if last_yview is None: - # This logic is only needed while dragging. - return - yview = self.sidebar_text.yview() - if yview != last_yview: - self.text.yview_moveto(yview[0]) - drag_update_selection_and_insert_mark(last_y) - self.sidebar_text.bind('<<Selection>>', selection_handler) - - def update_colors(self): - """Update the sidebar text colors, usually after config changes.""" - colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber') - self._update_colors(foreground=colors['foreground'], - background=colors['background']) - - def update_sidebar_text(self, end): - """ - Perform the following action: - Each line sidebar_text contains the linenumber for that line - Synchronize with editwin.text so that both sidebar_text and - editwin.text contain the same number of lines""" - if end == self.prev_end: - return - - width_difference = len(str(end)) - len(str(self.prev_end)) - if width_difference: - cur_width = int(float(self.sidebar_text['width'])) - new_width = cur_width + width_difference - self.sidebar_text['width'] = self._sidebar_width_type(new_width) - - self.sidebar_text.config(state=tk.NORMAL) - if end > self.prev_end: - new_text = '\n'.join(itertools.chain( - [''], - map(str, range(self.prev_end + 1, end + 1)), - )) - self.sidebar_text.insert(f'end -1c', new_text, 'linenumber') - else: - self.sidebar_text.delete(f'{end+1}.0 -1c', 'end -1c') - self.sidebar_text.config(state=tk.DISABLED) - - self.prev_end = end - - -def _linenumbers_drag_scrolling(parent): # htest # - from idlelib.idle_test.test_sidebar import Dummy_editwin - - toplevel = tk.Toplevel(parent) - text_frame = tk.Frame(toplevel) - text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - text_frame.rowconfigure(1, weight=1) - text_frame.columnconfigure(1, weight=1) - - font = idleConf.GetFont(toplevel, 'main', 'EditorWindow') - text = tk.Text(text_frame, width=80, height=24, wrap=tk.NONE, font=font) - text.grid(row=1, column=1, sticky=tk.NSEW) - - editwin = Dummy_editwin(text) - editwin.vbar = tk.Scrollbar(text_frame) - - linenumbers = LineNumbers(editwin) - linenumbers.show_sidebar() - - text.insert('1.0', '\n'.join('a'*i for i in range(1, 101))) - - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_sidebar', verbosity=2, exit=False) - - from idlelib.idle_test.htest import run - run(_linenumbers_drag_scrolling) diff --git a/Lib/idlelib/squeezer.py b/Lib/idlelib/squeezer.py deleted file mode 100644 index be1538a..0000000 --- a/Lib/idlelib/squeezer.py +++ /dev/null @@ -1,345 +0,0 @@ -"""An IDLE extension to avoid having very long texts printed in the shell. - -A common problem in IDLE's interactive shell is printing of large amounts of -text into the shell. This makes looking at the previous history difficult. -Worse, this can cause IDLE to become very slow, even to the point of being -completely unusable. - -This extension will automatically replace long texts with a small button. -Double-clicking this button will remove it and insert the original text instead. -Middle-clicking will copy the text to the clipboard. Right-clicking will open -the text in a separate viewing window. - -Additionally, any output can be manually "squeezed" by the user. This includes -output written to the standard error stream ("stderr"), such as exception -messages and their tracebacks. -""" -import re - -import tkinter as tk -import tkinter.messagebox as tkMessageBox - -from idlelib.config import idleConf -from idlelib.textview import view_text -from idlelib.tooltip import Hovertip -from idlelib import macosx - - -def count_lines_with_wrapping(s, linewidth=80): - """Count the number of lines in a given string. - - Lines are counted as if the string was wrapped so that lines are never over - linewidth characters long. - - Tabs are considered tabwidth characters long. - """ - tabwidth = 8 # Currently always true in Shell. - pos = 0 - linecount = 1 - current_column = 0 - - for m in re.finditer(r"[\t\n]", s): - # Process the normal chars up to tab or newline. - numchars = m.start() - pos - pos += numchars - current_column += numchars - - # Deal with tab or newline. - if s[pos] == '\n': - # Avoid the `current_column == 0` edge-case, and while we're - # at it, don't bother adding 0. - if current_column > linewidth: - # If the current column was exactly linewidth, divmod - # would give (1,0), even though a new line hadn't yet - # been started. The same is true if length is any exact - # multiple of linewidth. Therefore, subtract 1 before - # dividing a non-empty line. - linecount += (current_column - 1) // linewidth - linecount += 1 - current_column = 0 - else: - assert s[pos] == '\t' - current_column += tabwidth - (current_column % tabwidth) - - # If a tab passes the end of the line, consider the entire - # tab as being on the next line. - if current_column > linewidth: - linecount += 1 - current_column = tabwidth - - pos += 1 # After the tab or newline. - - # Process remaining chars (no more tabs or newlines). - current_column += len(s) - pos - # Avoid divmod(-1, linewidth). - if current_column > 0: - linecount += (current_column - 1) // linewidth - else: - # Text ended with newline; don't count an extra line after it. - linecount -= 1 - - return linecount - - -class ExpandingButton(tk.Button): - """Class for the "squeezed" text buttons used by Squeezer - - These buttons are displayed inside a Tk Text widget in place of text. A - user can then use the button to replace it with the original text, copy - the original text to the clipboard or view the original text in a separate - window. - - Each button is tied to a Squeezer instance, and it knows to update the - Squeezer instance when it is expanded (and therefore removed). - """ - def __init__(self, s, tags, numoflines, squeezer): - self.s = s - self.tags = tags - self.numoflines = numoflines - self.squeezer = squeezer - self.editwin = editwin = squeezer.editwin - self.text = text = editwin.text - # The base Text widget is needed to change text before iomark. - self.base_text = editwin.per.bottom - - line_plurality = "lines" if numoflines != 1 else "line" - button_text = f"Squeezed text ({numoflines} {line_plurality})." - tk.Button.__init__(self, text, text=button_text, - background="#FFFFC0", activebackground="#FFFFE0") - - button_tooltip_text = ( - "Double-click to expand, right-click for more options." - ) - Hovertip(self, button_tooltip_text, hover_delay=80) - - self.bind("<Double-Button-1>", self.expand) - if macosx.isAquaTk(): - # AquaTk defines <2> as the right button, not <3>. - self.bind("<Button-2>", self.context_menu_event) - else: - self.bind("<Button-3>", self.context_menu_event) - self.selection_handle( # X windows only. - lambda offset, length: s[int(offset):int(offset) + int(length)]) - - self.is_dangerous = None - self.after_idle(self.set_is_dangerous) - - def set_is_dangerous(self): - dangerous_line_len = 50 * self.text.winfo_width() - self.is_dangerous = ( - self.numoflines > 1000 or - len(self.s) > 50000 or - any( - len(line_match.group(0)) >= dangerous_line_len - for line_match in re.finditer(r'[^\n]+', self.s) - ) - ) - - def expand(self, event=None): - """expand event handler - - This inserts the original text in place of the button in the Text - widget, removes the button and updates the Squeezer instance. - - If the original text is dangerously long, i.e. expanding it could - cause a performance degradation, ask the user for confirmation. - """ - if self.is_dangerous is None: - self.set_is_dangerous() - if self.is_dangerous: - confirm = tkMessageBox.askokcancel( - title="Expand huge output?", - message="\n\n".join([ - "The squeezed output is very long: %d lines, %d chars.", - "Expanding it could make IDLE slow or unresponsive.", - "It is recommended to view or copy the output instead.", - "Really expand?" - ]) % (self.numoflines, len(self.s)), - default=tkMessageBox.CANCEL, - parent=self.text) - if not confirm: - return "break" - - self.base_text.insert(self.text.index(self), self.s, self.tags) - self.base_text.delete(self) - self.squeezer.expandingbuttons.remove(self) - - def copy(self, event=None): - """copy event handler - - Copy the original text to the clipboard. - """ - self.clipboard_clear() - self.clipboard_append(self.s) - - def view(self, event=None): - """view event handler - - View the original text in a separate text viewer window. - """ - view_text(self.text, "Squeezed Output Viewer", self.s, - modal=False, wrap='none') - - rmenu_specs = ( - # Item structure: (label, method_name). - ('copy', 'copy'), - ('view', 'view'), - ) - - def context_menu_event(self, event): - self.text.mark_set("insert", "@%d,%d" % (event.x, event.y)) - rmenu = tk.Menu(self.text, tearoff=0) - for label, method_name in self.rmenu_specs: - rmenu.add_command(label=label, command=getattr(self, method_name)) - rmenu.tk_popup(event.x_root, event.y_root) - return "break" - - -class Squeezer: - """Replace long outputs in the shell with a simple button. - - This avoids IDLE's shell slowing down considerably, and even becoming - completely unresponsive, when very long outputs are written. - """ - @classmethod - def reload(cls): - """Load class variables from config.""" - cls.auto_squeeze_min_lines = idleConf.GetOption( - "main", "PyShell", "auto-squeeze-min-lines", - type="int", default=50, - ) - - def __init__(self, editwin): - """Initialize settings for Squeezer. - - editwin is the shell's Editor window. - self.text is the editor window text widget. - self.base_test is the actual editor window Tk text widget, rather than - EditorWindow's wrapper. - self.expandingbuttons is the list of all buttons representing - "squeezed" output. - """ - self.editwin = editwin - self.text = text = editwin.text - - # Get the base Text widget of the PyShell object, used to change - # text before the iomark. PyShell deliberately disables changing - # text before the iomark via its 'text' attribute, which is - # actually a wrapper for the actual Text widget. Squeezer, - # however, needs to make such changes. - self.base_text = editwin.per.bottom - - # Twice the text widget's border width and internal padding; - # pre-calculated here for the get_line_width() method. - self.window_width_delta = 2 * ( - int(text.cget('border')) + - int(text.cget('padx')) - ) - - self.expandingbuttons = [] - - # Replace the PyShell instance's write method with a wrapper, - # which inserts an ExpandingButton instead of a long text. - def mywrite(s, tags=(), write=editwin.write): - # Only auto-squeeze text which has just the "stdout" tag. - if tags != "stdout": - return write(s, tags) - - # Only auto-squeeze text with at least the minimum - # configured number of lines. - auto_squeeze_min_lines = self.auto_squeeze_min_lines - # First, a very quick check to skip very short texts. - if len(s) < auto_squeeze_min_lines: - return write(s, tags) - # Now the full line-count check. - numoflines = self.count_lines(s) - if numoflines < auto_squeeze_min_lines: - return write(s, tags) - - # Create an ExpandingButton instance. - expandingbutton = ExpandingButton(s, tags, numoflines, self) - - # Insert the ExpandingButton into the Text widget. - text.mark_gravity("iomark", tk.RIGHT) - text.window_create("iomark", window=expandingbutton, - padx=3, pady=5) - text.see("iomark") - text.update() - text.mark_gravity("iomark", tk.LEFT) - - # Add the ExpandingButton to the Squeezer's list. - self.expandingbuttons.append(expandingbutton) - - editwin.write = mywrite - - def count_lines(self, s): - """Count the number of lines in a given text. - - Before calculation, the tab width and line length of the text are - fetched, so that up-to-date values are used. - - Lines are counted as if the string was wrapped so that lines are never - over linewidth characters long. - - Tabs are considered tabwidth characters long. - """ - return count_lines_with_wrapping(s, self.editwin.width) - - def squeeze_current_text_event(self, event): - """squeeze-current-text event handler - - Squeeze the block of text inside which contains the "insert" cursor. - - If the insert cursor is not in a squeezable block of text, give the - user a small warning and do nothing. - """ - # Set tag_name to the first valid tag found on the "insert" cursor. - tag_names = self.text.tag_names(tk.INSERT) - for tag_name in ("stdout", "stderr"): - if tag_name in tag_names: - break - else: - # The insert cursor doesn't have a "stdout" or "stderr" tag. - self.text.bell() - return "break" - - # Find the range to squeeze. - start, end = self.text.tag_prevrange(tag_name, tk.INSERT + "+1c") - s = self.text.get(start, end) - - # If the last char is a newline, remove it from the range. - if len(s) > 0 and s[-1] == '\n': - end = self.text.index("%s-1c" % end) - s = s[:-1] - - # Delete the text. - self.base_text.delete(start, end) - - # Prepare an ExpandingButton. - numoflines = self.count_lines(s) - expandingbutton = ExpandingButton(s, tag_name, numoflines, self) - - # insert the ExpandingButton to the Text - self.text.window_create(start, window=expandingbutton, - padx=3, pady=5) - - # Insert the ExpandingButton to the list of ExpandingButtons, - # while keeping the list ordered according to the position of - # the buttons in the Text widget. - i = len(self.expandingbuttons) - while i > 0 and self.text.compare(self.expandingbuttons[i-1], - ">", expandingbutton): - i -= 1 - self.expandingbuttons.insert(i, expandingbutton) - - return "break" - - -Squeezer.reload() - - -if __name__ == "__main__": - from unittest import main - main('idlelib.idle_test.test_squeezer', verbosity=2, exit=False) - - # Add htest. diff --git a/Lib/idlelib/statusbar.py b/Lib/idlelib/statusbar.py deleted file mode 100644 index c071f89..0000000 --- a/Lib/idlelib/statusbar.py +++ /dev/null @@ -1,49 +0,0 @@ -from tkinter import Frame, Label - - -class MultiStatusBar(Frame): - - def __init__(self, master, **kw): - Frame.__init__(self, master, **kw) - self.labels = {} - - def set_label(self, name, text='', side='left', width=0): - if name not in self.labels: - 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 _multistatus_bar(parent): # htest # - from tkinter import Toplevel, Frame, Text, Button - top = Toplevel(parent) - x, y = map(int, parent.geometry().split('+')[1:]) - top.geometry("+%d+%d" %(x, y + 175)) - top.title("Test multistatus bar") - frame = Frame(top) - text = Text(frame, height=5, width=40) - 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(top, text="Update status", command=change) - button.pack(side='bottom') - frame.pack() - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_statusbar', verbosity=2, exit=False) - - from idlelib.idle_test.htest import run - run(_multistatus_bar) diff --git a/Lib/idlelib/tabbedpages.py b/Lib/idlelib/tabbedpages.py new file mode 100644 index 0000000..0723d94 --- /dev/null +++ b/Lib/idlelib/tabbedpages.py @@ -0,0 +1,498 @@ +"""An implementation of tabbed pages using only standard Tkinter. + +Originally developed for use in IDLE. Based on tabpage.py. + +Classes exported: +TabbedPageSet -- A Tkinter implementation of a tabbed-page widget. +TabSet -- A widget containing tabs (buttons) in one or more rows. + +""" +from Tkinter import * + +class InvalidNameError(Exception): pass +class AlreadyExistsError(Exception): pass + + +class TabSet(Frame): + """A widget containing tabs (buttons) in one or more rows. + + Only one tab may be selected at a time. + + """ + def __init__(self, page_set, select_command, + tabs=None, n_rows=1, max_tabs_per_row=5, + expand_tabs=False, **kw): + """Constructor arguments: + + select_command -- A callable which will be called when a tab is + selected. It is called with the name of the selected tab as an + argument. + + tabs -- A list of strings, the names of the tabs. Should be specified in + the desired tab order. The first tab will be the default and first + active tab. If tabs is None or empty, the TabSet will be initialized + empty. + + n_rows -- Number of rows of tabs to be shown. If n_rows <= 0 or is + None, then the number of rows will be decided by TabSet. See + _arrange_tabs() for details. + + max_tabs_per_row -- Used for deciding how many rows of tabs are needed, + when the number of rows is not constant. See _arrange_tabs() for + details. + + """ + Frame.__init__(self, page_set, **kw) + self.select_command = select_command + self.n_rows = n_rows + self.max_tabs_per_row = max_tabs_per_row + self.expand_tabs = expand_tabs + self.page_set = page_set + + self._tabs = {} + self._tab2row = {} + if tabs: + self._tab_names = list(tabs) + else: + self._tab_names = [] + self._selected_tab = None + self._tab_rows = [] + + self.padding_frame = Frame(self, height=2, + borderwidth=0, relief=FLAT, + background=self.cget('background')) + self.padding_frame.pack(side=TOP, fill=X, expand=False) + + self._arrange_tabs() + + def add_tab(self, tab_name): + """Add a new tab with the name given in tab_name.""" + if not tab_name: + raise InvalidNameError("Invalid Tab name: '%s'" % tab_name) + if tab_name in self._tab_names: + raise AlreadyExistsError("Tab named '%s' already exists" %tab_name) + + self._tab_names.append(tab_name) + self._arrange_tabs() + + def remove_tab(self, tab_name): + """Remove the tab named <tab_name>""" + if not tab_name in self._tab_names: + raise KeyError("No such Tab: '%s" % page_name) + + self._tab_names.remove(tab_name) + self._arrange_tabs() + + def set_selected_tab(self, tab_name): + """Show the tab named <tab_name> as the selected one""" + if tab_name == self._selected_tab: + return + if tab_name is not None and tab_name not in self._tabs: + raise KeyError("No such Tab: '%s" % page_name) + + # deselect the current selected tab + if self._selected_tab is not None: + self._tabs[self._selected_tab].set_normal() + self._selected_tab = None + + if tab_name is not None: + # activate the tab named tab_name + self._selected_tab = tab_name + tab = self._tabs[tab_name] + tab.set_selected() + # move the tab row with the selected tab to the bottom + tab_row = self._tab2row[tab] + tab_row.pack_forget() + tab_row.pack(side=TOP, fill=X, expand=0) + + def _add_tab_row(self, tab_names, expand_tabs): + if not tab_names: + return + + tab_row = Frame(self) + tab_row.pack(side=TOP, fill=X, expand=0) + self._tab_rows.append(tab_row) + + for tab_name in tab_names: + tab = TabSet.TabButton(tab_name, self.select_command, + tab_row, self) + if expand_tabs: + tab.pack(side=LEFT, fill=X, expand=True) + else: + tab.pack(side=LEFT) + self._tabs[tab_name] = tab + self._tab2row[tab] = tab_row + + # tab is the last one created in the above loop + tab.is_last_in_row = True + + def _reset_tab_rows(self): + while self._tab_rows: + tab_row = self._tab_rows.pop() + tab_row.destroy() + self._tab2row = {} + + def _arrange_tabs(self): + """ + Arrange the tabs in rows, in the order in which they were added. + + If n_rows >= 1, this will be the number of rows used. Otherwise the + number of rows will be calculated according to the number of tabs and + max_tabs_per_row. In this case, the number of rows may change when + adding/removing tabs. + + """ + # remove all tabs and rows + for tab_name in self._tabs.keys(): + self._tabs.pop(tab_name).destroy() + self._reset_tab_rows() + + if not self._tab_names: + return + + if self.n_rows is not None and self.n_rows > 0: + n_rows = self.n_rows + else: + # calculate the required number of rows + n_rows = (len(self._tab_names) - 1) // self.max_tabs_per_row + 1 + + # not expanding the tabs with more than one row is very ugly + expand_tabs = self.expand_tabs or n_rows > 1 + i = 0 # index in self._tab_names + for row_index in xrange(n_rows): + # calculate required number of tabs in this row + n_tabs = (len(self._tab_names) - i - 1) // (n_rows - row_index) + 1 + tab_names = self._tab_names[i:i + n_tabs] + i += n_tabs + self._add_tab_row(tab_names, expand_tabs) + + # re-select selected tab so it is properly displayed + selected = self._selected_tab + self.set_selected_tab(None) + if selected in self._tab_names: + self.set_selected_tab(selected) + + class TabButton(Frame): + """A simple tab-like widget.""" + + bw = 2 # borderwidth + + def __init__(self, name, select_command, tab_row, tab_set): + """Constructor arguments: + + name -- The tab's name, which will appear in its button. + + select_command -- The command to be called upon selection of the + tab. It is called with the tab's name as an argument. + + """ + Frame.__init__(self, tab_row, borderwidth=self.bw, relief=RAISED) + + self.name = name + self.select_command = select_command + self.tab_set = tab_set + self.is_last_in_row = False + + self.button = Radiobutton( + self, text=name, command=self._select_event, + padx=5, pady=1, takefocus=FALSE, indicatoron=FALSE, + highlightthickness=0, selectcolor='', borderwidth=0) + self.button.pack(side=LEFT, fill=X, expand=True) + + self._init_masks() + self.set_normal() + + def _select_event(self, *args): + """Event handler for tab selection. + + With TabbedPageSet, this calls TabbedPageSet.change_page, so that + selecting a tab changes the page. + + Note that this does -not- call set_selected -- it will be called by + TabSet.set_selected_tab, which should be called when whatever the + tabs are related to changes. + + """ + self.select_command(self.name) + return + + def set_selected(self): + """Assume selected look""" + self._place_masks(selected=True) + + def set_normal(self): + """Assume normal look""" + self._place_masks(selected=False) + + def _init_masks(self): + page_set = self.tab_set.page_set + background = page_set.pages_frame.cget('background') + # mask replaces the middle of the border with the background color + self.mask = Frame(page_set, borderwidth=0, relief=FLAT, + background=background) + # mskl replaces the bottom-left corner of the border with a normal + # left border + self.mskl = Frame(page_set, borderwidth=0, relief=FLAT, + background=background) + self.mskl.ml = Frame(self.mskl, borderwidth=self.bw, + relief=RAISED) + self.mskl.ml.place(x=0, y=-self.bw, + width=2*self.bw, height=self.bw*4) + # mskr replaces the bottom-right corner of the border with a normal + # right border + self.mskr = Frame(page_set, borderwidth=0, relief=FLAT, + background=background) + self.mskr.mr = Frame(self.mskr, borderwidth=self.bw, + relief=RAISED) + + def _place_masks(self, selected=False): + height = self.bw + if selected: + height += self.bw + + self.mask.place(in_=self, + relx=0.0, x=0, + rely=1.0, y=0, + relwidth=1.0, width=0, + relheight=0.0, height=height) + + self.mskl.place(in_=self, + relx=0.0, x=-self.bw, + rely=1.0, y=0, + relwidth=0.0, width=self.bw, + relheight=0.0, height=height) + + page_set = self.tab_set.page_set + if selected and ((not self.is_last_in_row) or + (self.winfo_rootx() + self.winfo_width() < + page_set.winfo_rootx() + page_set.winfo_width()) + ): + # for a selected tab, if its rightmost edge isn't on the + # rightmost edge of the page set, the right mask should be one + # borderwidth shorter (vertically) + height -= self.bw + + self.mskr.place(in_=self, + relx=1.0, x=0, + rely=1.0, y=0, + relwidth=0.0, width=self.bw, + relheight=0.0, height=height) + + self.mskr.mr.place(x=-self.bw, y=-self.bw, + width=2*self.bw, height=height + self.bw*2) + + # finally, lower the tab set so that all of the frames we just + # placed hide it + self.tab_set.lower() + +class TabbedPageSet(Frame): + """A Tkinter tabbed-pane widget. + + Constains set of 'pages' (or 'panes') with tabs above for selecting which + page is displayed. Only one page will be displayed at a time. + + Pages may be accessed through the 'pages' attribute, which is a dictionary + of pages, using the name given as the key. A page is an instance of a + subclass of Tk's Frame widget. + + The page widgets will be created (and destroyed when required) by the + TabbedPageSet. Do not call the page's pack/place/grid/destroy methods. + + Pages may be added or removed at any time using the add_page() and + remove_page() methods. + + """ + class Page(object): + """Abstract base class for TabbedPageSet's pages. + + Subclasses must override the _show() and _hide() methods. + + """ + uses_grid = False + + def __init__(self, page_set): + self.frame = Frame(page_set, borderwidth=2, relief=RAISED) + + def _show(self): + raise NotImplementedError + + def _hide(self): + raise NotImplementedError + + class PageRemove(Page): + """Page class using the grid placement manager's "remove" mechanism.""" + uses_grid = True + + def _show(self): + self.frame.grid(row=0, column=0, sticky=NSEW) + + def _hide(self): + self.frame.grid_remove() + + class PageLift(Page): + """Page class using the grid placement manager's "lift" mechanism.""" + uses_grid = True + + def __init__(self, page_set): + super(TabbedPageSet.PageLift, self).__init__(page_set) + self.frame.grid(row=0, column=0, sticky=NSEW) + self.frame.lower() + + def _show(self): + self.frame.lift() + + def _hide(self): + self.frame.lower() + + class PagePackForget(Page): + """Page class using the pack placement manager's "forget" mechanism.""" + def _show(self): + self.frame.pack(fill=BOTH, expand=True) + + def _hide(self): + self.frame.pack_forget() + + def __init__(self, parent, page_names=None, page_class=PageLift, + n_rows=1, max_tabs_per_row=5, expand_tabs=False, + **kw): + """Constructor arguments: + + page_names -- A list of strings, each will be the dictionary key to a + page's widget, and the name displayed on the page's tab. Should be + specified in the desired page order. The first page will be the default + and first active page. If page_names is None or empty, the + TabbedPageSet will be initialized empty. + + n_rows, max_tabs_per_row -- Parameters for the TabSet which will + manage the tabs. See TabSet's docs for details. + + page_class -- Pages can be shown/hidden using three mechanisms: + + * PageLift - All pages will be rendered one on top of the other. When + a page is selected, it will be brought to the top, thus hiding all + other pages. Using this method, the TabbedPageSet will not be resized + when pages are switched. (It may still be resized when pages are + added/removed.) + + * PageRemove - When a page is selected, the currently showing page is + hidden, and the new page shown in its place. Using this method, the + TabbedPageSet may resize when pages are changed. + + * PagePackForget - This mechanism uses the pack placement manager. + When a page is shown it is packed, and when it is hidden it is + unpacked (i.e. pack_forget). This mechanism may also cause the + TabbedPageSet to resize when the page is changed. + + """ + Frame.__init__(self, parent, **kw) + + self.page_class = page_class + self.pages = {} + self._pages_order = [] + self._current_page = None + self._default_page = None + + self.columnconfigure(0, weight=1) + self.rowconfigure(1, weight=1) + + self.pages_frame = Frame(self) + self.pages_frame.grid(row=1, column=0, sticky=NSEW) + if self.page_class.uses_grid: + self.pages_frame.columnconfigure(0, weight=1) + self.pages_frame.rowconfigure(0, weight=1) + + # the order of the following commands is important + self._tab_set = TabSet(self, self.change_page, n_rows=n_rows, + max_tabs_per_row=max_tabs_per_row, + expand_tabs=expand_tabs) + if page_names: + for name in page_names: + self.add_page(name) + self._tab_set.grid(row=0, column=0, sticky=NSEW) + + self.change_page(self._default_page) + + def add_page(self, page_name): + """Add a new page with the name given in page_name.""" + if not page_name: + raise InvalidNameError("Invalid TabPage name: '%s'" % page_name) + if page_name in self.pages: + raise AlreadyExistsError( + "TabPage named '%s' already exists" % page_name) + + self.pages[page_name] = self.page_class(self.pages_frame) + self._pages_order.append(page_name) + self._tab_set.add_tab(page_name) + + if len(self.pages) == 1: # adding first page + self._default_page = page_name + self.change_page(page_name) + + def remove_page(self, page_name): + """Destroy the page whose name is given in page_name.""" + if not page_name in self.pages: + raise KeyError("No such TabPage: '%s" % page_name) + + self._pages_order.remove(page_name) + + # handle removing last remaining, default, or currently shown page + if len(self._pages_order) > 0: + if page_name == self._default_page: + # set a new default page + self._default_page = self._pages_order[0] + else: + self._default_page = None + + if page_name == self._current_page: + self.change_page(self._default_page) + + self._tab_set.remove_tab(page_name) + page = self.pages.pop(page_name) + page.frame.destroy() + + def change_page(self, page_name): + """Show the page whose name is given in page_name.""" + if self._current_page == page_name: + return + if page_name is not None and page_name not in self.pages: + raise KeyError("No such TabPage: '%s'" % page_name) + + if self._current_page is not None: + self.pages[self._current_page]._hide() + self._current_page = None + + if page_name is not None: + self._current_page = page_name + self.pages[page_name]._show() + + self._tab_set.set_selected_tab(page_name) + +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, + ) + tabPage.pack(side=TOP, expand=TRUE, fill=BOTH) + Label(tabPage.pages['Foobar'].frame, text='Foo', pady=20).pack() + Label(tabPage.pages['Foobar'].frame, text='Bar', pady=20).pack() + Label(tabPage.pages['Baz'].frame, text='Baz').pack() + entryPgName=Entry(root) + buttonAdd=Button(root, text='Add Page', + command=lambda:tabPage.add_page(entryPgName.get())) + buttonRemove=Button(root, text='Remove Page', + command=lambda:tabPage.remove_page(entryPgName.get())) + labelPgName=Label(root, text='name of page to add/remove:') + buttonAdd.pack(padx=5, pady=5) + buttonRemove.pack(padx=5, pady=5) + 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/textView.py b/Lib/idlelib/textView.py new file mode 100644 index 0000000..ec837f8 --- /dev/null +++ b/Lib/idlelib/textView.py @@ -0,0 +1,97 @@ +"""Simple text browser for IDLE + +""" + +from Tkinter import * +import tkMessageBox + +class TextViewer(Toplevel): + """A simple text viewer dialog for IDLE + + """ + 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) + # 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' + + self.CreateWidgets() + self.title(title) + self.protocol("WM_DELETE_WINDOW", self.Ok) + self.parent = parent + self.textView.focus_set() + #key bindings for this dialog + self.bind('<Return>',self.Ok) #dismiss dialog + self.bind('<Escape>',self.Ok) #dismiss dialog + self.textView.insert(0.0, text) + self.textView.config(state=DISABLED) + + self.is_modal = modal + if self.is_modal: + self.transient(parent) + self.grab_set() + self.wait_window() + + def CreateWidgets(self): + frameText = Frame(self, relief=SUNKEN, height=700) + frameButtons = Frame(self) + self.buttonOk = Button(frameButtons, text='Close', + command=self.Ok, takefocus=FALSE) + self.scrollbarView = Scrollbar(frameText, orient=VERTICAL, + takefocus=FALSE, highlightthickness=0) + self.textView = Text(frameText, wrap=WORD, highlightthickness=0, + fg=self.fg, bg=self.bg) + self.scrollbarView.config(command=self.textView.yview) + self.textView.config(yscrollcommand=self.scrollbarView.set) + self.buttonOk.pack() + self.scrollbarView.pack(side=RIGHT,fill=Y) + self.textView.pack(side=LEFT,expand=TRUE,fill=BOTH) + frameButtons.pack(side=BOTTOM,fill=X) + frameText.pack(side=TOP,expand=TRUE,fill=BOTH) + + def Ok(self, event=None): + if self.is_modal: + self.grab_release() + self.destroy() + + +def view_text(parent, title, text, modal=True): + return TextViewer(parent, title, text, modal) + +def view_file(parent, title, filename, encoding=None, modal=True): + try: + if encoding: + import codecs + textFile = codecs.open(filename, 'r') + else: + textFile = open(filename, 'r') + except IOError: + tkMessageBox.showerror(title='File Load Error', + message='Unable to load file %r .' % filename, + parent=parent) + except UnicodeDecodeError as err: + showerror(title='Unicode Decode Error', + message=str(err), + parent=parent) + else: + return view_text(parent, title, textFile.read(), modal) + + +if __name__ == '__main__': + import unittest + unittest.main('idlelib.idle_test.test_textview', verbosity=2, exit=False) + from idlelib.idle_test.htest import run + run(TextViewer) diff --git a/Lib/idlelib/textview.py b/Lib/idlelib/textview.py deleted file mode 100644 index 808a2ae..0000000 --- a/Lib/idlelib/textview.py +++ /dev/null @@ -1,194 +0,0 @@ -"""Simple text browser for IDLE - -""" -from tkinter import Toplevel, Text, TclError,\ - HORIZONTAL, VERTICAL, NS, EW, NSEW, NONE, WORD, SUNKEN -from tkinter.ttk import Frame, Scrollbar, Button -from tkinter.messagebox import showerror - -from functools import update_wrapper -from idlelib.colorizer import color_config - - -class AutoHideScrollbar(Scrollbar): - """A scrollbar that is automatically hidden when not needed. - - Only the grid geometry manager is supported. - """ - def set(self, lo, hi): - if float(lo) > 0.0 or float(hi) < 1.0: - self.grid() - else: - self.grid_remove() - super().set(lo, hi) - - def pack(self, **kwargs): - raise TclError(f'{self.__class__.__name__} does not support "pack"') - - def place(self, **kwargs): - raise TclError(f'{self.__class__.__name__} does not support "place"') - - -class ScrollableTextFrame(Frame): - """Display text with scrollbar(s).""" - - def __init__(self, master, wrap=NONE, **kwargs): - """Create a frame for Textview. - - master - master widget for this frame - wrap - type of text wrapping to use ('word', 'char' or 'none') - - All parameters except for 'wrap' are passed to Frame.__init__(). - - The Text widget is accessible via the 'text' attribute. - - Note: Changing the wrapping mode of the text widget after - instantiation is not supported. - """ - super().__init__(master, **kwargs) - - text = self.text = Text(self, wrap=wrap) - text.grid(row=0, column=0, sticky=NSEW) - self.grid_rowconfigure(0, weight=1) - self.grid_columnconfigure(0, weight=1) - - # vertical scrollbar - self.yscroll = AutoHideScrollbar(self, orient=VERTICAL, - takefocus=False, - command=text.yview) - self.yscroll.grid(row=0, column=1, sticky=NS) - text['yscrollcommand'] = self.yscroll.set - - # horizontal scrollbar - only when wrap is set to NONE - if wrap == NONE: - self.xscroll = AutoHideScrollbar(self, orient=HORIZONTAL, - takefocus=False, - command=text.xview) - self.xscroll.grid(row=1, column=0, sticky=EW) - text['xscrollcommand'] = self.xscroll.set - else: - self.xscroll = None - - -class ViewFrame(Frame): - "Display TextFrame and Close button." - def __init__(self, parent, contents, wrap='word'): - """Create a frame for viewing text with a "Close" button. - - parent - parent widget for this frame - contents - text to display - wrap - type of text wrapping to use ('word', 'char' or 'none') - - The Text widget is accessible via the 'text' attribute. - """ - super().__init__(parent) - self.parent = parent - self.bind('<Return>', self.ok) - self.bind('<Escape>', self.ok) - self.textframe = ScrollableTextFrame(self, relief=SUNKEN, height=700) - - text = self.text = self.textframe.text - text.insert('1.0', contents) - text.configure(wrap=wrap, highlightthickness=0, state='disabled') - color_config(text) - text.focus_set() - - self.button_ok = button_ok = Button( - self, text='Close', command=self.ok, takefocus=False) - self.textframe.pack(side='top', expand=True, fill='both') - button_ok.pack(side='bottom') - - def ok(self, event=None): - """Dismiss text viewer dialog.""" - self.parent.destroy() - - -class ViewWindow(Toplevel): - "A simple text viewer dialog for IDLE." - - def __init__(self, parent, title, contents, modal=True, wrap=WORD, - *, _htest=False, _utest=False): - """Show the given text in a scrollable window with a 'close' button. - - If modal is left True, users cannot interact with other windows - until the textview window is closed. - - parent - parent of this dialog - title - string which is title of popup dialog - contents - text to display in dialog - wrap - type of text wrapping to use ('word', 'char' or 'none') - _htest - bool; change box location when running htest. - _utest - bool; don't wait_window when running unittest. - """ - super().__init__(parent) - self['borderwidth'] = 5 - # Place dialog below parent if running htest. - x = parent.winfo_rootx() + 10 - y = parent.winfo_rooty() + (10 if not _htest else 100) - self.geometry(f'=750x500+{x}+{y}') - - self.title(title) - self.viewframe = ViewFrame(self, contents, wrap=wrap) - self.protocol("WM_DELETE_WINDOW", self.ok) - self.button_ok = button_ok = Button(self, text='Close', - command=self.ok, takefocus=False) - self.viewframe.pack(side='top', expand=True, fill='both') - - self.is_modal = modal - if self.is_modal: - self.transient(parent) - self.grab_set() - if not _utest: - self.wait_window() - - def ok(self, event=None): - """Dismiss text viewer dialog.""" - if self.is_modal: - self.grab_release() - self.destroy() - - -def view_text(parent, title, contents, modal=True, wrap='word', _utest=False): - """Create text viewer for given text. - - parent - parent of this dialog - title - string which is the title of popup dialog - contents - text to display in this dialog - wrap - type of text wrapping to use ('word', 'char' or 'none') - modal - controls if users can interact with other windows while this - dialog is displayed - _utest - bool; controls wait_window on unittest - """ - return ViewWindow(parent, title, contents, modal, wrap=wrap, _utest=_utest) - - -def view_file(parent, title, filename, encoding, modal=True, wrap='word', - _utest=False): - """Create text viewer for text in filename. - - Return error message if file cannot be read. Otherwise calls view_text - with contents of the file. - """ - try: - with open(filename, 'r', encoding=encoding) as file: - contents = file.read() - except OSError: - showerror(title='File Load Error', - message=f'Unable to load file {filename!r} .', - parent=parent) - except UnicodeDecodeError as err: - showerror(title='Unicode Decode Error', - message=str(err), - parent=parent) - else: - return view_text(parent, title, contents, modal, wrap=wrap, - _utest=_utest) - return None - - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_textview', verbosity=2, exit=False) - - from idlelib.idle_test.htest import run - run(ViewWindow) diff --git a/Lib/idlelib/tooltip.py b/Lib/idlelib/tooltip.py deleted file mode 100644 index 6965826..0000000 --- a/Lib/idlelib/tooltip.py +++ /dev/null @@ -1,186 +0,0 @@ -"""Tools for displaying tool-tips. - -This includes: - * an abstract base-class for different kinds of tooltips - * a simple text-only Tooltip class -""" -from tkinter import * - - -class TooltipBase(object): - """abstract base class for tooltips""" - - def __init__(self, anchor_widget): - """Create a tooltip. - - anchor_widget: the widget next to which the tooltip will be shown - - Note that a widget will only be shown when showtip() is called. - """ - self.anchor_widget = anchor_widget - self.tipwindow = None - - def __del__(self): - self.hidetip() - - def showtip(self): - """display the tooltip""" - if self.tipwindow: - return - self.tipwindow = tw = Toplevel(self.anchor_widget) - # show no border on the top level window - tw.wm_overrideredirect(1) - try: - # This command is only needed and available on Tk >= 8.4.0 for OSX. - # Without it, call tips intrude on the typing process by grabbing - # the focus. - tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w, - "help", "noActivates") - except TclError: - pass - - self.position_window() - self.showcontents() - self.tipwindow.update_idletasks() # Needed on MacOS -- see #34275. - self.tipwindow.lift() # work around bug in Tk 8.5.18+ (issue #24570) - - def position_window(self): - """(re)-set the tooltip's screen position""" - x, y = self.get_position() - root_x = self.anchor_widget.winfo_rootx() + x - root_y = self.anchor_widget.winfo_rooty() + y - self.tipwindow.wm_geometry("+%d+%d" % (root_x, root_y)) - - def get_position(self): - """choose a screen position for the tooltip""" - # The tip window must be completely outside the anchor widget; - # otherwise when the mouse enters the tip window we get - # a leave event and it disappears, and then we get an enter - # event and it reappears, and so on forever :-( - # - # Note: This is a simplistic implementation; sub-classes will likely - # want to override this. - return 20, self.anchor_widget.winfo_height() + 1 - - def showcontents(self): - """content display hook for sub-classes""" - # See ToolTip for an example - raise NotImplementedError - - def hidetip(self): - """hide the tooltip""" - # Note: This is called by __del__, so careful when overriding/extending - tw = self.tipwindow - self.tipwindow = None - if tw: - try: - tw.destroy() - except TclError: # pragma: no cover - pass - - -class OnHoverTooltipBase(TooltipBase): - """abstract base class for tooltips, with delayed on-hover display""" - - def __init__(self, anchor_widget, hover_delay=1000): - """Create a tooltip with a mouse hover delay. - - anchor_widget: the widget next to which the tooltip will be shown - hover_delay: time to delay before showing the tooltip, in milliseconds - - Note that a widget will only be shown when showtip() is called, - e.g. after hovering over the anchor widget with the mouse for enough - time. - """ - super(OnHoverTooltipBase, self).__init__(anchor_widget) - self.hover_delay = hover_delay - - self._after_id = None - self._id1 = self.anchor_widget.bind("<Enter>", self._show_event) - self._id2 = self.anchor_widget.bind("<Leave>", self._hide_event) - self._id3 = self.anchor_widget.bind("<Button>", self._hide_event) - - def __del__(self): - try: - self.anchor_widget.unbind("<Enter>", self._id1) - self.anchor_widget.unbind("<Leave>", self._id2) # pragma: no cover - self.anchor_widget.unbind("<Button>", self._id3) # pragma: no cover - except TclError: - pass - super(OnHoverTooltipBase, self).__del__() - - def _show_event(self, event=None): - """event handler to display the tooltip""" - if self.hover_delay: - self.schedule() - else: - self.showtip() - - def _hide_event(self, event=None): - """event handler to hide the tooltip""" - self.hidetip() - - def schedule(self): - """schedule the future display of the tooltip""" - self.unschedule() - self._after_id = self.anchor_widget.after(self.hover_delay, - self.showtip) - - def unschedule(self): - """cancel the future display of the tooltip""" - after_id = self._after_id - self._after_id = None - if after_id: - self.anchor_widget.after_cancel(after_id) - - def hidetip(self): - """hide the tooltip""" - try: - self.unschedule() - except TclError: # pragma: no cover - pass - super(OnHoverTooltipBase, self).hidetip() - - -class Hovertip(OnHoverTooltipBase): - "A tooltip that pops up when a mouse hovers over an anchor widget." - def __init__(self, anchor_widget, text, hover_delay=1000): - """Create a text tooltip with a mouse hover delay. - - anchor_widget: the widget next to which the tooltip will be shown - hover_delay: time to delay before showing the tooltip, in milliseconds - - Note that a widget will only be shown when showtip() is called, - e.g. after hovering over the anchor widget with the mouse for enough - time. - """ - super(Hovertip, self).__init__(anchor_widget, hover_delay=hover_delay) - self.text = text - - def showcontents(self): - label = Label(self.tipwindow, text=self.text, justify=LEFT, - background="#ffffe0", relief=SOLID, borderwidth=1) - label.pack() - - -def _tooltip(parent): # htest # - top = Toplevel(parent) - top.title("Test tooltip") - x, y = map(int, parent.geometry().split('+')[1:]) - top.geometry("+%d+%d" % (x, y + 150)) - label = Label(top, text="Place your mouse over buttons") - label.pack() - button1 = Button(top, text="Button 1 -- 1/2 second hover delay") - button1.pack() - Hovertip(button1, "This is tooltip text for button1.", hover_delay=500) - button2 = Button(top, text="Button 2 -- no hover delay") - button2.pack() - Hovertip(button2, "This is tooltip\ntext for button2.", hover_delay=None) - - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_tooltip', verbosity=2, exit=False) - - from idlelib.idle_test.htest import run - run(_tooltip) diff --git a/Lib/idlelib/zoomheight.py b/Lib/idlelib/zoomheight.py deleted file mode 100644 index cd50c91..0000000 --- a/Lib/idlelib/zoomheight.py +++ /dev/null @@ -1,124 +0,0 @@ -"Zoom a window to maximum height." - -import re -import sys -import tkinter - - -class WmInfoGatheringError(Exception): - pass - - -class ZoomHeight: - # Cached values for maximized window dimensions, one for each set - # of screen dimensions. - _max_height_and_y_coords = {} - - def __init__(self, editwin): - self.editwin = editwin - self.top = self.editwin.top - - def zoom_height_event(self, event=None): - zoomed = self.zoom_height() - - if zoomed is None: - self.top.bell() - else: - menu_status = 'Restore' if zoomed else 'Zoom' - self.editwin.update_menu_label(menu='options', index='* Height', - label=f'{menu_status} Height') - - return "break" - - def zoom_height(self): - top = self.top - - width, height, x, y = get_window_geometry(top) - - if top.wm_state() != 'normal': - # Can't zoom/restore window height for windows not in the 'normal' - # state, e.g. maximized and full-screen windows. - return None - - try: - maxheight, maxy = self.get_max_height_and_y_coord() - except WmInfoGatheringError: - return None - - if height != maxheight: - # Maximize the window's height. - set_window_geometry(top, (width, maxheight, x, maxy)) - return True - else: - # Restore the window's height. - # - # .wm_geometry('') makes the window revert to the size requested - # by the widgets it contains. - top.wm_geometry('') - return False - - def get_max_height_and_y_coord(self): - top = self.top - - screen_dimensions = (top.winfo_screenwidth(), - top.winfo_screenheight()) - if screen_dimensions not in self._max_height_and_y_coords: - orig_state = top.wm_state() - - # Get window geometry info for maximized windows. - try: - top.wm_state('zoomed') - except tkinter.TclError: - # The 'zoomed' state is not supported by some esoteric WMs, - # such as Xvfb. - raise WmInfoGatheringError( - 'Failed getting geometry of maximized windows, because ' + - 'the "zoomed" window state is unavailable.') - top.update() - maxwidth, maxheight, maxx, maxy = get_window_geometry(top) - if sys.platform == 'win32': - # On Windows, the returned Y coordinate is the one before - # maximizing, so we use 0 which is correct unless a user puts - # their dock on the top of the screen (very rare). - maxy = 0 - maxrooty = top.winfo_rooty() - - # Get the "root y" coordinate for non-maximized windows with their - # y coordinate set to that of maximized windows. This is needed - # to properly handle different title bar heights for non-maximized - # vs. maximized windows, as seen e.g. in Windows 10. - top.wm_state('normal') - top.update() - orig_geom = get_window_geometry(top) - max_y_geom = orig_geom[:3] + (maxy,) - set_window_geometry(top, max_y_geom) - top.update() - max_y_geom_rooty = top.winfo_rooty() - - # Adjust the maximum window height to account for the different - # title bar heights of non-maximized vs. maximized windows. - maxheight += maxrooty - max_y_geom_rooty - - self._max_height_and_y_coords[screen_dimensions] = maxheight, maxy - - set_window_geometry(top, orig_geom) - top.wm_state(orig_state) - - return self._max_height_and_y_coords[screen_dimensions] - - -def get_window_geometry(top): - geom = top.wm_geometry() - m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom) - return tuple(map(int, m.groups())) - - -def set_window_geometry(top, geometry): - top.wm_geometry("{:d}x{:d}+{:d}+{:d}".format(*geometry)) - - -if __name__ == "__main__": - from unittest import main - main('idlelib.idle_test.test_zoomheight', verbosity=2, exit=False) - - # Add htest? diff --git a/Lib/idlelib/zzdummy.py b/Lib/idlelib/zzdummy.py deleted file mode 100644 index 8084499..0000000 --- a/Lib/idlelib/zzdummy.py +++ /dev/null @@ -1,42 +0,0 @@ -"Example extension, also used for testing." - -from idlelib.config import idleConf - -ztext = idleConf.GetOption('extensions', 'ZzDummy', 'z-text') - - -class ZzDummy: - -## menudefs = [ -## ('format', [ -## ('Z in', '<<z-in>>'), -## ('Z out', '<<z-out>>'), -## ] ) -## ] - - def __init__(self, editwin): - self.text = editwin.text - z_in = False - - @classmethod - def reload(cls): - cls.ztext = idleConf.GetOption('extensions', 'ZzDummy', 'z-text') - - def z_in_event(self, event): - """ - """ - text = self.text - text.undo_block_start() - for line in range(1, text.index('end')): - text.insert('%d.0', ztest) - text.undo_block_stop() - return "break" - - def z_out_event(self, event): pass - -ZzDummy.reload() - -##if __name__ == "__main__": -## import unittest -## unittest.main('idlelib.idle_test.test_zzdummy', -## verbosity=2, exit=False) |