From 0d9220e162f1e5f8caa3d7ebaa54665776d361a1 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Sun, 22 May 2016 19:10:31 -0400 Subject: Issue #24225: Rename many idlelib/*.py and idlelib/idle_test/test_*.py files. --- Lib/idlelib/AutoComplete.py | 233 ---- Lib/idlelib/AutoCompleteWindow.py | 416 ------ Lib/idlelib/AutoExpand.py | 104 -- Lib/idlelib/Bindings.py | 94 -- Lib/idlelib/CallTipWindow.py | 161 --- Lib/idlelib/CallTips.py | 175 --- Lib/idlelib/ClassBrowser.py | 236 ---- Lib/idlelib/CodeContext.py | 176 --- Lib/idlelib/ColorDelegator.py | 256 ---- Lib/idlelib/Debugger.py | 539 -------- Lib/idlelib/Delegator.py | 33 - Lib/idlelib/EditorWindow.py | 1703 ------------------------ Lib/idlelib/FileList.py | 129 -- Lib/idlelib/FormatParagraph.py | 195 --- Lib/idlelib/GrepDialog.py | 158 --- Lib/idlelib/HyperParser.py | 313 ----- Lib/idlelib/IOBinding.py | 565 -------- Lib/idlelib/IdleHistory.py | 104 -- Lib/idlelib/MultiCall.py | 446 ------- Lib/idlelib/MultiStatusBar.py | 47 - Lib/idlelib/ObjectBrowser.py | 143 -- Lib/idlelib/OutputWindow.py | 144 -- Lib/idlelib/ParenMatch.py | 178 --- Lib/idlelib/PathBrowser.py | 108 -- Lib/idlelib/Percolator.py | 105 -- Lib/idlelib/PyParse.py | 617 --------- Lib/idlelib/PyShell.py | 1619 ---------------------- Lib/idlelib/RemoteDebugger.py | 388 ------ Lib/idlelib/RemoteObjectBrowser.py | 36 - Lib/idlelib/ReplaceDialog.py | 241 ---- Lib/idlelib/RstripExtension.py | 33 - Lib/idlelib/ScriptBinding.py | 206 --- Lib/idlelib/ScrolledList.py | 145 -- Lib/idlelib/SearchDialog.py | 97 -- Lib/idlelib/SearchDialogBase.py | 184 --- Lib/idlelib/SearchEngine.py | 233 ---- Lib/idlelib/StackViewer.py | 151 --- Lib/idlelib/ToolTip.py | 97 -- Lib/idlelib/TreeWidget.py | 466 ------- Lib/idlelib/UndoDelegator.py | 368 ----- Lib/idlelib/WidgetRedirector.py | 176 --- Lib/idlelib/WindowList.py | 90 -- Lib/idlelib/ZoomHeight.py | 51 - Lib/idlelib/aboutDialog.py | 149 --- Lib/idlelib/autocomplete.py | 233 ++++ Lib/idlelib/autocomplete_w.py | 416 ++++++ Lib/idlelib/autoexpand.py | 104 ++ Lib/idlelib/browser.py | 236 ++++ Lib/idlelib/calltip_w.py | 161 +++ Lib/idlelib/calltips.py | 175 +++ Lib/idlelib/codecontext.py | 176 +++ Lib/idlelib/colorizer.py | 256 ++++ Lib/idlelib/config.py | 760 +++++++++++ Lib/idlelib/configDialog.py | 1434 -------------------- Lib/idlelib/configHandler.py | 760 ----------- Lib/idlelib/configHelpSourceEdit.py | 170 --- Lib/idlelib/configSectionNameDialog.py | 98 -- Lib/idlelib/config_help.py | 170 +++ Lib/idlelib/config_key.py | 266 ++++ Lib/idlelib/config_sec.py | 98 ++ Lib/idlelib/configdialog.py | 1434 ++++++++++++++++++++ Lib/idlelib/debugger.py | 539 ++++++++ Lib/idlelib/debugger_r.py | 388 ++++++ Lib/idlelib/debugobj.py | 143 ++ Lib/idlelib/debugobj_r.py | 36 + Lib/idlelib/delegator.py | 33 + Lib/idlelib/dynOptionMenuWidget.py | 57 - Lib/idlelib/dynoption.py | 57 + Lib/idlelib/editor.py | 1703 ++++++++++++++++++++++++ Lib/idlelib/filelist.py | 129 ++ Lib/idlelib/grep.py | 158 +++ Lib/idlelib/help_about.py | 149 +++ Lib/idlelib/history.py | 104 ++ Lib/idlelib/hyperparser.py | 313 +++++ Lib/idlelib/idle_test/test_formatparagraph.py | 377 ------ Lib/idlelib/idle_test/test_history.py | 167 +++ Lib/idlelib/idle_test/test_idlehistory.py | 167 --- Lib/idlelib/idle_test/test_io.py | 233 ---- Lib/idlelib/idle_test/test_iomenu.py | 233 ++++ Lib/idlelib/idle_test/test_paragraph.py | 377 ++++++ Lib/idlelib/idle_test/test_redirector.py | 122 ++ Lib/idlelib/idle_test/test_replace.py | 292 ++++ Lib/idlelib/idle_test/test_replacedialog.py | 292 ---- Lib/idlelib/idle_test/test_search.py | 80 ++ Lib/idlelib/idle_test/test_searchbase.py | 165 +++ Lib/idlelib/idle_test/test_searchdialog.py | 80 -- Lib/idlelib/idle_test/test_searchdialogbase.py | 165 --- Lib/idlelib/idle_test/test_undo.py | 134 ++ Lib/idlelib/idle_test/test_undodelegator.py | 134 -- Lib/idlelib/idle_test/test_widgetredir.py | 122 -- Lib/idlelib/iomenu.py | 565 ++++++++ Lib/idlelib/keybindingDialog.py | 266 ---- Lib/idlelib/macosx.py | 239 ++++ Lib/idlelib/macosxSupport.py | 239 ---- Lib/idlelib/mainmenu.py | 94 ++ Lib/idlelib/multicall.py | 446 +++++++ Lib/idlelib/outwin.py | 144 ++ Lib/idlelib/paragraph.py | 195 +++ Lib/idlelib/parenmatch.py | 178 +++ Lib/idlelib/pathbrowser.py | 108 ++ Lib/idlelib/percolator.py | 105 ++ Lib/idlelib/pyparse.py | 617 +++++++++ Lib/idlelib/pyshell.py | 1619 ++++++++++++++++++++++ Lib/idlelib/redirector.py | 176 +++ Lib/idlelib/replace.py | 241 ++++ Lib/idlelib/rstrip.py | 33 + Lib/idlelib/runscript.py | 206 +++ Lib/idlelib/scrolledlist.py | 145 ++ Lib/idlelib/search.py | 97 ++ Lib/idlelib/searchbase.py | 184 +++ Lib/idlelib/searchengine.py | 233 ++++ Lib/idlelib/stackviewer.py | 151 +++ Lib/idlelib/statusbar.py | 47 + Lib/idlelib/textView.py | 86 -- Lib/idlelib/textview.py | 86 ++ Lib/idlelib/tooltip.py | 97 ++ Lib/idlelib/tree.py | 466 +++++++ Lib/idlelib/undo.py | 368 +++++ Lib/idlelib/windows.py | 90 ++ Lib/idlelib/zoomheight.py | 51 + 120 files changed, 16788 insertions(+), 16788 deletions(-) delete mode 100644 Lib/idlelib/AutoComplete.py delete mode 100644 Lib/idlelib/AutoCompleteWindow.py delete mode 100644 Lib/idlelib/AutoExpand.py delete mode 100644 Lib/idlelib/Bindings.py delete mode 100644 Lib/idlelib/CallTipWindow.py delete mode 100644 Lib/idlelib/CallTips.py delete mode 100644 Lib/idlelib/ClassBrowser.py delete mode 100644 Lib/idlelib/CodeContext.py delete mode 100644 Lib/idlelib/ColorDelegator.py delete mode 100644 Lib/idlelib/Debugger.py delete mode 100644 Lib/idlelib/Delegator.py delete mode 100644 Lib/idlelib/EditorWindow.py delete mode 100644 Lib/idlelib/FileList.py delete mode 100644 Lib/idlelib/FormatParagraph.py delete mode 100644 Lib/idlelib/GrepDialog.py delete mode 100644 Lib/idlelib/HyperParser.py delete mode 100644 Lib/idlelib/IOBinding.py delete mode 100644 Lib/idlelib/IdleHistory.py delete mode 100644 Lib/idlelib/MultiCall.py delete mode 100644 Lib/idlelib/MultiStatusBar.py delete mode 100644 Lib/idlelib/ObjectBrowser.py delete mode 100644 Lib/idlelib/OutputWindow.py delete mode 100644 Lib/idlelib/ParenMatch.py delete mode 100644 Lib/idlelib/PathBrowser.py delete mode 100644 Lib/idlelib/Percolator.py delete mode 100644 Lib/idlelib/PyParse.py delete mode 100755 Lib/idlelib/PyShell.py delete mode 100644 Lib/idlelib/RemoteDebugger.py delete mode 100644 Lib/idlelib/RemoteObjectBrowser.py delete mode 100644 Lib/idlelib/ReplaceDialog.py delete mode 100644 Lib/idlelib/RstripExtension.py delete mode 100644 Lib/idlelib/ScriptBinding.py delete mode 100644 Lib/idlelib/ScrolledList.py delete mode 100644 Lib/idlelib/SearchDialog.py delete mode 100644 Lib/idlelib/SearchDialogBase.py delete mode 100644 Lib/idlelib/SearchEngine.py delete mode 100644 Lib/idlelib/StackViewer.py delete mode 100644 Lib/idlelib/ToolTip.py delete mode 100644 Lib/idlelib/TreeWidget.py delete mode 100644 Lib/idlelib/UndoDelegator.py delete mode 100644 Lib/idlelib/WidgetRedirector.py delete mode 100644 Lib/idlelib/WindowList.py delete mode 100644 Lib/idlelib/ZoomHeight.py delete mode 100644 Lib/idlelib/aboutDialog.py create mode 100644 Lib/idlelib/autocomplete.py create mode 100644 Lib/idlelib/autocomplete_w.py create mode 100644 Lib/idlelib/autoexpand.py create mode 100644 Lib/idlelib/browser.py create mode 100644 Lib/idlelib/calltip_w.py create mode 100644 Lib/idlelib/calltips.py create mode 100644 Lib/idlelib/codecontext.py create mode 100644 Lib/idlelib/colorizer.py create mode 100644 Lib/idlelib/config.py delete mode 100644 Lib/idlelib/configDialog.py delete mode 100644 Lib/idlelib/configHandler.py delete mode 100644 Lib/idlelib/configHelpSourceEdit.py delete mode 100644 Lib/idlelib/configSectionNameDialog.py create mode 100644 Lib/idlelib/config_help.py create mode 100644 Lib/idlelib/config_key.py create mode 100644 Lib/idlelib/config_sec.py create mode 100644 Lib/idlelib/configdialog.py create mode 100644 Lib/idlelib/debugger.py create mode 100644 Lib/idlelib/debugger_r.py create mode 100644 Lib/idlelib/debugobj.py create mode 100644 Lib/idlelib/debugobj_r.py create mode 100644 Lib/idlelib/delegator.py delete mode 100644 Lib/idlelib/dynOptionMenuWidget.py create mode 100644 Lib/idlelib/dynoption.py create mode 100644 Lib/idlelib/editor.py create mode 100644 Lib/idlelib/filelist.py create mode 100644 Lib/idlelib/grep.py create mode 100644 Lib/idlelib/help_about.py create mode 100644 Lib/idlelib/history.py create mode 100644 Lib/idlelib/hyperparser.py delete mode 100644 Lib/idlelib/idle_test/test_formatparagraph.py create mode 100644 Lib/idlelib/idle_test/test_history.py delete mode 100644 Lib/idlelib/idle_test/test_idlehistory.py delete mode 100644 Lib/idlelib/idle_test/test_io.py create mode 100644 Lib/idlelib/idle_test/test_iomenu.py create mode 100644 Lib/idlelib/idle_test/test_paragraph.py create mode 100644 Lib/idlelib/idle_test/test_redirector.py create mode 100644 Lib/idlelib/idle_test/test_replace.py delete mode 100644 Lib/idlelib/idle_test/test_replacedialog.py create mode 100644 Lib/idlelib/idle_test/test_search.py create mode 100644 Lib/idlelib/idle_test/test_searchbase.py delete mode 100644 Lib/idlelib/idle_test/test_searchdialog.py delete mode 100644 Lib/idlelib/idle_test/test_searchdialogbase.py create mode 100644 Lib/idlelib/idle_test/test_undo.py delete mode 100644 Lib/idlelib/idle_test/test_undodelegator.py delete mode 100644 Lib/idlelib/idle_test/test_widgetredir.py create mode 100644 Lib/idlelib/iomenu.py delete mode 100644 Lib/idlelib/keybindingDialog.py create mode 100644 Lib/idlelib/macosx.py delete mode 100644 Lib/idlelib/macosxSupport.py create mode 100644 Lib/idlelib/mainmenu.py create mode 100644 Lib/idlelib/multicall.py create mode 100644 Lib/idlelib/outwin.py create mode 100644 Lib/idlelib/paragraph.py create mode 100644 Lib/idlelib/parenmatch.py create mode 100644 Lib/idlelib/pathbrowser.py create mode 100644 Lib/idlelib/percolator.py create mode 100644 Lib/idlelib/pyparse.py create mode 100755 Lib/idlelib/pyshell.py create mode 100644 Lib/idlelib/redirector.py create mode 100644 Lib/idlelib/replace.py create mode 100644 Lib/idlelib/rstrip.py create mode 100644 Lib/idlelib/runscript.py create mode 100644 Lib/idlelib/scrolledlist.py create mode 100644 Lib/idlelib/search.py create mode 100644 Lib/idlelib/searchbase.py create mode 100644 Lib/idlelib/searchengine.py create mode 100644 Lib/idlelib/stackviewer.py create mode 100644 Lib/idlelib/statusbar.py delete mode 100644 Lib/idlelib/textView.py create mode 100644 Lib/idlelib/textview.py create mode 100644 Lib/idlelib/tooltip.py create mode 100644 Lib/idlelib/tree.py create mode 100644 Lib/idlelib/undo.py create mode 100644 Lib/idlelib/windows.py create mode 100644 Lib/idlelib/zoomheight.py diff --git a/Lib/idlelib/AutoComplete.py b/Lib/idlelib/AutoComplete.py deleted file mode 100644 index b9ec539..0000000 --- a/Lib/idlelib/AutoComplete.py +++ /dev/null @@ -1,233 +0,0 @@ -"""AutoComplete.py - An IDLE extension for automatically completing names. - -This extension can complete either attribute names of file names. It can pop -a window with all available names, for the user to select from. -""" -import os -import sys -import string - -from idlelib.configHandler import idleConf - -# This string includes all chars that may be in an identifier -ID_CHARS = string.ascii_letters + string.digits + "_" - -# 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", "<>"), - ]) - ] - - popupwait = idleConf.GetOption("extensions", "AutoComplete", - "popupwait", type="int", default=0) - - def __init__(self, editwin=None): - self.editwin = editwin - 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. - self._delayed_completion_id = None - self._delayed_completion_index = None - - def _make_autocomplete_window(self): - return AutoCompleteWindow.AutoCompleteWindow(self.text) - - def _remove_autocomplete_window(self, event=None): - if self.autocompletewindow: - self.autocompletewindow.hide_window() - self.autocompletewindow = None - - def force_open_completions_event(self, event): - """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): - """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(False, True, True) - if opened: - return "break" - - 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: - return - self.open_completions(*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 - start of completion, won't open completions and return False. - If mode is given, will open a completion list only in this mode. - """ - # Cancel another delayed call, if it exists. - if self._delayed_completion_id is not None: - self.text.after_cancel(self._delayed_completion_id) - self._delayed_completion_id = None - - 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==COMPLETE_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. - self._remove_autocomplete_window() - mode = COMPLETE_FILES - # Find last separator or string start - while i and curline[i-1] not in "'\"" + SEPS: - i -= 1 - comp_start = curline[i:j] - j = i - # Find string start - while i and curline[i-1] not in "'\"": - i -= 1 - comp_what = curline[i:j] - elif hp.is_in_code() and (not mode or mode==COMPLETE_ATTRIBUTES): - self._remove_autocomplete_window() - mode = COMPLETE_ATTRIBUTES - while i and (curline[i-1] in ID_CHARS or ord(curline[i-1]) > 127): - i -= 1 - comp_start = curline[i:j] - 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 - else: - comp_what = "" - else: - return - - if complete and not comp_what and not comp_start: - return - comp_lists = self.fetch_completions(comp_what, mode) - if not comp_lists[0]: - return - self.autocompletewindow = self._make_autocomplete_window() - return not self.autocompletewindow.show_window( - comp_lists, "insert-%dc" % len(comp_start), - complete, mode, userWantsWin) - - def fetch_completions(self, what, mode): - """Return a pair of lists of completions for something. The first list - is a sublist of the second. Both are sorted. - - If there is a Python subprocess, get the comp. list there. Otherwise, - either fetch_completions() 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. - """ - try: - rpcclt = self.editwin.flist.pyshell.interp.rpcclt - except: - rpcclt = None - if rpcclt: - return rpcclt.remotecall("exec", "get_the_completion_list", - (what, mode), {}) - else: - if mode == COMPLETE_ATTRIBUTES: - if what == "": - namespace = __main__.__dict__.copy() - namespace.update(__main__.__builtins__.__dict__) - bigl = eval("dir()", namespace) - bigl.sort() - if "__all__" in bigl: - smalll = sorted(eval("__all__", namespace)) - else: - smalll = [s for s in bigl if s[:1] != '_'] - else: - try: - entity = self.get_entity(what) - bigl = dir(entity) - bigl.sort() - if "__all__" in bigl: - smalll = sorted(entity.__all__) - else: - smalll = [s for s in bigl if s[:1] != '_'] - except: - return [], [] - - elif mode == COMPLETE_FILES: - if what == "": - what = "." - try: - expandedpath = os.path.expanduser(what) - bigl = os.listdir(expandedpath) - bigl.sort() - smalll = [s for s in bigl if s[:1] != '.'] - except OSError: - return [], [] - - if not smalll: - smalll = bigl - return smalll, bigl - - def get_entity(self, name): - """Lookup name in a namespace spanning sys.modules and __main.dict__""" - namespace = sys.modules.copy() - namespace.update(__main__.__dict__) - return eval(name, namespace) - - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_autocomplete', verbosity=2) diff --git a/Lib/idlelib/AutoCompleteWindow.py b/Lib/idlelib/AutoCompleteWindow.py deleted file mode 100644 index 2ee6878..0000000 --- a/Lib/idlelib/AutoCompleteWindow.py +++ /dev/null @@ -1,416 +0,0 @@ -""" -An auto-completion window for IDLE, used by the AutoComplete extension -""" -from tkinter import * -from idlelib.MultiCall import MC_SHIFT -from idlelib.AutoComplete import COMPLETE_FILES, COMPLETE_ATTRIBUTES - -HIDE_VIRTUAL_EVENT_NAME = "<>" -HIDE_SEQUENCES = ("", "") -KEYPRESS_VIRTUAL_EVENT_NAME = "<>" -# We need to bind event beyond so that the function will be called -# before the default specific IDLE function -KEYPRESS_SEQUENCES = ("", "", "", "", - "", "", "", "", - "", "") -KEYRELEASE_VIRTUAL_EVENT_NAME = "<>" -KEYRELEASE_SEQUENCE = "" -LISTUPDATE_SEQUENCE = "" -WINCONFIG_SEQUENCE = "" -DOUBLECLICK_SEQUENCE = "" - -class AutoCompleteWindow: - - def __init__(self, widget): - # The widget (Text) on which we place the AutoCompleteWindow - self.widget = widget - # The widgets we create - self.autocompletewindow = self.listbox = self.scrollbar = None - # The default foreground and background of a selection. Saved because - # they are changed to the regular colors of list items when the - # completion start is not a prefix of the selected completion - self.origselforeground = self.origselbackground = None - # The list of completions - self.completions = None - # A list with more completions, or None - self.morecompletions = None - # 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 - # The index of the start of the completion - self.startindex = None - # The last typed start, used so that when the selection changes, - # the new start will be as close as possible to the last typed one. - self.lasttypedstart = None - # Do we have an indication that the user wants the completion window - # (for example, he clicked the list) - self.userwantswindow = None - # event ids - 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 - - def _change_start(self, newstart): - min_len = min(len(self.start), len(newstart)) - i = 0 - while i < min_len and self.start[i] == newstart[i]: - i += 1 - if i < len(self.start): - self.widget.delete("%s+%dc" % (self.startindex, i), - "%s+%dc" % (self.startindex, len(self.start))) - if i < len(newstart): - self.widget.insert("%s+%dc" % (self.startindex, i), - newstart[i:]) - self.start = newstart - - 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 - one.""" - i = 0; j = len(self.completions) - while j > i: - m = (i + j) // 2 - if self.completions[m] >= s: - j = m - else: - i = m + 1 - return min(i, len(self.completions)-1) - - 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.""" - first = self._binary_search(s) - if self.completions[first][:len(s)] != s: - # There is not even one completion which s is a prefix of. - return s - # Find the end of the range of completions where s is a prefix of. - i = first + 1 - j = len(self.completions) - while j > i: - m = (i + j) // 2 - if self.completions[m][:len(s)] != s: - j = m - else: - i = m + 1 - last = i-1 - - if first == last: # only one possible completion - return self.completions[first] - - # We should return the maximum prefix of first and last - first_comp = self.completions[first] - last_comp = self.completions[last] - min_len = min(len(first_comp), len(last_comp)) - i = len(s) - while i < min_len and first_comp[i] == last_comp[i]: - i += 1 - return first_comp[:i] - - def _selection_changed(self): - """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) - - lts = self.lasttypedstart - selstart = self.completions[cursel] - if self._binary_search(lts) == cursel: - newstart = lts - else: - min_len = min(len(lts), len(selstart)) - i = 0 - while i < min_len and lts[i] == selstart[i]: - i += 1 - newstart = selstart[:i] - self._change_start(newstart) - - if self.completions[cursel][:len(self.start)] == self.start: - # start is a prefix of the selected completion - self.listbox.configure(selectbackground=self.origselbackground, - selectforeground=self.origselforeground) - else: - self.listbox.configure(selectbackground=self.listbox.cget("bg"), - selectforeground=self.listbox.cget("fg")) - # If there are more completions, show them, and call me again. - if self.morecompletions: - self.completions = self.morecompletions - self.morecompletions = None - self.listbox.delete(0, END) - for item in self.completions: - self.listbox.insert(END, item) - self.listbox.select_set(self._binary_search(self.start)) - self._selection_changed() - - 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.""" - # Handle the start we already have - self.completions, self.morecompletions = comp_lists - self.mode = mode - self.startindex = self.widget.index(index) - self.start = self.widget.get(self.startindex, "insert") - if complete: - completed = self._complete_string(self.start) - start = self.start - self._change_start(completed) - i = self._binary_search(completed) - if self.completions[i] == completed and \ - (i == len(self.completions)-1 or - self.completions[i+1][:len(completed)] != completed): - # There is exactly one matching completion - return completed == start - self.userwantswindow = userWantsWin - self.lasttypedstart = self.start - - # Put widgets in place - self.autocompletewindow = acw = Toplevel(self.widget) - # Put it in a position so that it is not seen. - acw.wm_geometry("+10000+10000") - # Make it float - acw.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. - acw.tk.call("::tk::unsupported::MacWindowStyle", "style", acw._w, - "help", "noActivates") - except TclError: - pass - self.scrollbar = scrollbar = Scrollbar(acw, orient=VERTICAL) - self.listbox = listbox = Listbox(acw, yscrollcommand=scrollbar.set, - exportselection=False, bg="white") - for item in self.completions: - listbox.insert(END, item) - self.origselforeground = listbox.cget("selectforeground") - self.origselbackground = listbox.cget("selectbackground") - scrollbar.config(command=listbox.yview) - scrollbar.pack(side=RIGHT, fill=Y) - listbox.pack(side=LEFT, fill=BOTH, expand=True) - acw.lift() # work around bug in Tk 8.5.18+ (issue #24570) - - # Initialize the listbox selection - self.listbox.select_set(self._binary_search(self.start)) - self._selection_changed() - - # bind events - 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: - self.widget.event_add(KEYPRESS_VIRTUAL_EVENT_NAME, seq) - self.keyreleaseid = self.widget.bind(KEYRELEASE_VIRTUAL_EVENT_NAME, - self.keyrelease_event) - self.widget.event_add(KEYRELEASE_VIRTUAL_EVENT_NAME,KEYRELEASE_SEQUENCE) - self.listupdateid = listbox.bind(LISTUPDATE_SEQUENCE, - self.listselect_event) - self.winconfigid = acw.bind(WINCONFIG_SEQUENCE, self.winconfig_event) - self.doubleclickid = listbox.bind(DOUBLECLICK_SEQUENCE, - self.doubleclick_event) - - def winconfig_event(self, event): - if not self.is_active(): - return - # Position the completion list window - text = self.widget - text.see(self.startindex) - x, y, cx, cy = text.bbox(self.startindex) - acw = self.autocompletewindow - 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)) - new_y = text.winfo_rooty() + y - if (text_height - (y + cy) >= acw_height # enough height below - or y < acw_height): # not enough height above - # place acw below current line - new_y += cy - else: - # place acw above current line - new_y -= acw_height - acw.wm_geometry("+%d+%d" % (new_x, new_y)) - - def hide_event(self, event): - if not self.is_active(): - return - self.hide_window() - - def listselect_event(self, event): - 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 - cursel = int(self.listbox.curselection()[0]) - self._change_start(self.completions[cursel]) - self.hide_window() - - def keypress_event(self, event): - if not self.is_active(): - return - keysym = event.keysym - if hasattr(event, "mc_state"): - state = event.mc_state - else: - state = 0 - if keysym != "Tab": - self.lastkey_was_tab = False - if (len(keysym) == 1 or keysym in ("underscore", "BackSpace") - or (self.mode == COMPLETE_FILES and keysym in - ("period", "minus"))) \ - and not (state & ~MC_SHIFT): - # Normal editing of text - if len(keysym) == 1: - self._change_start(self.start + keysym) - elif keysym == "underscore": - self._change_start(self.start + '_') - elif keysym == "period": - self._change_start(self.start + '.') - elif keysym == "minus": - self._change_start(self.start + '-') - else: - # keysym == "BackSpace" - if len(self.start) == 0: - self.hide_window() - return - self._change_start(self.start[:-1]) - 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" - - elif keysym == "Return": - self.hide_window() - return - - elif (self.mode == COMPLETE_ATTRIBUTES and keysym in - ("period", "space", "parenleft", "parenright", "bracketleft", - "bracketright")) or \ - (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 - # completing file names, put the whole - # selected completion. Anyway, close the list. - cursel = int(self.listbox.curselection()[0]) - if self.completions[cursel][:len(self.start)] == self.start \ - and (self.mode == COMPLETE_ATTRIBUTES or self.start): - self._change_start(self.completions[cursel]) - self.hide_window() - return - - elif keysym in ("Home", "End", "Prior", "Next", "Up", "Down") and \ - not state: - # Move the selection in the listbox - self.userwantswindow = True - cursel = int(self.listbox.curselection()[0]) - if keysym == "Home": - newsel = 0 - elif keysym == "End": - newsel = len(self.completions)-1 - elif keysym in ("Prior", "Next"): - jump = self.listbox.nearest(self.listbox.winfo_height()) - \ - self.listbox.nearest(0) - if keysym == "Prior": - newsel = max(0, cursel-jump) - else: - assert keysym == "Next" - newsel = min(len(self.completions)-1, cursel+jump) - elif keysym == "Up": - newsel = max(0, cursel-1) - else: - assert keysym == "Down" - newsel = min(len(self.completions)-1, cursel+1) - self.listbox.select_clear(cursel) - self.listbox.select_set(newsel) - self._selection_changed() - self._change_start(self.completions[newsel]) - return "break" - - elif (keysym == "Tab" and not state): - if self.lastkey_was_tab: - # two tabs in a row; insert current selection and close acw - cursel = int(self.listbox.curselection()[0]) - self._change_start(self.completions[cursel]) - self.hide_window() - return "break" - else: - # first tab; let AutoComplete handle the completion - self.userwantswindow = True - self.lastkey_was_tab = True - return - - elif any(s in keysym for s in ("Shift", "Control", "Alt", - "Meta", "Command", "Option")): - # A modifier key, so ignore - return - - 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" - - else: - # Unknown event, close the window and let it through. - self.hide_window() - return - - def keyrelease_event(self, event): - if not self.is_active(): - return - if self.widget.index("insert") != \ - self.widget.index("%s+%dc" % (self.startindex, len(self.start))): - # If we didn't catch an event which moved the insert, close window - self.hide_window() - - def is_active(self): - return self.autocompletewindow is not None - - def complete(self): - self._change_start(self._complete_string(self.start)) - # The selection doesn't change. - - def hide_window(self): - if not self.is_active(): - return - - # unbind events - 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 - for seq in KEYPRESS_SEQUENCES: - self.widget.event_delete(KEYPRESS_VIRTUAL_EVENT_NAME, seq) - self.widget.unbind(KEYPRESS_VIRTUAL_EVENT_NAME, self.keypressid) - self.keypressid = None - self.widget.event_delete(KEYRELEASE_VIRTUAL_EVENT_NAME, - KEYRELEASE_SEQUENCE) - self.widget.unbind(KEYRELEASE_VIRTUAL_EVENT_NAME, self.keyreleaseid) - self.keyreleaseid = None - self.listbox.unbind(LISTUPDATE_SEQUENCE, self.listupdateid) - self.listupdateid = None - self.autocompletewindow.unbind(WINCONFIG_SEQUENCE, self.winconfigid) - self.winconfigid = None - - # destroy widgets - self.scrollbar.destroy() - self.scrollbar = None - self.listbox.destroy() - self.listbox = None - self.autocompletewindow.destroy() - self.autocompletewindow = None diff --git a/Lib/idlelib/AutoExpand.py b/Lib/idlelib/AutoExpand.py deleted file mode 100644 index 7059054..0000000 --- a/Lib/idlelib/AutoExpand.py +++ /dev/null @@ -1,104 +0,0 @@ -'''Complete the current word before the cursor with words in the editor. - -Each menu selection or shortcut key selection replaces the word with a -different word with the same prefix. The search for matches begins -before the target and moves toward the top of the editor. It then starts -after the cursor and moves down. It then returns to the original word and -the cycle starts again. - -Changing the current text line or leaving the cursor in a different -place before requesting the next selection causes AutoExpand to reset -its state. - -This is an extension file and there is only one instance of AutoExpand. -''' -import string -import re - -###$ event <> -###$ win -###$ unix - -class AutoExpand: - - menudefs = [ - ('edit', [ - ('E_xpand Word', '<>'), - ]), - ] - - wordchars = string.ascii_letters + string.digits + "_" - - def __init__(self, editwin): - self.text = editwin.text - self.state = None - - def expand_word_event(self, event): - "Replace the current word with the next expansion." - curinsert = self.text.index("insert") - curline = self.text.get("insert linestart", "insert lineend") - if not self.state: - words = self.getwords() - index = 0 - else: - words, index, insert, line = self.state - if insert != curinsert or line != curline: - words = self.getwords() - index = 0 - if not words: - 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.text.bell() # Warn we cycled around - self.text.insert("insert", newword) - curinsert = self.text.index("insert") - curline = self.text.get("insert linestart", "insert lineend") - self.state = words, index, curinsert, curline - return "break" - - def getwords(self): - "Return a list of words that match the prefix before the cursor." - word = self.getprevword() - if not word: - return [] - before = self.text.get("1.0", "insert wordstart") - wbefore = re.findall(r"\b" + word + r"\w+\b", before) - del before - after = self.text.get("insert wordend", "end") - wafter = re.findall(r"\b" + word + r"\w+\b", after) - del after - if not wbefore and not wafter: - return [] - words = [] - dict = {} - # search backwards through words before - wbefore.reverse() - for w in wbefore: - if dict.get(w): - continue - words.append(w) - dict[w] = w - # search onwards through words after - for w in wafter: - if dict.get(w): - continue - words.append(w) - dict[w] = w - words.append(word) - return words - - def getprevword(self): - "Return the word prefix before the cursor." - line = self.text.get("insert linestart", "insert") - i = len(line) - while i > 0 and line[i-1] in self.wordchars: - i = i-1 - return line[i:] - -if __name__ == '__main__': - import unittest - unittest.main('idlelib.idle_test.test_autoexpand', verbosity=2) diff --git a/Lib/idlelib/Bindings.py b/Lib/idlelib/Bindings.py deleted file mode 100644 index ab25ff1..0000000 --- a/Lib/idlelib/Bindings.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Define the menu contents, hotkeys, and event bindings. - -There is additional configuration information in the EditorWindow class (and -subclasses): the menus are created there based on the menu_specs (class) -variable, and menus not created are silently skipped in the code here. This -makes it possible, for example, to define a Debug menu which is only present in -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 - -# Warning: menudefs is altered in macosxSupport.overrideRootMenu() -# after it is determined that an OS X Aqua Tk is in use, -# which cannot be done until after Tk() is first called. -# Do not alter the 'file', 'options', or 'help' cascades here -# without altering overrideRootMenu() as well. -# TODO: Make this more robust - -menudefs = [ - # underscore prefixes character to underscore - ('file', [ - ('_New File', '<>'), - ('_Open...', '<>'), - ('Open _Module...', '<>'), - ('Class _Browser', '<>'), - ('_Path Browser', '<>'), - None, - ('_Save', '<>'), - ('Save _As...', '<>'), - ('Save Cop_y As...', '<>'), - None, - ('Prin_t Window', '<>'), - None, - ('_Close', '<>'), - ('E_xit', '<>'), - ]), - ('edit', [ - ('_Undo', '<>'), - ('_Redo', '<>'), - None, - ('Cu_t', '<>'), - ('_Copy', '<>'), - ('_Paste', '<>'), - ('Select _All', '<>'), - None, - ('_Find...', '<>'), - ('Find A_gain', '<>'), - ('Find _Selection', '<>'), - ('Find in Files...', '<>'), - ('R_eplace...', '<>'), - ('Go to _Line', '<>'), - ]), -('format', [ - ('_Indent Region', '<>'), - ('_Dedent Region', '<>'), - ('Comment _Out Region', '<>'), - ('U_ncomment Region', '<>'), - ('Tabify Region', '<>'), - ('Untabify Region', '<>'), - ('Toggle Tabs', '<>'), - ('New Indent Width', '<>'), - ]), - ('run', [ - ('Python Shell', '<>'), - ]), - ('shell', [ - ('_View Last Restart', '<>'), - ('_Restart Shell', '<>'), - ]), - ('debug', [ - ('_Go to File/Line', '<>'), - ('!_Debugger', '<>'), - ('_Stack Viewer', '<>'), - ('!_Auto-open Stack Viewer', '<>'), - ]), - ('options', [ - ('Configure _IDLE', '<>'), - None, - ]), - ('help', [ - ('_About IDLE', '<>'), - None, - ('_IDLE Help', '<>'), - ('Python _Docs', '<>'), - ]), -] - -if find_spec('turtledemo'): - menudefs[-1][1].append(('Turtle Demo', '<>')) - -default_keydefs = idleConf.GetCurrentKeySet() diff --git a/Lib/idlelib/CallTipWindow.py b/Lib/idlelib/CallTipWindow.py deleted file mode 100644 index 8e68a76..0000000 --- a/Lib/idlelib/CallTipWindow.py +++ /dev/null @@ -1,161 +0,0 @@ -"""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 = "<>" -HIDE_SEQUENCES = ("", "") -CHECKHIDE_VIRTUAL_EVENT_NAME = "<>" -CHECKHIDE_SEQUENCES = ("", "") -CHECKHIDE_TIME = 100 # miliseconds - -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.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("<>", "(") - text.event_add("<>", ")") - text.bind("<>", calltip_show) - text.bind("<>", 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 deleted file mode 100644 index 81bd5f1..0000000 --- a/Lib/idlelib/CallTips.py +++ /dev/null @@ -1,175 +0,0 @@ -"""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 inspect -import re -import sys -import textwrap -import types - -from idlelib import CallTipWindow -from idlelib.HyperParser import HyperParser - -class CallTips: - - menudefs = [ - ('edit', [ - ("Show call tip", "<>"), - ]) - ] - - 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 CallTipWindow.CallTip(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) - - 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.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: - 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.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 - -# 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('(?<=\()\w*\,?\s*') -_default_callable_argspec = "See source or doc" - - -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 = "" - try: - ob_call = ob.__call__ - except BaseException: - return argspec - if isinstance(ob, type): - fob = ob.__init__ - elif isinstance(ob_call, types.MethodType): - fob = ob_call - else: - fob = ob - if isinstance(fob, (types.FunctionType, types.MethodType)): - argspec = inspect.formatargspec(*inspect.getfullargspec(fob)) - if (isinstance(ob, (type, types.MethodType)) or - isinstance(ob_call, types.MethodType)): - argspec = _first_param.sub("", 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_calltips', verbosity=2) diff --git a/Lib/idlelib/ClassBrowser.py b/Lib/idlelib/ClassBrowser.py deleted file mode 100644 index d09c52f..0000000 --- a/Lib/idlelib/ClassBrowser.py +++ /dev/null @@ -1,236 +0,0 @@ -"""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("", 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 deleted file mode 100644 index 7d25ada..0000000 --- a/Lib/idlelib/CodeContext.py +++ /dev/null @@ -1,176 +0,0 @@ -"""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 tkinter.constants import TOP, LEFT, X, W, SUNKEN -import re -from sys import maxsize 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', '<>')])] - 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('<>', 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 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 vertical padding - padx = 0 - for widget in widgets: - padx += widget.tk.getint(widget.pack_info()['padx']) - padx += widget.tk.getint(widget.cget('padx')) - # Calculate the required border width - border = 0 - for widget in widgets: - border += widget.tk.getint(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 range(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/ColorDelegator.py b/Lib/idlelib/ColorDelegator.py deleted file mode 100644 index 9f31349..0000000 --- a/Lib/idlelib/ColorDelegator.py +++ /dev/null @@ -1,256 +0,0 @@ -import time -import re -import keyword -import builtins -from idlelib.Delegator import Delegator -from idlelib.configHandler import idleConf - -DEBUG = False - -def any(name, alternates): - "Return a named group pattern matching list of alternates." - return "(?P<%s>" % name + "|".join(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] - # self.file = open("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"(\br|u|ur|R|U|UR|Ur|uR|b|B|br|Br|bR|BR|rb|rB|Rb|RB)?" - sqstring = stringprefix + r"'[^'\\\n]*(\\.[^'\\\n]*)*'?" - dqstring = stringprefix + r'"[^"\\\n]*(\\.[^"\\\n]*)*"?' - sq3string = stringprefix + r"'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?" - dq3string = stringprefix + r'"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?' - string = any("STRING", [sq3string, dq3string, sqstring, dqstring]) - return kw + "|" + builtin + "|" + comment + "|" + string +\ - "|" + any("SYNC", [r"\n"]) - -prog = re.compile(make_pat(), re.S) -idprog = re.compile(r"\s+(\w+)", re.S) - -class ColorDelegator(Delegator): - - def __init__(self): - Delegator.__init__(self) - self.prog = prog - self.idprog = idprog - self.LoadTagDefs() - - def setdelegate(self, delegate): - if self.delegate is not None: - self.unbind("<>") - Delegator.setdelegate(self, delegate) - if delegate is not None: - self.config_colors() - self.bind("<>", self.toggle_colorize_event) - self.notify_range("1.0", "end") - else: - # No delegate - stop any colorizing - self.stop_colorizing = True - self.allow_colorizing = False - - def config_colors(self): - for tag, cnf in self.tagdefs.items(): - if cnf: - self.tag_configure(tag, **cnf) - self.tag_raise('sel') - - def LoadTagDefs(self): - theme = idleConf.CurrentTheme() - self.tagdefs = { - "COMMENT": idleConf.GetHighlight(theme, "comment"), - "KEYWORD": idleConf.GetHighlight(theme, "keyword"), - "BUILTIN": idleConf.GetHighlight(theme, "builtin"), - "STRING": idleConf.GetHighlight(theme, "string"), - "DEFINITION": idleConf.GetHighlight(theme, "definition"), - "SYNC": {'background':None,'foreground':None}, - "TODO": {'background':None,'foreground':None}, - "ERROR": idleConf.GetHighlight(theme, "error"), - # The following is used by ReplaceDialog: - "hit": idleConf.GetHighlight(theme, "hit"), - } - - if DEBUG: print('tagdefs',self.tagdefs) - - def insert(self, index, chars, tags=None): - index = self.index(index) - self.delegate.insert(index, chars, tags) - self.notify_range(index, index + "+%dc" % len(chars)) - - def delete(self, index1, index2=None): - 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): - self.tag_add("TODO", index1, index2) - if self.after_id: - if DEBUG: print("colorizing already scheduled") - return - if self.colorizing: - self.stop_colorizing = True - if DEBUG: print("stop colorizing") - if self.allow_colorizing: - if DEBUG: print("schedule colorizing") - self.after_id = self.after(1, self.recolorize) - - 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") - 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): - if self.after_id: - after_id = self.after_id - self.after_id = None - if DEBUG: print("cancel scheduled recolorizer") - self.after_cancel(after_id) - if self.allow_colorizing and self.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") - return "break" - - def recolorize(self): - self.after_id = None - if not self.delegate: - if DEBUG: print("no delegate") - return - if not self.allow_colorizing: - if DEBUG: print("auto colorizing is off") - return - if self.colorizing: - if DEBUG: print("already colorizing") - return - try: - self.stop_colorizing = False - self.colorizing = True - if DEBUG: print("colorizing...") - t0 = time.perf_counter() - self.recolorize_main() - t1 = time.perf_counter() - 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") - 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): - next = "1.0" - while True: - item = self.tag_nextrange("TODO", next) - if not item: - break - head, tail = item - self.tag_remove("SYNC", head, tail) - item = self.tag_prevrange("SYNC", head) - if item: - head = item[1] - else: - head = "1.0" - - chars = "" - next = head - lines_to_get = 1 - ok = False - while not ok: - mark = next - next = self.index(mark + "+%d lines linestart" % - lines_to_get) - lines_to_get = min(lines_to_get * 2, 100) - ok = "SYNC" in self.tag_names(next + "-1c") - line = self.get(mark, next) - ##print head, "get", mark, next, "->", repr(line) - if not line: - return - for tag in self.tagdefs: - self.tag_remove(tag, mark, next) - chars = chars + line - m = self.prog.search(chars) - while m: - for key, value in m.groupdict().items(): - if value: - a, b = m.span(key) - self.tag_add(key, - head + "+%dc" % a, - head + "+%dc" % b) - if value in ("def", "class"): - m1 = self.idprog.match(chars, b) - if m1: - a, b = m1.span(1) - self.tag_add("DEFINITION", - head + "+%dc" % a, - head + "+%dc" % b) - m = self.prog.search(chars, m.end()) - if "SYNC" in self.tag_names(next + "-1c"): - head = next - chars = "" - else: - ok = False - if not ok: - # We're in an inconsistent state, and the call to - # update may tell us to stop. It may also change - # the correct value for "next" (since this is a - # line.col string, not a true mark). So leave a - # crumb telling the next invocation to resume here - # in case update tells us to leave. - self.tag_add("TODO", next) - self.update() - if self.stop_colorizing: - if DEBUG: print("colorizing stopped") - return - - def removecolors(self): - for tag in self.tagdefs: - self.tag_remove(tag, "1.0", "end") - -def _color_delegator(parent): # htest # - from tkinter import Toplevel, Text - from idlelib.Percolator import Percolator - - top = Toplevel(parent) - top.title("Test ColorDelegator") - top.geometry("200x100+%d+%d" % (parent.winfo_rootx() + 200, - parent.winfo_rooty() + 150)) - source = "if somename: x = 'abc' # comment\nprint\n" - text = Text(top, background="white") - text.pack(expand=1, fill="both") - text.insert("insert", source) - text.focus_set() - - p = Percolator(text) - d = ColorDelegator() - p.insertfilter(d) - -if __name__ == "__main__": - from idlelib.idle_test.htest import run - run(_color_delegator) diff --git a/Lib/idlelib/Debugger.py b/Lib/idlelib/Debugger.py deleted file mode 100644 index d5e217d..0000000 --- a/Lib/idlelib/Debugger.py +++ /dev/null @@ -1,539 +0,0 @@ -import os -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 - bdb.Bdb.__init__(self) - - def user_line(self, frame): - if self.in_rpc_code(frame): - self.set_step() - return - message = self.__frame2message(frame) - try: - self.gui.interaction(message, frame) - except TclError: # When closing debugger window with [x] in 3.x - pass - - def user_exception(self, frame, info): - if self.in_rpc_code(frame): - self.set_step() - return - message = self.__frame2message(frame) - self.gui.interaction(message, frame, info) - - def in_rpc_code(self, frame): - if frame.f_code.co_filename.count('rpc.py'): - return True - else: - prev_frame = frame.f_back - 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) - - def __frame2message(self, frame): - code = frame.f_code - filename = code.co_filename - lineno = frame.f_lineno - basename = os.path.basename(filename) - message = "%s:%s" % (basename, lineno) - if code.co_name != "?": - message = "%s: %s()" % (message, code.co_name) - return message - - -class Debugger: - - vstack = vsource = vlocals = vglobals = None - - def __init__(self, pyshell, idb=None): - if idb is None: - idb = Idb(self) - self.pyshell = pyshell - self.idb = idb - self.frame = None - self.make_gui() - self.interacting = 0 - self.nesting_level = 0 - - def run(self, *args): - # Deal with the scenario where we've already got a program running - # in the debugger and we want to start another. If that is the case, - # our second 'run' was invoked from an event dispatched not from - # the main event loop, but from the nested event loop in 'interaction' - # below. So our stack looks something like this: - # outer main event loop - # run() - # - # callback to debugger's interaction() - # nested event loop - # run() for second command - # - # This kind of nesting of event loops causes all kinds of problems - # (see e.g. issue #24455) especially when dealing with running as a - # subprocess, where there's all kinds of extra stuff happening in - # there - insert a traceback.print_stack() to check it out. - # - # By this point, we've already called restart_subprocess() in - # ScriptBinding. However, we also need to unwind the stack back to - # that outer event loop. To accomplish this, we: - # - return immediately from the nested run() - # - abort_loop ensures the nested event loop will terminate - # - the debugger's interaction routine completes normally - # - the restart_subprocess() will have taken care of stopping - # the running program, which will also let the outer run complete - # - # That leaves us back at the outer main event loop, at which point our - # after event can fire, and we'll come back to this routine with a - # clean stack. - if self.nesting_level > 0: - self.abort_loop() - self.root.after(100, lambda: self.run(*args)) - return - try: - self.interacting = 1 - return self.idb.run(*args) - finally: - self.interacting = 0 - - def close(self, event=None): - try: - self.quit() - except Exception: - pass - if self.interacting: - self.top.bell() - return - if self.stackviewer: - self.stackviewer.close(); self.stackviewer = None - # Clean up pyshell if user clicked debugger control close widget. - # (Causes a harmless extra cycle through close_debugger() if user - # toggled debugger from pyshell Debug menu) - self.pyshell.close_debugger() - # Now close the debugger control window.... - self.top.destroy() - - def make_gui(self): - pyshell = self.pyshell - self.flist = pyshell.flist - self.root = root = pyshell.root - self.top = top = ListedToplevel(root) - self.top.wm_title("Debug Control") - self.top.wm_iconname("Debug") - top.wm_protocol("WM_DELETE_WINDOW", self.close) - self.top.bind("", self.close) - # - self.bframe = bframe = Frame(top) - self.bframe.pack(anchor="w") - self.buttons = bl = [] - # - self.bcont = b = Button(bframe, text="Go", command=self.cont) - bl.append(b) - self.bstep = b = Button(bframe, text="Step", command=self.step) - bl.append(b) - self.bnext = b = Button(bframe, text="Over", command=self.next) - bl.append(b) - self.bret = b = Button(bframe, text="Out", command=self.ret) - bl.append(b) - self.bret = b = Button(bframe, text="Quit", command=self.quit) - bl.append(b) - # - for b in bl: - b.configure(state="disabled") - b.pack(side="left") - # - self.cframe = cframe = Frame(bframe) - self.cframe.pack(side="left") - # - if not self.vstack: - self.__class__.vstack = BooleanVar(top) - self.vstack.set(1) - self.bstack = Checkbutton(cframe, - text="Stack", command=self.show_stack, variable=self.vstack) - self.bstack.grid(row=0, column=0) - if not self.vsource: - self.__class__.vsource = BooleanVar(top) - self.bsource = Checkbutton(cframe, - text="Source", command=self.show_source, variable=self.vsource) - self.bsource.grid(row=0, column=1) - if not self.vlocals: - self.__class__.vlocals = BooleanVar(top) - self.vlocals.set(1) - self.blocals = Checkbutton(cframe, - text="Locals", command=self.show_locals, variable=self.vlocals) - self.blocals.grid(row=1, column=0) - if not self.vglobals: - self.__class__.vglobals = BooleanVar(top) - self.bglobals = Checkbutton(cframe, - text="Globals", command=self.show_globals, variable=self.vglobals) - self.bglobals.grid(row=1, column=1) - # - self.status = Label(top, anchor="w") - self.status.pack(anchor="w") - self.error = Label(top, anchor="w") - self.error.pack(anchor="w", fill="x") - self.errorbg = self.error.cget("background") - # - self.fstack = Frame(top, height=1) - self.fstack.pack(expand=1, fill="both") - self.flocals = Frame(top) - self.flocals.pack(expand=1, fill="both") - self.fglobals = Frame(top, height=1) - self.fglobals.pack(expand=1, fill="both") - # - if self.vstack.get(): - self.show_stack() - if self.vlocals.get(): - self.show_locals() - if self.vglobals.get(): - self.show_globals() - - def interaction(self, message, frame, info=None): - self.frame = frame - self.status.configure(text=message) - # - if info: - type, value, tb = info - try: - m1 = type.__name__ - except AttributeError: - m1 = "%s" % str(type) - if value is not None: - try: - m1 = "%s: %s" % (m1, str(value)) - except: - pass - bg = "yellow" - else: - m1 = "" - tb = None - bg = self.errorbg - self.error.configure(text=m1, background=bg) - # - sv = self.stackviewer - if sv: - stack, i = self.idb.get_stack(self.frame, tb) - sv.load_stack(stack, i) - # - self.show_variables(1) - # - if self.vsource.get(): - self.sync_source_line() - # - for b in self.buttons: - b.configure(state="normal") - # - self.top.wakeup() - # Nested main loop: Tkinter's main loop is not reentrant, so use - # Tcl's vwait facility, which reenters the event loop until an - # event handler sets the variable we're waiting on - self.nesting_level += 1 - self.root.tk.call('vwait', '::idledebugwait') - self.nesting_level -= 1 - # - for b in self.buttons: - b.configure(state="disabled") - self.status.configure(text="") - self.error.configure(text="", background=self.errorbg) - self.frame = None - - def sync_source_line(self): - frame = self.frame - if not frame: - return - filename, lineno = self.__frame2fileline(frame) - if filename[:1] + filename[-1:] != "<>" and os.path.exists(filename): - self.flist.gotofileline(filename, lineno) - - def __frame2fileline(self, frame): - code = frame.f_code - filename = code.co_filename - lineno = frame.f_lineno - return filename, lineno - - def cont(self): - self.idb.set_continue() - self.abort_loop() - - def step(self): - self.idb.set_step() - self.abort_loop() - - def next(self): - self.idb.set_next(self.frame) - self.abort_loop() - - def ret(self): - self.idb.set_return(self.frame) - self.abort_loop() - - def quit(self): - self.idb.set_quit() - self.abort_loop() - - def abort_loop(self): - self.root.tk.call('set', '::idledebugwait', '1') - - stackviewer = None - - def show_stack(self): - if not self.stackviewer and self.vstack.get(): - self.stackviewer = sv = StackViewer(self.fstack, self.flist, self) - if self.frame: - stack, i = self.idb.get_stack(self.frame, None) - sv.load_stack(stack, i) - else: - sv = self.stackviewer - if sv and not self.vstack.get(): - self.stackviewer = None - sv.close() - self.fstack['height'] = 1 - - def show_source(self): - if self.vsource.get(): - self.sync_source_line() - - def show_frame(self, stackitem): - self.frame = stackitem[0] # lineno is stackitem[1] - self.show_variables() - - localsviewer = None - globalsviewer = None - - def show_locals(self): - lv = self.localsviewer - if self.vlocals.get(): - if not lv: - self.localsviewer = NamespaceViewer(self.flocals, "Locals") - else: - if lv: - self.localsviewer = None - lv.close() - self.flocals['height'] = 1 - self.show_variables() - - def show_globals(self): - gv = self.globalsviewer - if self.vglobals.get(): - if not gv: - self.globalsviewer = NamespaceViewer(self.fglobals, "Globals") - else: - if gv: - self.globalsviewer = None - gv.close() - self.fglobals['height'] = 1 - self.show_variables() - - def show_variables(self, force=0): - lv = self.localsviewer - gv = self.globalsviewer - frame = self.frame - if not frame: - ldict = gdict = None - else: - ldict = frame.f_locals - gdict = frame.f_globals - if lv and gv and ldict is gdict: - ldict = None - if lv: - lv.load_dict(ldict, force, self.pyshell.interp.rpcclt) - if gv: - gv.load_dict(gdict, force, self.pyshell.interp.rpcclt) - - def set_breakpoint_here(self, filename, lineno): - self.idb.set_break(filename, lineno) - - def clear_breakpoint_here(self, filename, lineno): - self.idb.clear_break(filename, lineno) - - def clear_file_breaks(self, filename): - self.idb.clear_all_file_breaks(filename) - - def load_breakpoints(self): - "Load PyShellEditorWindow breakpoints into subprocess debugger" - for editwin in self.pyshell.flist.inversedict: - filename = editwin.io.filename - try: - for lineno in editwin.breakpoints: - self.set_breakpoint_here(filename, lineno) - except AttributeError: - continue - -class StackViewer(ScrolledList): - - def __init__(self, master, flist, gui): - 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. - ScrolledList.__init__(self, master) - else: - ScrolledList.__init__(self, master, width=80) - self.flist = flist - self.gui = gui - self.stack = [] - - def load_stack(self, stack, index=None): - self.stack = stack - self.clear() - for i in range(len(stack)): - frame, lineno = stack[i] - try: - modname = frame.f_globals["__name__"] - except: - modname = "?" - code = frame.f_code - filename = code.co_filename - funcname = code.co_name - import linecache - sourceline = linecache.getline(filename, lineno) - sourceline = sourceline.strip() - if funcname in ("?", "", None): - item = "%s, line %d: %s" % (modname, lineno, sourceline) - else: - item = "%s.%s(), line %d: %s" % (modname, funcname, - lineno, sourceline) - if i == index: - item = "> " + item - self.append(item) - if index is not None: - self.select(index) - - def popup_event(self, event): - "override base method" - if self.stack: - return ScrolledList.popup_event(self, event) - - def fill_menu(self): - "override base method" - menu = self.menu - menu.add_command(label="Go to source line", - command=self.goto_source_line) - menu.add_command(label="Show stack frame", - command=self.show_stack_frame) - - def on_select(self, index): - "override base method" - if 0 <= index < len(self.stack): - self.gui.show_frame(self.stack[index]) - - def on_double(self, index): - "override base method" - self.show_source(index) - - def goto_source_line(self): - index = self.listbox.index("active") - self.show_source(index) - - def show_stack_frame(self): - index = self.listbox.index("active") - if 0 <= index < len(self.stack): - self.gui.show_frame(self.stack[index]) - - def show_source(self, index): - if not (0 <= index < len(self.stack)): - return - frame, lineno = self.stack[index] - code = frame.f_code - filename = code.co_filename - if os.path.isfile(filename): - edit = self.flist.open(filename) - if edit: - edit.gotoline(lineno) - - -class NamespaceViewer: - - def __init__(self, master, title, dict=None): - width = 0 - height = 40 - if dict: - height = 20*len(dict) # XXX 20 == observed height of Entry widget - self.master = master - self.title = title - import reprlib - self.repr = reprlib.Repr() - self.repr.maxstring = 60 - self.repr.maxother = 60 - self.frame = frame = Frame(master) - self.frame.pack(expand=1, fill="both") - self.label = Label(frame, text=title, borderwidth=2, relief="groove") - self.label.pack(fill="x") - self.vbar = vbar = Scrollbar(frame, name="vbar") - vbar.pack(side="right", fill="y") - self.canvas = canvas = Canvas(frame, - height=min(300, max(40, height)), - scrollregion=(0, 0, width, height)) - canvas.pack(side="left", fill="both", expand=1) - vbar["command"] = canvas.yview - canvas["yscrollcommand"] = vbar.set - self.subframe = subframe = Frame(canvas) - self.sfid = canvas.create_window(0, 0, window=subframe, anchor="nw") - self.load_dict(dict) - - dict = -1 - - def load_dict(self, dict, force=0, rpc_client=None): - if dict is self.dict and not force: - return - subframe = self.subframe - frame = self.frame - for c in list(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 RemoteDebugger.DictProxy. - ### - keys_list = dict.keys() - names = sorted(keys_list) - ### - row = 0 - for name in names: - value = dict[name] - svalue = self.repr.repr(value) # repr(value) - # Strip extra quotes caused by calling repr on the (already) - # repr'd value sent across the RPC interface: - if rpc_client: - svalue = svalue[1:-1] - l = Label(subframe, text=name) - l.grid(row=row, column=0, sticky="nw") - l = Entry(subframe, width=0, borderwidth=0) - l.insert(0, svalue) - l.grid(row=row, column=1, sticky="nw") - row = row+1 - self.dict = dict - # XXX Could we use a callback for the following? - subframe.update_idletasks() # Alas! - width = subframe.winfo_reqwidth() - height = subframe.winfo_reqheight() - canvas = self.canvas - self.canvas["scrollregion"] = (0, 0, width, height) - if height > 300: - canvas["height"] = 300 - frame.pack(expand=1) - else: - canvas["height"] = height - frame.pack(expand=0) - - def close(self): - self.frame.destroy() diff --git a/Lib/idlelib/Delegator.py b/Lib/idlelib/Delegator.py deleted file mode 100644 index dc2a1aa..0000000 --- a/Lib/idlelib/Delegator.py +++ /dev/null @@ -1,33 +0,0 @@ -class Delegator: - - 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 - setattr(self, name, attr) - self.__cache.add(name) - return attr - - def resetcache(self): - "Removes added attributes while leaving original attributes." - # Function is really about resetting delagator dict - # to original state. Cache is just a means - for key in self.__cache: - try: - delattr(self, key) - except AttributeError: - pass - 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/EditorWindow.py b/Lib/idlelib/EditorWindow.py deleted file mode 100644 index b5868be..0000000 --- a/Lib/idlelib/EditorWindow.py +++ /dev/null @@ -1,1703 +0,0 @@ -import importlib -import importlib.abc -import importlib.util -import os -import platform -import re -import string -import sys -from tkinter import * -import tkinter.simpledialog as tkSimpleDialog -import tkinter.messagebox as tkMessageBox -import traceback -import webbrowser - -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 - -# The default tab setting for a Text widget, in average-width characters. -TK_TABWIDTH_DEFAULT = 8 - -_py_version = ' (%s)' % platform.python_version() - -def _sphinx_version(): - "Format sys.version_info to produce the Sphinx version string used to install the chm docs" - major, minor, micro, level, serial = sys.version_info - release = '%s%s' % (major, minor) - release += '%s' % (micro,) - if level == 'candidate': - release += 'rc%s' % (serial,) - elif level != 'final': - release += '%s%s' % (level[0], serial) - return release - - -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('', 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.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 - - help_url = None - - def __init__(self, flist=None, filename=None, key=None, root=None): - if EditorWindow.help_url is None: - dochome = os.path.join(sys.base_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] - if os.path.isdir('/var/www/html/python/'): # "python2" rpm - dochome = '/var/www/html/python/index.html' - else: - basepath = '/usr/share/doc/' # standard location - dochome = os.path.join(basepath, pyver, - 'Doc', 'index.html') - elif sys.platform[:3] == 'win': - chmfile = os.path.join(sys.base_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, - 'Resources/English.lproj/Documentation/index.html') - dochome = os.path.normpath(dochome) - if os.path.isfile(dochome): - EditorWindow.help_url = dochome - if sys.platform == 'darwin': - # 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] - 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 = 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 - 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 = os.path.join(idleConf.GetUserCfgDir(), - 'recent-files.lst') - self.text_frame = text_frame = Frame(top) - self.vbar = vbar = Scrollbar(text_frame, name='vbar') - self.width = idleConf.GetOption('main', 'EditorWindow', - 'width', type='int') - text_options = { - 'name': 'text', - 'padx': 5, - 'wrap': 'none', - 'highlightthickness': 0, - '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 - - self.createmenubar() - self.apply_bindings() - - self.top.protocol("WM_DELETE_WINDOW", self.close) - self.top.bind("<>", self.close_event) - if macosxSupport.isAquaTk(): - # Command-W on editorwindows doesn't work without this. - text.bind('<>', self.close_event) - # Some OS X systems have only one mouse button, so use - # control-click for popup context menus there. For two - # buttons, AquaTk defines <2> as the right button, not <3>. - text.bind("",self.right_menu_event) - text.bind("<2>", self.right_menu_event) - else: - # Elsewhere, use right-click for popup menus. - text.bind("<3>",self.right_menu_event) - text.bind("<>", self.cut) - text.bind("<>", self.copy) - text.bind("<>", self.paste) - text.bind("<>", self.center_insert_event) - text.bind("<>", self.help_dialog) - text.bind("<>", self.python_docs) - text.bind("<>", self.about_dialog) - text.bind("<>", self.config_dialog) - text.bind("<>", self.open_module) - text.bind("<>", lambda event: "break") - text.bind("<>", self.select_all) - text.bind("<>", self.remove_selection) - text.bind("<>", self.find_event) - text.bind("<>", self.find_again_event) - text.bind("<>", self.find_in_files_event) - text.bind("<>", self.find_selection_event) - text.bind("<>", self.replace_event) - text.bind("<>", self.goto_line_event) - text.bind("<>",self.smart_backspace_event) - text.bind("<>",self.newline_and_indent_event) - text.bind("<>",self.smart_indent_event) - text.bind("<>",self.indent_region_event) - text.bind("<>",self.dedent_region_event) - text.bind("<>",self.comment_region_event) - text.bind("<>",self.uncomment_region_event) - text.bind("<>",self.tabify_region_event) - text.bind("<>",self.untabify_region_event) - text.bind("<>",self.toggle_tabs_event) - text.bind("<>",self.change_indentwidth_event) - text.bind("", self.move_at_edge_if_selection(0)) - text.bind("", self.move_at_edge_if_selection(1)) - text.bind("<>", self.del_word_left) - text.bind("<>", self.del_word_right) - text.bind("<>", self.home_callback) - - if flist: - flist.inversedict[self] = key - if key: - flist.dict[key] = self - text.bind("<>", self.new_callback) - text.bind("<>", self.flist.close_all_callback) - text.bind("<>", self.open_class_browser) - text.bind("<>", self.open_path_browser) - text.bind("<>", self.open_turtle_demo) - - self.set_status_bar() - vbar['command'] = text.yview - vbar.pack(side=RIGHT, fill=Y) - text['yscrollcommand'] = vbar.set - text['font'] = idleConf.GetFont(self.root, 'main', 'EditorWindow') - text_frame.pack(side=LEFT, fill=BOTH, expand=1) - text.pack(side=TOP, fill=BOTH, expand=1) - text.focus_set() - - # usetabs true -> literal tab characters are used by indent and - # dedent cmds, possibly mixed with spaces if - # indentwidth is not a multiple of tabwidth, - # which will cause Tabnanny to nag! - # false -> tab characters are converted to spaces by indent - # and dedent cmds, and ditto TAB keystrokes - # 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') - self.usetabs = not usespaces - - # tabwidth is the display width of a literal tab character. - # CAUTION: telling Tk to use anything other than its default - # tab setting causes it to use an entirely different tabbing algorithm, - # treating tab stops as fixed distances from the left margin. - # Nobody expects this, so for now tabwidth should never be changed. - self.tabwidth = 8 # must remain 8 until Tk is fixed. - - # indentwidth is the number of screen characters per indent level. - # The recommended Python indentation is four spaces. - self.indentwidth = self.tabwidth - self.set_notabs_indentwidth() - - # 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 - # num_context_lines[1] lines back if that didn't work, and so on. - # The last value should be huge (larger than the # of lines in a - # 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) - self.color = None # initialized below in self.ResetColorizer - 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) - else: - io.set_filename(filename) - self.good_load = True - - self.ResetColorizer() - self.saved_change_hook() - self.update_recent_files_list() - self.load_extensions() - menu = self.menudict.get('windows') - if menu: - end = menu.index("end") - if end is None: - end = -1 - if end >= 0: - menu.add_separator() - end = end + 1 - self.wmenu_end = end - 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 - - def _filename_to_unicode(self, filename): - """Return filename as BMP unicode so diplayable in Tk.""" - # Decode bytes to unicode. - if isinstance(filename, bytes): - try: - filename = filename.decode(self.filesystemencoding) - except UnicodeDecodeError: - try: - filename = filename.decode(self.encoding) - except UnicodeDecodeError: - # byte-to-byte conversion - filename = filename.decode('iso8859-1') - # Replace non-BMP char with diamond questionmark. - return re.sub('[\U00010000-\U0010FFFF]', '\ufffd', filename) - - def new_callback(self, event): - dirname, basename = self.io.defaultfilename() - self.flist.new(dirname) - return "break" - - def home_callback(self, event): - if (event.state & 4) != 0 and event.keysym == "Home": - # state&4==Control. If , use the Tk binding. - return - if self.text.index("iomark") and \ - self.text.compare("iomark", "<=", "insert lineend") and \ - self.text.compare("insert linestart", "<=", "iomark"): - # In Shell on input line, go to just after prompt - insertpt = int(self.text.index("iomark").split(".")[1]) - else: - line = self.text.get("insert linestart", "insert lineend") - for insertpt in range(len(line)): - if line[insertpt] not in (' ','\t'): - break - else: - insertpt=len(line) - lineat = int(self.text.index("insert").split('.')[1]) - if insertpt == lineat: - insertpt = 0 - dest = "insert linestart+"+str(insertpt)+"c" - if (event.state&1) == 0: - # shift was not pressed - 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") - else: - 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 - first = self.text.index(dest) - last = self.text.index("my_anchor") - if self.text.compare(first,">",last): - first,last = last,first - self.text.tag_remove("sel", "1.0", "end") - self.text.tag_add("sel", first, last) - self.text.mark_set("insert", dest) - self.text.see("insert") - return "break" - - def set_status_bar(self): - self.status_bar = self.MultiStatusBar(self.top) - sep = Frame(self.top, height=1, borderwidth=1, background='grey75') - if sys.platform == "darwin": - # Insert some padding to avoid obscuring some of the statusbar - # by the resize widget. - self.status_bar.set_label('_padding1', ' ', side=RIGHT) - self.status_bar.set_label('column', 'Col: ?', side=RIGHT) - self.status_bar.set_label('line', 'Ln: ?', side=RIGHT) - self.status_bar.pack(side=BOTTOM, fill=X) - sep.pack(side=BOTTOM, fill=X) - self.text.bind("<>", self.set_line_and_column) - self.text.event_add("<>", - "", "") - self.text.after_idle(self.set_line_and_column) - - def set_line_and_column(self, event=None): - line, column = self.text.index(INSERT).split('.') - self.status_bar.set_label('column', 'Col: %s' % column) - self.status_bar.set_label('line', 'Ln: %s' % line) - - menu_specs = [ - ("file", "_File"), - ("edit", "_Edit"), - ("format", "F_ormat"), - ("run", "_Run"), - ("options", "_Options"), - ("windows", "_Window"), - ("help", "_Help"), - ] - - - def createmenubar(self): - mbar = self.menubar - self.menudict = menudict = {} - for name, label in self.menu_specs: - underline, label = prepstr(label) - menudict[name] = menu = Menu(mbar, name=name, tearoff=0) - mbar.add_cascade(label=label, menu=menu, underline=underline) - if macosxSupport.isCarbonTk(): - # Insert the application menu - menudict['application'] = menu = Menu(mbar, name='apple', - tearoff=0) - mbar.add_cascade(label='IDLE', menu=menu) - self.fill_menus() - self.recent_files_menu = Menu(self.menubar, 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 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) - WindowList.add_windows_to_menu(menu) - - rmenu = None - - def right_menu_event(self, event): - self.text.mark_set("insert", "@%d,%d" % (event.x, event.y)) - if not self.rmenu: - self.make_rmenu() - rmenu = self.rmenu - self.event = event - iswin = sys.platform[:3] == 'win' - if iswin: - self.text.config(cursor="arrow") - - for item in self.rmenu_specs: - try: - label, eventname, verify_state = item - except ValueError: # see issue1207589 - continue - - if verify_state is None: - continue - 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") - - rmenu_specs = [ - # ("Label", "<>", "statefuncname"), ... - ("Close", "<>", None), # Example - ] - - def make_rmenu(self): - rmenu = Menu(self.text, tearoff=0) - for item in self.rmenu_specs: - label, eventname = item[0], item[1] - if label is not None: - def command(text=self.text, eventname=eventname): - text.event_generate(eventname) - rmenu.add_command(label=label, command=command) - else: - rmenu.add_separator() - self.rmenu = rmenu - - def rmenu_check_cut(self): - return self.rmenu_check_copy() - - def rmenu_check_copy(self): - try: - indx = self.text.index('sel.first') - except TclError: - return 'disabled' - else: - return 'normal' if indx else 'disabled' - - def rmenu_check_paste(self): - try: - self.text.tk.call('tk::GetSelection', self.text, 'CLIPBOARD') - except TclError: - return 'disabled' - else: - return 'normal' - - def about_dialog(self, event=None): - "Handle Help 'About IDLE' event." - # Synchronize with macosxSupport.overrideRootMenu.about_dialog. - aboutDialog.AboutDialog(self.top,'About IDLE') - - def config_dialog(self, event=None): - "Handle Options 'Configure IDLE' event." - # Synchronize with macosxSupport.overrideRootMenu.config_dialog. - configDialog.ConfigDialog(self.top,'Settings') - - def help_dialog(self, event=None): - "Handle Help 'IDLE Help' event." - # Synchronize with macosxSupport.overrideRootMenu.help_dialog. - if self.root: - parent = self.root - else: - parent = self.top - help.show_idlehelp(parent) - - def python_docs(self, event=None): - if sys.platform[:3] == 'win': - try: - os.startfile(self.help_url) - except OSError as why: - tkMessageBox.showerror(title='Document Start Failure', - message=str(why), parent=self.text) - else: - webbrowser.open(self.help_url) - return "break" - - def cut(self,event): - self.text.event_generate("<>") - return "break" - - def copy(self,event): - if not self.text.tag_ranges("sel"): - # There is no selection, so do nothing and maybe interrupt. - return - self.text.event_generate("<>") - return "break" - - def paste(self,event): - self.text.event_generate("<>") - self.text.see("insert") - return "break" - - def select_all(self, event=None): - self.text.tag_add("sel", "1.0", "end-1c") - self.text.mark_set("insert", "1.0") - self.text.see("insert") - return "break" - - def remove_selection(self, event=None): - self.text.tag_remove("sel", "1.0", "end") - self.text.see("insert") - - def move_at_edge_if_selection(self, edge_index): - """Cursor move begins at start or end of selection - - When a left/right cursor key is pressed create and return to Tkinter a - function which causes a cursor move from the associated edge of the - selection. - - """ - self_text_index = self.text.index - self_text_mark_set = self.text.mark_set - edges_table = ("sel.first+1c", "sel.last-1c") - def move_at_edge(event): - if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed - try: - self_text_index("sel.first") - self_text_mark_set("insert", edges_table[edge_index]) - except TclError: - pass - return move_at_edge - - def del_word_left(self, event): - self.text.event_generate('') - return "break" - - def del_word_right(self, event): - self.text.event_generate('') - return "break" - - def find_event(self, event): - SearchDialog.find(self.text) - return "break" - - def find_again_event(self, event): - SearchDialog.find_again(self.text) - return "break" - - def find_selection_event(self, event): - SearchDialog.find_selection(self.text) - return "break" - - def find_in_files_event(self, event): - GrepDialog.grep(self.text, self.io, self.flist) - return "break" - - def replace_event(self, event): - ReplaceDialog.replace(self.text) - return "break" - - def goto_line_event(self, event): - text = self.text - lineno = tkSimpleDialog.askinteger("Goto", - "Go to line number:",parent=text) - if lineno is None: - return "break" - if lineno <= 0: - text.bell() - return "break" - text.mark_set("insert", "%d.0" % lineno) - text.see("insert") - - def open_module(self, event=None): - # XXX Shouldn't this be in IOBinding? - try: - name = self.text.get("sel.first", "sel.last") - except TclError: - 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: - spec = importlib.util.find_spec(name) - except (ValueError, ImportError) as msg: - tkMessageBox.showerror("Import error", str(msg), parent=self.text) - return - if spec is None: - tkMessageBox.showerror("Import error", "module not found", - parent=self.text) - return - if not isinstance(spec.loader, importlib.abc.SourceLoader): - tkMessageBox.showerror("Import error", "not a source-based module", - parent=self.text) - return - try: - file_path = spec.loader.get_filename(name) - except AttributeError: - tkMessageBox.showerror("Import error", - "loader does not support get_filename", - parent=self.text) - return - if self.flist: - self.flist.open(file_path) - else: - self.io.loadfile(file_path) - return file_path - - def open_class_browser(self, event=None): - filename = self.io.filename - if not (self.__class__.__name__ == 'PyShellEditorWindow' - and filename): - filename = self.open_module() - if filename is None: - return - head, tail = os.path.split(filename) - base, ext = os.path.splitext(tail) - from idlelib import ClassBrowser - ClassBrowser.ClassBrowser(self.flist, base, [head]) - - def open_path_browser(self, event=None): - from idlelib import PathBrowser - PathBrowser.PathBrowser(self.flist) - - def open_turtle_demo(self, event = None): - import subprocess - - cmd = [sys.executable, - '-c', - 'from turtledemo.__main__ import main; main()'] - subprocess.Popen(cmd, shell=False) - - def gotoline(self, lineno): - if lineno is not None and lineno > 0: - self.text.mark_set("insert", "%d.0" % lineno) - self.text.tag_remove("sel", "1.0", "end") - self.text.tag_add("sel", "insert", "insert +1l") - self.center() - - def ispythonsource(self, filename): - if not filename or os.path.isdir(filename): - return True - 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 - - def close_hook(self): - if self.flist: - self.flist.unregister_maybe_terminate(self) - self.flist = None - - def set_close_hook(self, close_hook): - self.close_hook = close_hook - - def filename_change_hook(self): - if self.flist: - self.flist.filename_changed_edit(self) - self.saved_change_hook() - self.top.update_windowlist_registry(self) - self.ResetColorizer() - - def _addcolorizer(self): - if self.color: - return - if self.ispythonsource(self.io.filename): - self.color = self.ColorDelegator() - # can add more colorizers here... - if self.color: - self.per.removefilter(self.undo) - self.per.insertfilter(self.color) - self.per.insertfilter(self.undo) - - def _rmcolorizer(self): - if not self.color: - return - self.color.removecolors() - self.per.removefilter(self.color) - self.color = None - - def ResetColorizer(self): - "Update the color theme" - # Called from self.filename_change_hook and from configDialog.py - self._rmcolorizer() - self._addcolorizer() - 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']) - - 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 ResetFont(self): - "Update the text widgets' font if it is changed" - # 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.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(): - xkeydefs = idleConf.GetExtensionBindings(extensionName) - if xkeydefs: - for event, keylist in xkeydefs.items(): - self.text.event_delete(event, *keylist) - - def ApplyKeybindings(self): - "Update the keybindings after they are changed" - # 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) - if xkeydefs: - self.apply_bindings(xkeydefs) - #update menu accelerators - menuEventDict = {} - 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: - menu = self.menudict[menubarItem] - end = menu.index(END) - if end is None: - # Skip empty menus - continue - end += 1 - for index in range(0, end): - if menu.type(index) == 'command': - accel = menu.entrycget(index, 'accelerator') - if accel: - itemName = menu.entrycget(index, 'label') - event = '' - if menubarItem in menuEventDict: - if itemName in menuEventDict[menubarItem]: - event = menuEventDict[menubarItem][itemName] - if event: - accel = get_accelerator(keydefs, event) - menu.entryconfig(index, accelerator=accel) - - def set_notabs_indentwidth(self): - "Update the indentwidth if changed and not using tabs in this window" - # Called from configDialog.py - if not self.usetabs: - self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces', - type='int') - - def reset_help_menu_entries(self): - "Update the additional help entries on the Help menu" - help_list = idleConf.GetAllExtraHelpSourcesList() - helpmenu = self.menudict['help'] - # first delete the extra help entries, if any - helpmenu_length = helpmenu.index(END) - if helpmenu_length > self.base_helpmenu_length: - helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length) - # then rebuild them - if help_list: - helpmenu.add_separator() - for entry in help_list: - cmd = self.__extra_help_callback(entry[1]) - helpmenu.add_command(label=entry[0], command=cmd) - # and update the menu dictionary - self.menudict['help'] = helpmenu - - def __extra_help_callback(self, helpfile): - "Create a callback with the helpfile value frozen at definition time" - def display_extra_help(helpfile=helpfile): - if not helpfile.startswith(('www', 'http')): - helpfile = os.path.normpath(helpfile) - if sys.platform[:3] == 'win': - try: - os.startfile(helpfile) - except OSError as why: - tkMessageBox.showerror(title='Document Start Failure', - message=str(why), parent=self.text) - else: - webbrowser.open(helpfile) - return display_extra_help - - def update_recent_files_list(self, new_file=None): - "Load and update the recent files list and menus" - rf_list = [] - if os.path.exists(self.recent_files_path): - with open(self.recent_files_path, 'r', - encoding='utf_8', errors='replace') as rf_list_file: - rf_list = rf_list_file.readlines() - if new_file: - new_file = os.path.abspath(new_file) + '\n' - if new_file in rf_list: - rf_list.remove(new_file) # move to top - rf_list.insert(0, new_file) - # clean and save the recent files list - bad_paths = [] - for path in rf_list: - if '\0' in path or not os.path.exists(path[0:-1]): - bad_paths.append(path) - rf_list = [path for path in rf_list if path not in bad_paths] - ulchars = "1234567890ABCDEFGHIJK" - rf_list = rf_list[0:len(ulchars)] - try: - with open(self.recent_files_path, 'w', - encoding='utf_8', errors='replace') as rf_file: - rf_file.writelines(rf_list) - except OSError 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." - % self._filename_to_unicode(str(err)), - parent=self.text) - # for each edit window instance, construct the recent files menu - for instance in self.top.instance_dict: - 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] + " " + ufile_name, - command=callback, - underline=0) - - def __recent_file_callback(self, file_name): - def open_recent_file(fn_closure=file_name): - self.io.open(editFile=fn_closure) - return open_recent_file - - def saved_change_hook(self): - short = self.short_title() - long = self.long_title() - if short and long: - title = short + " - " + long + _py_version - elif short: - title = short - elif long: - title = long - else: - title = "Untitled" - icon = short or long or title - if not self.get_saved(): - title = "*%s*" % title - icon = "*%s" % icon - self.top.wm_title(title) - self.top.wm_iconname(icon) - - def get_saved(self): - return self.undo.get_saved() - - def set_saved(self, flag): - self.undo.set_saved(flag) - - def reset_undo(self): - self.undo.reset_undo() - - def short_title(self): - filename = self.io.filename - 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 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() - - def center(self, mark="insert"): - text = self.text - top, bot = self.getwindowlines() - lineno = self.getlineno(mark) - height = bot - top - newtop = max(1, lineno - height//2) - text.yview(float(newtop)) - - def getwindowlines(self): - text = self.text - top = self.getlineno("@0,0") - bot = self.getlineno("@0,65535") - if top == bot and text.winfo_height() == 1: - # Geometry manager hasn't run yet - height = int(text['height']) - bot = top + height - 1 - return top, bot - - def getlineno(self, mark="insert"): - text = self.text - return int(float(text.index(mark))) - - def get_geometry(self): - "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())) - - def close_event(self, event): - self.close() - - def maybesave(self): - if self.io: - if not self.get_saved(): - if self.top.state()!='normal': - self.top.deiconify() - self.top.lower() - self.top.lift() - return self.io.maybesave() - - def close(self): - 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) - WindowList.unregister_callback(self.postwindowsmenu) - self.unload_extensions() - self.io.close() - self.io = None - self.undo = None - if self.color: - self.color.close(False) - self.color = None - self.text = None - self.tkinter_vars = None - self.per.close() - self.per = None - self.top.destroy() - if self.close_hook: - # unless override: unregister from flist, terminate if last window - self.close_hook() - - def load_extensions(self): - self.extensions = {} - self.load_standard_extensions() - - def unload_extensions(self): - for ins in list(self.extensions.values()): - if hasattr(ins, "close"): - ins.close() - self.extensions = {} - - def load_standard_extensions(self): - for name in self.get_standard_extension_names(): - try: - self.load_extension(name) - except: - print("Failed to load extension", repr(name)) - traceback.print_exc() - - def get_standard_extension_names(self): - return idleConf.GetExtensions(editor_only=True) - - def load_extension(self, name): - try: - try: - mod = importlib.import_module('.' + name, package=__package__) - except (ImportError, TypeError): - mod = importlib.import_module(name) - except ImportError: - print("\nFailed to import extension: ", name) - raise - cls = getattr(mod, name) - keydefs = idleConf.GetExtensionBindings(name) - if hasattr(cls, "menudefs"): - self.fill_menus(cls.menudefs, keydefs) - ins = cls(self) - self.extensions[name] = ins - if keydefs: - self.apply_bindings(keydefs) - for vevent in keydefs: - methodname = vevent.replace("-", "_") - while methodname[:1] == '<': - methodname = methodname[1:] - while methodname[-1:] == '>': - methodname = methodname[:-1] - methodname = methodname + "_event" - if hasattr(ins, methodname): - self.text.bind(vevent, getattr(ins, methodname)) - - def apply_bindings(self, keydefs=None): - if keydefs is None: - keydefs = self.Bindings.default_keydefs - text = self.text - text.keydefs = keydefs - for event, keylist in keydefs.items(): - if keylist: - text.event_add(event, *keylist) - - def fill_menus(self, menudefs=None, keydefs=None): - """Add appropriate entries to the menus and submenus - - Menus that are absent or None in self.menudict are ignored. - """ - if menudefs is None: - menudefs = self.Bindings.menudefs - if keydefs is None: - keydefs = self.Bindings.default_keydefs - menudict = self.menudict - text = self.text - for mname, entrylist in menudefs: - menu = menudict.get(mname) - if not menu: - continue - for entry in entrylist: - if not entry: - menu.add_separator() - else: - label, eventname = entry - checkbutton = (label[:1] == '!') - if checkbutton: - label = label[1:] - underline, label = prepstr(label) - accelerator = get_accelerator(keydefs, eventname) - def command(text=text, eventname=eventname): - text.event_generate(eventname) - if checkbutton: - var = self.get_var_obj(eventname, BooleanVar) - menu.add_checkbutton(label=label, underline=underline, - command=command, accelerator=accelerator, - variable=var) - else: - menu.add_command(label=label, underline=underline, - command=command, - accelerator=accelerator) - - def getvar(self, name): - var = self.get_var_obj(name) - if var: - value = var.get() - return value - else: - 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) - - def get_var_obj(self, name, vartype=None): - var = self.tkinter_vars.get(name) - if not var and vartype: - # create a Tkinter variable object with self.text as master: - self.tkinter_vars[name] = var = vartype(self.text) - return var - - # Tk implementations of "virtual text methods" -- each platform - # reusing IDLE's support code needs to define these for its GUI's - # flavor of widget. - - # Is character at text_index in a Python string? Return 0 for - # "guaranteed no", true for anything else. This info is expensive - # to compute ab initio, but is probably already known by the - # platform's colorizer. - - def is_char_in_string(self, text_index): - if self.color: - # Return true iff colorizer hasn't (re)gotten this far - # yet, or the character is tagged as being in a string - return self.text.tag_prevrange("TODO", text_index) or \ - "STRING" in self.text.tag_names(text_index) - else: - # The colorizer is missing: assume the worst - return 1 - - # If a selection is defined in the text widget, return (start, - # end) as Tkinter text indices, otherwise return (None, None) - def get_selection_indices(self): - try: - first = self.text.index("sel.first") - last = self.text.index("sel.last") - return first, last - except TclError: - return None, None - - # Return the text widget's current view of what a tab stop means - # (equivalent width in spaces). - - def get_tk_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): - text = self.text - if self.get_tk_tabwidth() != newtabwidth: - # Set text widget tab width - 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) - - def set_indentation_params(self, is_py_src, guess=True): - if is_py_src and guess: - 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) - - def smart_backspace_event(self, event): - text = self.text - first, last = self.get_selection_indices() - if first and last: - text.delete(first, last) - text.mark_set("insert", first) - return "break" - # Delete whitespace left, until hitting a real char or closest - # preceding virtual tab stop. - chars = text.get("insert linestart", "insert") - if chars == '': - if text.compare("insert", ">", "1.0"): - # easy: delete preceding newline - text.delete("insert-1c") - else: - text.bell() # at start of buffer - return "break" - if chars[-1] not in " \t": - # easy: delete preceding real char - text.delete("insert-1c") - return "break" - # Ick. It may require *inserting* spaces if we back up over a - # tab character! This is written to be clear, not fast. - tabwidth = self.tabwidth - have = len(chars.expandtabs(tabwidth)) - 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 == last_line_of_prompt: - break - chars = chars[:-1] - ncharsdeleted = ncharsdeleted + 1 - have = len(chars.expandtabs(tabwidth)) - if have <= want or chars[-1] not in " \t": - break - text.undo_block_start() - text.delete("insert-%dc" % ncharsdeleted, "insert") - if have < want: - text.insert("insert", ' ' * (want - have)) - text.undo_block_stop() - return "break" - - def smart_indent_event(self, event): - # if intraline selection: - # delete it - # elif multiline selection: - # do indent-region - # else: - # indent one level - text = self.text - first, last = self.get_selection_indices() - text.undo_block_start() - try: - if first and last: - if index2line(first) != index2line(last): - return self.indent_region_event(event) - text.delete(first, last) - text.mark_set("insert", first) - prefix = text.get("insert linestart", "insert") - raw, effective = classifyws(prefix, self.tabwidth) - if raw == len(prefix): - # only whitespace to the left - self.reindent_to(effective + self.indentwidth) - else: - # tab to the next 'stop' within or to right of line's text: - if self.usetabs: - pad = '\t' - else: - effective = len(prefix.expandtabs(self.tabwidth)) - n = self.indentwidth - pad = ' ' * (n - effective % n) - text.insert("insert", pad) - text.see("insert") - return "break" - finally: - text.undo_block_stop() - - def newline_and_indent_event(self, event): - text = self.text - first, last = self.get_selection_indices() - text.undo_block_start() - try: - if first and last: - text.delete(first, last) - text.mark_set("insert", first) - line = text.get("insert linestart", "insert") - i, n = 0, len(line) - while i < n and line[i] in " \t": - i = i+1 - if i == n: - # the cursor is in or at leading indentation in a continuation - # line; just inject an empty line at the start - text.insert("insert linestart", '\n') - return "break" - indent = line[:i] - # strip whitespace before insert point unless it's in the prompt - i = 0 - 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: - text.delete("insert - %d chars" % i, "insert") - # strip whitespace after insert point - while text.get("insert") in " \t": - text.delete("insert") - # start new line - text.insert("insert", '\n') - - # 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.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_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 - y.set_lo(bod or 0) - else: - r = text.tag_prevrange("console", "insert") - if r: - startatindex = r[1] - else: - startatindex = "1.0" - rawtext = text.get(startatindex, "insert") - y.set_str(rawtext) - y.set_lo(0) - - c = y.get_continuation_type() - if c != PyParse.C_NONE: - # The current stmt hasn't ended yet. - 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: - # inside a string which started before this line; - # just mimic the current indent - text.insert("insert", indent) - 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: - # 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 - # beyond leftmost =; else to beyond first chunk of - # non-whitespace on initial line - if y.get_num_lines_in_stmt() > 1: - text.insert("insert", indent) - else: - self.reindent_to(y.compute_backslash_indent()) - else: - assert 0, "bogus continuation type %r" % (c,) - return "break" - - # This line starts a brand new stmt; indent relative to - # indentation of initial line of closest preceding - # interesting stmt. - indent = y.get_base_indent_string() - text.insert("insert", indent) - if y.is_block_opener(): - self.smart_indent_event(event) - elif indent and y.is_block_closer(): - self.smart_backspace_event(event) - return "break" - finally: - text.see("insert") - text.undo_block_stop() - - # Our editwin provides an is_char_in_string function that works - # with a Tk text index, but PyParse only knows about offsets into - # a string. This builds a function for PyParse that accepts an - # offset. - - def _build_char_in_string_func(self, startindex): - def inner(offset, _startindex=startindex, - _icis=self.is_char_in_string): - 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() -## if new != self.tabwidth: -## self.tabwidth = new -## 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): - if self.usetabs: - ntabs, nspaces = divmod(n, self.tabwidth) - return '\t' * ntabs + ' ' * nspaces - else: - return ' ' * n - - # Delete from beginning of line to insert point, then reinsert - # column logical (meaning use tabs if appropriate) spaces. - - def reindent_to(self, column): - text = self.text - text.undo_block_start() - if text.compare("insert linestart", "!=", "insert"): - text.delete("insert linestart", "insert") - if column: - 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 - # blocks are found). - - def guess_indent(self): - opener, indented = IndentSearcher(self.text, self.tabwidth).run() - if opener and indented: - raw, indentsmall = classifyws(opener, self.tabwidth) - raw, indentlarge = classifyws(indented, self.tabwidth) - else: - indentsmall = indentlarge = 0 - return indentlarge - indentsmall - -# "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 - -import tokenize -_tokenize = tokenize -del tokenize - -class IndentSearcher(object): - - # .run() chews over the Text widget, looking for a block opener - # and the stmt following it. Returns a pair, - # (line containing block opener, line containing stmt) - # Either or both may be None. - - def __init__(self, text, tabwidth): - self.text = text - self.tabwidth = tabwidth - self.i = self.finished = 0 - self.blkopenline = self.indentedline = None - - def readline(self): - if self.finished: - return "" - i = self.i = self.i + 1 - mark = repr(i) + ".0" - if self.text.compare(mark, ">=", "end"): - return "" - return self.text.get(mark, mark + " lineend+1c") - - def tokeneater(self, type, token, start, end, line, - INDENT=_tokenize.INDENT, - NAME=_tokenize.NAME, - OPENERS=('class', 'def', 'for', 'if', 'try', 'while')): - if self.finished: - pass - elif type == NAME and token in OPENERS: - self.blkopenline = line - elif type == INDENT and self.blkopenline: - self.indentedline = line - self.finished = 1 - - def run(self): - 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): - # since we cut off the tokenizer early, we can trigger - # spurious errors - pass - finally: - _tokenize.tabsize = save_tabsize - return self.blkopenline, self.indentedline - -### end autoindent code ### - -def prepstr(s): - # Helper to extract the underscore from a string, e.g. - # prepstr("Co_py") returns (2, "Copy"). - i = s.find('_') - if i >= 0: - s = s[:i] + s[i+1:] - return i, s - - -keynames = { - 'bracketleft': '[', - 'bracketright': ']', - 'slash': '/', -} - -def get_accelerator(keydefs, eventname): - keylist = keydefs.get(eventname) - # issue10940: temporary workaround to prevent hang with OS X Cocoa Tk 8.5 - # if not keylist: - if (not keylist) or (macosxSupport.isCocoaTk() and eventname in { - "<>", - "<>", - "<>"}): - return "" - s = keylist[0] - s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s) - s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s) - s = re.sub("Key-", "", s) - s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu - s = re.sub("Control-", "Ctrl-", s) - s = re.sub("-", "+", s) - s = re.sub("><", " ", s) - s = re.sub("<", "", s) - s = re.sub(">", "", s) - return s - - -def fixwordbreaks(root): - # 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', '[a-zA-Z0-9_]') - tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]') - - -def _editor_window(parent): # htest # - # error if close master window first - timer event, after script - root = parent - fixwordbreaks(root) - if sys.argv[1:]: - filename = sys.argv[1] - else: - filename = None - macosxSupport.setupApp(root, None) - edit = EditorWindow(root=root, filename=filename) - edit.text.bind("<>", edit.close_event) - # Does not stop error, neither does following - # edit.text.bind("<>", edit.close_event) - -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 deleted file mode 100644 index a9989a8..0000000 --- a/Lib/idlelib/FileList.py +++ /dev/null @@ -1,129 +0,0 @@ -import os -from tkinter import * -import tkinter.messagebox as tkMessageBox - - -class FileList: - - # N.B. this import overridden in PyShellFileList. - from idlelib.EditorWindow import EditorWindow - - def __init__(self, root): - self.root = root - self.dict = {} - self.inversedict = {} - self.vars = {} # For EditorWindow.getrawvar (shared Tcl variables) - - def open(self, filename, action=None): - assert filename - filename = self.canonize(filename) - if os.path.isdir(filename): - # This can happen when bad filename is passed on command line: - tkMessageBox.showerror( - "File Error", - "%r is a directory." % (filename,), - master=self.root) - return None - key = os.path.normcase(filename) - if key in self.dict: - edit = self.dict[key] - edit.top.wakeup() - return edit - if action: - # 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 - - def gotofileline(self, filename, lineno=None): - edit = self.open(filename) - if edit is not None and lineno is not None: - edit.gotoline(lineno) - - def new(self, filename=None): - return self.EditorWindow(self, filename) - - def close_all_callback(self, *args, **kwds): - for edit in list(self.inversedict): - reply = edit.close() - if reply == "cancel": - break - return "break" - - def unregister_maybe_terminate(self, edit): - try: - key = self.inversedict[edit] - except KeyError: - print("Don't know this EditorWindow object. (close)") - return - if key: - del self.dict[key] - del self.inversedict[edit] - if not self.inversedict: - self.root.quit() - - def filename_changed_edit(self, edit): - edit.saved_change_hook() - try: - key = self.inversedict[edit] - except KeyError: - print("Don't know this EditorWindow object. (rename)") - return - filename = edit.io.filename - if not filename: - if key: - del self.dict[key] - self.inversedict[edit] = None - return - filename = self.canonize(filename) - newkey = os.path.normcase(filename) - if newkey == key: - return - if newkey in self.dict: - conflict = self.dict[newkey] - self.inversedict[conflict] = None - tkMessageBox.showerror( - "Name Conflict", - "You now have multiple edit windows open for %r" % (filename,), - master=self.root) - self.dict[newkey] = edit - self.inversedict[edit] = newkey - if key: - try: - del self.dict[key] - except KeyError: - pass - - def canonize(self, filename): - if not os.path.isabs(filename): - try: - pwd = os.getcwd() - except OSError: - pass - else: - filename = os.path.join(pwd, filename) - return os.path.normpath(filename) - - -def _test(): - from idlelib.EditorWindow import fixwordbreaks - import sys - root = Tk() - fixwordbreaks(root) - root.withdraw() - flist = FileList(root) - if sys.argv[1:]: - for filename in sys.argv[1:]: - flist.open(filename) - else: - flist.new() - if flist.inversedict: - root.mainloop() - -if __name__ == '__main__': - _test() diff --git a/Lib/idlelib/FormatParagraph.py b/Lib/idlelib/FormatParagraph.py deleted file mode 100644 index 7a9d185..0000000 --- a/Lib/idlelib/FormatParagraph.py +++ /dev/null @@ -1,195 +0,0 @@ -"""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', '<>'), - ]) - ] - - 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 deleted file mode 100644 index 721b231..0000000 --- a/Lib/idlelib/GrepDialog.py +++ /dev/null @@ -1,158 +0,0 @@ -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, errors='replace') as f: - for lineno, line in enumerate(f, 1): - if line[-1:] == '\n': - line = line[:-1] - if prog.search(line): - sys.stdout.write("%s: %s: %s\n" % - (fn, lineno, line)) - hits += 1 - except OSError as msg: - print(msg) - print(("Hits found: %s\n" - "(Hint: right-click to open locations.)" - % hits) if hits else "No hits.") - except AttributeError: - # Tk window has been closed, OutputWindow.text = None, - # so in OW.write, OW.text.insert fails. - pass - - def findfiles(self, dir, base, rec): - try: - names = os.listdir(dir or os.curdir) - except OSError 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/HyperParser.py b/Lib/idlelib/HyperParser.py deleted file mode 100644 index 77cb057..0000000 --- a/Lib/idlelib/HyperParser.py +++ /dev/null @@ -1,313 +0,0 @@ -"""Provide advanced parsing abilities for ParenMatch and other extensions. - -HyperParser uses PyParser. PyParser mostly gives information on the -proper indentation of code. HyperParser gives additional information on -the structure of code. -""" - -import string -from keyword import iskeyword -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)] - - -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) - - def index2line(index): - return int(float(index)) - lno = index2line(text.index(index)) - - if not editwin.context_use_ps1: - for context in editwin.num_context_lines: - startat = max(lno - context, 1) - startatindex = repr(startat) + ".0" - stopatindex = "%d.end" % lno - # We add the newline because PyParse requires a newline - # at end. We add a space so that index won't be at end - # of line, so that its status will be the same as the - # char before it, if should. - 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: - break - parser.set_lo(bod or 0) - else: - r = text.tag_prevrange("console", index) - if r: - startatindex = r[1] - else: - startatindex = "1.0" - stopatindex = "%d.end" % lno - # We add the newline because PyParse requires it. We add a - # space so that index won't be at end of line, so that its - # status will be the same as the char before it, if should. - parser.set_str(text.get(startatindex, stopatindex)+' \n') - parser.set_lo(0) - - # We want what the parser has, minus the last newline and space. - 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 - self.bracketing = parser.get_last_stmt_bracketing() - # find which pairs of bracketing are openers. These always - # correspond to a character of rawtext. - self.isopener = [i>0 and self.bracketing[i][1] > - self.bracketing[i-1][1] - for i in range(len(self.bracketing))] - - self.set_index(index) - - def set_index(self, index): - """Set the index to which the functions relate. - - The index must be in the same statement. - """ - indexinrawtext = (len(self.rawtext) - - len(self.text.get(index, self.stopatindex))) - if indexinrawtext < 0: - raise ValueError("Index %s precedes the analyzed statement" - % index) - self.indexinrawtext = indexinrawtext - # find the rightmost bracket to which index belongs - self.indexbracket = 0 - while (self.indexbracket < len(self.bracketing)-1 and - self.bracketing[self.indexbracket+1][0] < self.indexinrawtext): - self.indexbracket += 1 - if (self.indexbracket < len(self.bracketing)-1 and - self.bracketing[self.indexbracket+1][0] == self.indexinrawtext and - not self.isopener[self.indexbracket+1]): - self.indexbracket += 1 - - def is_in_string(self): - """Is the index given to the HyperParser in a string?""" - # The bracket to which we belong should be an opener. - # If it's an opener, it has to have a character. - return (self.isopener[self.indexbracket] and - self.rawtext[self.bracketing[self.indexbracket][0]] - in ('"', "'")) - - def is_in_code(self): - """Is the index given to the HyperParser in normal code?""" - return (not self.isopener[self.indexbracket] or - self.rawtext[self.bracketing[self.indexbracket][0]] - not in ('#', '"', "'")) - - def get_surrounding_brackets(self, openers='([{', mustclose=False): - """Return bracket indexes or None. - - If the index given to the HyperParser is surrounded by a - bracket defined in openers (or at least has one before it), - return the indices of the opening bracket and the closing - bracket (or the end of line, whichever comes first). - - If it is not surrounded by brackets, or the end of line comes - before the closing bracket and mustclose is True, returns None. - """ - - bracketinglevel = self.bracketing[self.indexbracket][1] - before = self.indexbracket - while (not self.isopener[before] or - self.rawtext[self.bracketing[before][0]] not in openers or - self.bracketing[before][1] > bracketinglevel): - before -= 1 - if before < 0: - return None - bracketinglevel = min(bracketinglevel, self.bracketing[before][1]) - after = self.indexbracket + 1 - while (after < len(self.bracketing) and - self.bracketing[after][1] >= bracketinglevel): - after += 1 - - beforeindex = self.text.index("%s-%dc" % - (self.stopatindex, len(self.rawtext)-self.bracketing[before][0])) - if (after >= len(self.bracketing) or - self.bracketing[after][0] > len(self.rawtext)): - if mustclose: - return None - afterindex = self.stopatindex - else: - # We are after a real char, so it is a ')' and we give the - # index before it. - afterindex = self.text.index( - "%s-%dc" % (self.stopatindex, - len(self.rawtext)-(self.bracketing[after][0]-1))) - - 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. - 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])] - ): - 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 - - 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. - """ - if not self.is_in_code(): - raise ValueError("get_expression should only be called" - "if index is inside a code.") - - rawtext = self.rawtext - bracketing = self.bracketing - - brck_index = self.indexbracket - brck_limit = bracketing[brck_index][0] - pos = self.indexinrawtext - - last_identifier_pos = pos - postdot_phase = True - - while 1: - # Eat whitespaces, comments, and if postdot_phase is False - a dot - while 1: - if pos>brck_limit and rawtext[pos-1] in self._whitespace_chars: - # Eat a whitespace - pos -= 1 - elif (not postdot_phase and - pos > brck_limit and rawtext[pos-1] == '.'): - # Eat a dot - pos -= 1 - postdot_phase = True - # The next line will fail if we are *inside* a comment, - # but we shouldn't be. - elif (pos == brck_limit and brck_index > 0 and - rawtext[bracketing[brck_index-1][0]] == '#'): - # Eat a comment - brck_index -= 2 - brck_limit = bracketing[brck_index][0] - pos = bracketing[brck_index+1][0] - else: - # If we didn't eat anything, quit. - break - - if not postdot_phase: - # We didn't find a dot, so the expression end at the - # last identifier pos. - break - - ret = self._eat_identifier(rawtext, brck_limit, pos) - if ret: - # There is an identifier to eat - pos = pos - ret - last_identifier_pos = pos - # Now, to continue the search, we must find a dot. - postdot_phase = False - # (the loop continues now) - - elif pos == brck_limit: - # We are at a bracketing limit. If it is a closing - # bracket, eat the bracket, otherwise, stop the search. - level = bracketing[brck_index][1] - while brck_index > 0 and bracketing[brck_index-1][1] > level: - brck_index -= 1 - if bracketing[brck_index][0] == brck_limit: - # We were not at the end of a closing bracket - break - pos = bracketing[brck_index][0] - brck_index -= 1 - brck_limit = bracketing[brck_index][0] - last_identifier_pos = pos - if rawtext[pos] in "([": - # [] and () may be used after an identifier, so we - # continue. postdot_phase is True, so we don't allow a dot. - pass - else: - # We can't continue after other types of brackets - if rawtext[pos] in "'\"": - # Scan a string prefix - while pos > 0 and rawtext[pos - 1] in "rRbBuU": - pos -= 1 - last_identifier_pos = pos - break - - else: - # We've found an operator or something. - break - - return rawtext[last_identifier_pos:self.indexinrawtext] - - -if __name__ == '__main__': - import unittest - unittest.main('idlelib.idle_test.test_hyperparser', verbosity=2) diff --git a/Lib/idlelib/IOBinding.py b/Lib/idlelib/IOBinding.py deleted file mode 100644 index 84f39a2..0000000 --- a/Lib/idlelib/IOBinding.py +++ /dev/null @@ -1,565 +0,0 @@ -import codecs -from codecs import BOM_UTF8 -import os -import re -import shlex -import sys -import tempfile - -import tkinter.filedialog as tkFileDialog -import tkinter.messagebox as tkMessageBox -from tkinter.simpledialog import askstring - -from idlelib.configHandler import idleConf - - -# 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 - -# Encoding for file names -filesystemencoding = sys.getfilesystemencoding() ### currently unused - -locale_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: - locale_encoding = locale.getdefaultlocale()[1] - codecs.lookup(locale_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 - locale_encoding = locale.nl_langinfo(locale.CODESET) - if locale_encoding is None or locale_encoding is '': - # situation occurs on Mac OS X - locale_encoding = 'ascii' - 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 is None or locale_encoding is '': - # situation occurs on Mac OS X - locale_encoding = 'ascii' - codecs.lookup(locale_encoding) - except (ValueError, LookupError): - pass - -locale_encoding = locale_encoding.lower() - -encoding = locale_encoding ### KBK 07Sep07 This is used all over IDLE, check! - ### 'encoding' is used below in encode(), check! - -coding_re = re.compile(r'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)', re.ASCII) -blank_re = re.compile(r'^[ \t\f]*(?:[#\r\n]|$)', re.ASCII) - -def coding_spec(data): - """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. - """ - 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] - for line in lst: - match = coding_re.match(line) - if match is not None: - break - if not blank_re.match(line): - return None - else: - return None - name = match.group(1) - try: - codecs.lookup(name) - except LookupError: - # The standard encoding error does not indicate the encoding - raise LookupError("Unknown encoding: "+name) - return name - - -class IOBinding: - - def __init__(self, editwin): - self.editwin = editwin - self.text = editwin.text - self.__id_open = self.text.bind("<>", self.open) - self.__id_save = self.text.bind("<>", self.save) - self.__id_saveas = self.text.bind("<>", - self.save_as) - self.__id_savecopy = self.text.bind("<>", - self.save_a_copy) - self.fileencoding = None - self.__id_print = self.text.bind("<>", self.print_window) - - def close(self): - # Undo command bindings - self.text.unbind("<>", self.__id_open) - self.text.unbind("<>", self.__id_save) - self.text.unbind("<>",self.__id_saveas) - self.text.unbind("<>", self.__id_savecopy) - self.text.unbind("<>", self.__id_print) - # Break cycles - self.editwin = None - self.text = None - self.filename_change_hook = None - - def get_saved(self): - return self.editwin.get_saved() - - def set_saved(self, flag): - self.editwin.set_saved(flag) - - def reset_undo(self): - self.editwin.reset_undo() - - filename_change_hook = None - - def set_filename_change_hook(self, hook): - self.filename_change_hook = hook - - filename = None - dirname = None - - def set_filename(self, filename): - if filename and os.path.isdir(filename): - self.filename = None - self.dirname = filename - else: - self.filename = filename - self.dirname = None - self.set_saved(1) - if self.filename_change_hook: - self.filename_change_hook() - - def open(self, event=None, editFile=None): - flist = self.editwin.flist - # Save in case parent window is closed (ie, during askopenfile()). - if flist: - if not editFile: - filename = self.askopenfile() - else: - filename=editFile - if filename: - # If editFile is valid and already open, flist.open will - # shift focus to its existing window. - # If the current window exists and is a fresh unnamed, - # unmodified editor window (not an interpreter shell), - # pass self.loadfile to flist.open so it will load the file - # in the current window (if the file is not already open) - # instead of a new window. - if (self.editwin and - not getattr(self.editwin, 'interp', None) and - not self.filename and - self.get_saved()): - flist.open(filename, self.loadfile) - else: - flist.open(filename) - else: - if self.text: - self.text.focus_set() - return "break" - - # Code for use outside IDLE: - if self.get_saved(): - reply = self.maybesave() - if reply == "cancel": - self.text.focus_set() - return "break" - if not editFile: - filename = self.askopenfile() - else: - filename=editFile - if filename: - self.loadfile(filename) - else: - self.text.focus_set() - return "break" - - eol = r"(\r\n)|\n|\r" # \r\n (Windows), \n (UNIX), or \r (Mac) - eol_re = re.compile(eol) - 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. - with open(filename, 'rb') as f: - two_lines = f.readline() + f.readline() - f.seek(0) - bytes = f.read() - except OSError 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 - # We now convert all end-of-lines to '\n's - firsteol = self.eol_re.search(chars) - if firsteol: - self.eol_convention = firsteol.group(0) - 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 - # Check presence of a UTF-8 signature first - if bytes.startswith(BOM_UTF8): - try: - chars = bytes[3:].decode("utf-8") - except UnicodeDecodeError: - # has UTF-8 signature, but fails to decode... - return None, False - else: - # Indicates that this file originally had a BOM - self.fileencoding = 'BOM' - return chars, False - # Next look for coding specification - try: - enc = coding_spec(two_lines) - except LookupError as name: - tkMessageBox.showerror( - title="Error loading the file", - message="The encoding '%s' is not known to this Python "\ - "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: - pass - # Try ascii: - try: - chars = str(bytes, 'ascii') - self.fileencoding = None - return chars, False - except UnicodeDecodeError: - pass - # Try utf-8: - try: - chars = str(bytes, 'utf-8') - self.fileencoding = 'utf-8' - return chars, False - except UnicodeDecodeError: - 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 = locale_encoding, - parent = self.editwin.text) - - if enc: - chars = str(bytes, enc) - self.fileencoding = None - return chars, True - except (UnicodeDecodeError, LookupError): - pass - return None, False # None on failure - - def maybesave(self): - if self.get_saved(): - return "yes" - message = "Do you want to save %s before closing?" % ( - self.filename or "this untitled document") - confirm = tkMessageBox.askyesnocancel( - title="Save On Close", - message=message, - default=tkMessageBox.YES, - parent=self.text) - if confirm: - reply = "yes" - self.save(None) - if not self.get_saved(): - reply = "cancel" - elif confirm is None: - reply = "cancel" - else: - reply = "no" - self.text.focus_set() - return reply - - def save(self, event): - if not self.filename: - self.save_as(event) - else: - if self.writefile(self.filename): - self.set_saved(True) - try: - self.editwin.store_file_breaks() - except AttributeError: # may be a PyShell - pass - self.text.focus_set() - return "break" - - def save_as(self, event): - filename = self.asksavefile() - if filename: - if self.writefile(filename): - self.set_filename(filename) - self.set_saved(1) - try: - self.editwin.store_file_breaks() - except AttributeError: - pass - self.text.focus_set() - self.updaterecentfileslist(filename) - return "break" - - def save_a_copy(self, event): - filename = self.asksavefile() - if filename: - self.writefile(filename) - self.text.focus_set() - self.updaterecentfileslist(filename) - return "break" - - def writefile(self, filename): - self.fixlastline() - text = self.text.get("1.0", "end-1c") - if self.eol_convention != "\n": - text = text.replace("\n", self.eol_convention) - chars = self.encode(text) - try: - with open(filename, "wb") as f: - f.write(chars) - return True - except OSError as msg: - tkMessageBox.showerror("I/O Error", str(msg), - parent=self.text) - return False - - def encode(self, chars): - if isinstance(chars, bytes): - # 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 - 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") - - 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( - title="Print", - message="Print to Default Printer", - default=tkMessageBox.OK, - parent=self.text) - if not confirm: - self.text.focus_set() - return "break" - tempfilename = None - saved = self.get_saved() - if saved: - filename = self.filename - # shell undo is reset after every prompt, looks saved, probably isn't - if not saved or filename is None: - (tfd, tempfilename) = tempfile.mkstemp(prefix='IDLE_tmp_') - filename = tempfilename - os.close(tfd) - if not self.writefile(tempfilename): - os.unlink(tempfilename) - return "break" - platform = os.name - printPlatform = True - if platform == 'posix': #posix platform - command = idleConf.GetOption('main','General', - 'print-command-posix') - command = command + " 2>&1" - elif platform == 'nt': #win32 platform - command = idleConf.GetOption('main','General','print-command-win') - else: #no printing for this platform - printPlatform = False - if printPlatform: #we can try to print for this platform - command = command % shlex.quote(filename) - pipe = os.popen(command, "r") - # things can get ugly on NT if there is no printer available. - output = pipe.read().strip() - status = pipe.close() - if status: - output = "Printing failed (exit status 0x%x)\n" % \ - status + output - if output: - output = "Printing command: %s\n" % repr(command) + output - tkMessageBox.showerror("Print status", output, parent=self.text) - else: #no printing for this platform - message = "Printing is not enabled for this platform: %s" % platform - tkMessageBox.showinfo("Print status", message, parent=self.text) - if tempfilename: - os.unlink(tempfilename) - return "break" - - opendialog = None - savedialog = None - - filetypes = [ - ("Python files", "*.py *.pyw", "TEXT"), - ("Text files", "*.txt", "TEXT"), - ("All files", "*"), - ] - - defaultextension = '.py' if sys.platform == 'darwin' else '' - - def askopenfile(self): - dir, base = self.defaultfilename("open") - if not self.opendialog: - self.opendialog = tkFileDialog.Open(parent=self.text, - filetypes=self.filetypes) - filename = self.opendialog.show(initialdir=dir, initialfile=base) - return filename - - def defaultfilename(self, mode="open"): - if self.filename: - return os.path.split(self.filename) - elif self.dirname: - return self.dirname, "" - else: - try: - pwd = os.getcwd() - except OSError: - pwd = "" - return pwd, "" - - def asksavefile(self): - dir, base = self.defaultfilename("save") - if not self.savedialog: - self.savedialog = tkFileDialog.SaveAs( - parent=self.text, - filetypes=self.filetypes, - defaultextension=self.defaultextension) - filename = self.savedialog.show(initialdir=dir, initialfile=base) - 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) - -def _io_binding(parent): # htest # - from tkinter import Toplevel, Text - - root = Toplevel(parent) - root.title("Test IOBinding") - width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) - root.geometry("+%d+%d"%(x, y + 150)) - class MyEditWin: - def __init__(self, text): - self.text = text - self.flist = None - self.text.bind("", self.open) - self.text.bind('', self.print) - self.text.bind("", self.save) - self.text.bind("", self.saveas) - self.text.bind('', self.savecopy) - def get_saved(self): return 0 - def set_saved(self, flag): pass - def reset_undo(self): pass - def open(self, event): - self.text.event_generate("<>") - def print(self, event): - self.text.event_generate("<>") - def save(self, event): - self.text.event_generate("<>") - def saveas(self, event): - self.text.event_generate("<>") - def savecopy(self, event): - self.text.event_generate("<>") - - text = Text(root) - text.pack() - text.focus_set() - editwin = MyEditWin(text) - IOBinding(editwin) - -if __name__ == "__main__": - from idlelib.idle_test.htest import run - run(_io_binding) diff --git a/Lib/idlelib/IdleHistory.py b/Lib/idlelib/IdleHistory.py deleted file mode 100644 index 078af29..0000000 --- a/Lib/idlelib/IdleHistory.py +++ /dev/null @@ -1,104 +0,0 @@ -"Implement Idle Shell history mechanism with History class" - -from idlelib.configHandler import idleConf - -class History: - ''' Implement Idle Shell history mechanism. - - store - Store source statement (called from PyShell.resetoutput). - fetch - Fetch stored statement matching prefix already entered. - history_next - Bound to <> event (default Alt-N). - history_prev - Bound to <> event (default Alt-P). - ''' - def __init__(self, text): - '''Initialize data attributes and bind event methods. - - .text - Idle wrapper of tk Text widget, with .bell(). - .history - source statements, possibly with multiple lines. - .prefix - source already entered at prompt; filters history list. - .pointer - index into history. - .cyclic - wrap around history list (or not). - ''' - self.text = text - self.history = [] - self.prefix = None - self.pointer = None - self.cyclic = idleConf.GetOption("main", "History", "cyclic", 1, "bool") - text.bind("<>", self.history_prev) - text.bind("<>", self.history_next) - - def history_next(self, event): - "Fetch later statement; start with ealiest if cyclic." - self.fetch(reverse=False) - return "break" - - def history_prev(self, event): - "Fetch earlier statement; start with most recent." - self.fetch(reverse=True) - return "break" - - def fetch(self, reverse): - '''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. - Sound bell when return to start line or cannot leave a line - because cyclic is False. - ''' - nhist = len(self.history) - pointer = self.pointer - prefix = self.prefix - if pointer is not None and prefix is not None: - if self.text.compare("insert", "!=", "end-1c") or \ - self.text.get("iomark", "end-1c") != self.history[pointer]: - pointer = prefix = None - self.text.mark_set("insert", "end-1c") # != after cursor move - if pointer is None or prefix is None: - prefix = self.text.get("iomark", "end-1c") - if reverse: - pointer = nhist # will be decremented - else: - if self.cyclic: - pointer = -1 # will be incremented - else: # abort history_next - self.text.bell() - return - nprefix = len(prefix) - while 1: - pointer += -1 if reverse else 1 - if pointer < 0 or pointer >= nhist: - self.text.bell() - if not self.cyclic and pointer < 0: # abort history_prev - return - else: - if self.text.get("iomark", "end-1c") != prefix: - self.text.delete("iomark", "end-1c") - self.text.insert("iomark", prefix) - pointer = prefix = None - break - item = self.history[pointer] - if item[:nprefix] == prefix and len(item) > nprefix: - self.text.delete("iomark", "end-1c") - self.text.insert("iomark", item) - break - self.text.see("insert") - self.text.tag_remove("sel", "1.0", "end") - self.pointer = pointer - self.prefix = prefix - - def store(self, source): - "Store Shell input statement into history list." - source = source.strip() - if len(source) > 2: - # avoid duplicates - try: - self.history.remove(source) - except ValueError: - pass - self.history.append(source) - self.pointer = None - self.prefix = None - -if __name__ == "__main__": - from unittest import main - main('idlelib.idle_test.test_idlehistory', verbosity=2, exit=False) diff --git a/Lib/idlelib/MultiCall.py b/Lib/idlelib/MultiCall.py deleted file mode 100644 index 251a84d..0000000 --- a/Lib/idlelib/MultiCall.py +++ /dev/null @@ -1,446 +0,0 @@ -""" -MultiCall - a class which inherits its methods from a Tkinter widget (Text, for -example), but enables multiple calls of functions per virtual event - all -matching events will be called, not only the most specific one. This is done -by wrapping the event functions - event_add, event_delete and event_info. -MultiCall recognizes only a subset of legal event sequences. Sequences which -are not recognized are treated by the original Tk handling mechanism. A -more-specific event will be called before a less-specific event. - -The recognized sequences are complete one-event sequences (no emacs-style -Ctrl-X Ctrl-C, no shortcuts like <3>), for all types of events. -Key/Button Press/Release events can have modifiers. -The recognized modifiers are Shift, Control, Option and Command for Mac, and -Control, Alt, Shift, Meta/M for other platforms. - -For all events which were handled by MultiCall, a new member is added to the -event instance passed to the binded functions - mc_type. This is one of the -event type constants defined in this module (such as MC_KEYPRESS). -For Key/Button events (which are handled by MultiCall and may receive -modifiers), another member is added - mc_state. This member gives the state -of the recognized modifiers, as a combination of the modifier constants -also defined in this module (for example, MC_SHIFT). -Using these members is absolutely portable. - -The order by which events are called is defined by these rules: -1. A more-specific event will be called before a less-specific event. -2. A recently-binded event will be called before a previously-binded event, - unless this conflicts with the first rule. -Each function will be called at most once for each event. -""" - -import sys -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; -MC_ACTIVATE=4; MC_CIRCULATE=5; MC_COLORMAP=6; MC_CONFIGURE=7; -MC_DEACTIVATE=8; MC_DESTROY=9; MC_ENTER=10; MC_EXPOSE=11; MC_FOCUSIN=12; -MC_FOCUSOUT=13; MC_GRAVITY=14; MC_LEAVE=15; MC_MAP=16; MC_MOTION=17; -MC_MOUSEWHEEL=18; MC_PROPERTY=19; MC_REPARENT=20; MC_UNMAP=21; MC_VISIBILITY=22; -# the modifier state constants, which define the meaning of mc_state -MC_SHIFT = 1<<0; MC_CONTROL = 1<<2; MC_ALT = 1<<3; MC_META = 1<<5 -MC_OPTION = 1<<6; MC_COMMAND = 1<<7 - -# define the list of modifiers, to be used in complex event types. -if sys.platform == "darwin": - _modifiers = (("Shift",), ("Control",), ("Option",), ("Command",)) - _modifier_masks = (MC_SHIFT, MC_CONTROL, MC_OPTION, MC_COMMAND) -else: - _modifiers = (("Control",), ("Alt",), ("Shift",), ("Meta", "M")) - _modifier_masks = (MC_CONTROL, MC_ALT, MC_SHIFT, MC_META) - -# a dictionary to map a modifier name into its number -_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: -# _SimpleBinder handles event types with no modifiers and no detail. -# No Python functions are called when no events are binded. -# _ComplexBinder handles event types with modifiers and a detail. -# A Python function is called each time an event is generated. - -class _SimpleBinder: - def __init__(self, type, widget, widgetinst): - self.type = type - self.sequence = '<'+_types[type][0]+'>' - self.widget = widget - self.widgetinst = widgetinst - self.bindedfuncs = [] - self.handlerid = None - - def bind(self, triplet, func): - if not self.handlerid: - def handler(event, l = self.bindedfuncs, mc_type = self.type): - event.mc_type = mc_type - wascalled = {} - for i in range(len(l)-1, -1, -1): - func = l[i] - if func not in wascalled: - wascalled[func] = True - r = func(event) - if r: - return r - self.handlerid = self.widget.bind(self.widgetinst, - self.sequence, handler) - self.bindedfuncs.append(func) - - def unbind(self, triplet, func): - self.bindedfuncs.remove(func) - if not self.bindedfuncs: - self.widget.unbind(self.widgetinst, self.sequence, self.handlerid) - self.handlerid = None - - 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 - -# An int in range(1 << len(_modifiers)) represents a combination of modifiers -# (if the least significent bit is on, _modifiers[0] is on, and so on). -# _state_subsets gives for each combination of modifiers, or *state*, -# a list of the states which are a subset of it. This list is ordered by the -# number of modifiers is the state - the most specific state comes first. -_states = range(1 << len(_modifiers)) -_state_names = [''.join(m[0]+'-' - for i, m in enumerate(_modifiers) - if (1 << i) & s) - for s in _states] - -def expand_substates(states): - '''For each item of states return a list containing all combinations of - that item with individual bits reset, sorted by the number of set bits. - ''' - def nbits(n): - "number of bits set in n base 2" - nb = 0 - while n: - n, rem = divmod(n, 2) - nb += rem - return nb - statelist = [] - for state in states: - substates = list(set(state & x for x in states)) - substates.sort(key=nbits, reverse=True) - statelist.append(substates) - return statelist - -_state_subsets = expand_substates(_states) - -# _state_codes gives for each state, the portable code to be passed as mc_state -_state_codes = [] -for s in _states: - r = 0 - for i in range(len(_modifiers)): - if (1 << i) & s: - r |= _modifier_masks[i] - _state_codes.append(r) - -class _ComplexBinder: - # This class binds many functions, and only unbinds them when it is deleted. - # self.handlerids is the list of seqs and ids of binded handler functions. - # The binded functions sit in a dictionary of lists of lists, which maps - # a detail (or None) and a state into a list of functions. - # When a new detail is discovered, handlers for all the possible states - # are binded. - - def __create_handler(self, lists, mc_type, mc_state): - def handler(event, lists = lists, - mc_type = mc_type, mc_state = mc_state, - ishandlerrunning = self.ishandlerrunning, - doafterhandler = self.doafterhandler): - ishandlerrunning[:] = [True] - event.mc_type = mc_type - event.mc_state = mc_state - wascalled = {} - r = None - for l in lists: - for i in range(len(l)-1, -1, -1): - func = l[i] - if func not in wascalled: - wascalled[func] = True - r = l[i](event) - if r: - break - if r: - break - ishandlerrunning[:] = [] - # Call all functions in doafterhandler and remove them from list - for f in doafterhandler: - f() - doafterhandler[:] = [] - if r: - return r - return handler - - def __init__(self, type, widget, widgetinst): - self.type = type - self.typename = _types[type][0] - self.widget = widget - self.widgetinst = widgetinst - self.bindedfuncs = {None: [[] for s in _states]} - self.handlerids = [] - # we don't want to change the lists of functions while a handler is - # running - it will mess up the loop and anyway, we usually want the - # change to happen from the next event. So we have a list of functions - # for the handler to run after it finishes calling the binded functions. - # It calls them only once. - # ishandlerrunning is a list. An empty one means no, otherwise - yes. - # this is done so that it would be mutable. - self.ishandlerrunning = [] - self.doafterhandler = [] - for s in _states: - lists = [self.bindedfuncs[None][i] for i in _state_subsets[s]] - handler = self.__create_handler(lists, type, _state_codes[s]) - seq = '<'+_state_names[s]+self.typename+'>' - self.handlerids.append((seq, self.widget.bind(self.widgetinst, - seq, handler))) - - def bind(self, triplet, func): - if triplet[2] not in self.bindedfuncs: - self.bindedfuncs[triplet[2]] = [[] for s in _states] - for s in _states: - lists = [ self.bindedfuncs[detail][i] - for detail in (triplet[2], None) - for i in _state_subsets[s] ] - handler = self.__create_handler(lists, self.type, - _state_codes[s]) - seq = "<%s%s-%s>"% (_state_names[s], self.typename, triplet[2]) - self.handlerids.append((seq, self.widget.bind(self.widgetinst, - seq, handler))) - doit = lambda: self.bindedfuncs[triplet[2]][triplet[0]].append(func) - if not self.ishandlerrunning: - doit() - else: - self.doafterhandler.append(doit) - - def unbind(self, triplet, func): - doit = lambda: self.bindedfuncs[triplet[2]][triplet[0]].remove(func) - if not self.ishandlerrunning: - doit() - else: - self.doafterhandler.append(doit) - - 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 - -# define the list of event types to be handled by MultiEvent. the order is -# compatible with the definition of event type constants. -_types = ( - ("KeyPress", "Key"), ("KeyRelease",), ("ButtonPress", "Button"), - ("ButtonRelease",), ("Activate",), ("Circulate",), ("Colormap",), - ("Configure",), ("Deactivate",), ("Destroy",), ("Enter",), ("Expose",), - ("FocusIn",), ("FocusOut",), ("Gravity",), ("Leave",), ("Map",), - ("Motion",), ("MouseWheel",), ("Property",), ("Reparent",), ("Unmap",), - ("Visibility",), -) - -# which binder should be used for every event type? -_binder_classes = (_ComplexBinder,) * 4 + (_SimpleBinder,) * (len(_types)-4) - -# A dictionary to map a type name into its number -_type_names = dict([(name, number) - for number in range(len(_types)) - for name in _types[number]]) - -_keysym_re = re.compile(r"^\w+$") -_button_re = re.compile(r"^[1-5]$") -def _parse_sequence(sequence): - """Get a string which should describe an event sequence. If it is - successfully parsed as one, return a tuple containing the state (as an int), - the event type (as an index of _types), and the detail - None if none, or a - string if there is one. If the parsing is unsuccessful, return None. - """ - if not sequence or sequence[0] != '<' or sequence[-1] != '>': - return None - words = sequence[1:-1].split('-') - 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 - else: - detail = None - else: - # _ComplexBinder - if type in [_type_names[s] for s in ("KeyPress", "KeyRelease")]: - type_re = _keysym_re - else: - type_re = _button_re - - if not words: - detail = None - elif len(words) == 1 and type_re.match(words[0]): - detail = words[0] - else: - return None - - return modifiers, type, detail - -def _triplet_to_sequence(triplet): - if triplet[2]: - return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'-'+ \ - triplet[2]+'>' - else: - return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'>' - -_multicall_dict = {} -def MultiCallCreator(widget): - """Return a MultiCall class which inherits its methods from the - given widget class (for example, Tkinter.Text). This is used - instead of a templating mechanism. - """ - if widget in _multicall_dict: - return _multicall_dict[widget] - - class MultiCall (widget): - assert issubclass(widget, tkinter.Misc) - - def __init__(self, *args, **kwargs): - widget.__init__(self, *args, **kwargs) - # a dictionary which maps a virtual event to a tuple with: - # 0. the function binded - # 1. a list of triplets - the sequences it is binded to - self.__eventinfo = {} - self.__binders = [_binder_classes[i](i, widget, self) - 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__) - if type(sequence) is str and len(sequence) > 2 and \ - sequence[:2] == "<<" and sequence[-2:] == ">>": - if sequence in self.__eventinfo: - ei = self.__eventinfo[sequence] - if ei[0] is not None: - for triplet in ei[1]: - self.__binders[triplet[1]].unbind(triplet, ei[0]) - ei[0] = func - if ei[0] is not None: - for triplet in ei[1]: - self.__binders[triplet[1]].bind(triplet, func) - else: - self.__eventinfo[sequence] = [func, []] - return widget.bind(self, sequence, func, add) - - def unbind(self, sequence, funcid=None): - if type(sequence) is str and len(sequence) > 2 and \ - sequence[:2] == "<<" and sequence[-2:] == ">>" and \ - sequence in self.__eventinfo: - func, triplets = self.__eventinfo[sequence] - if func is not None: - for triplet in triplets: - self.__binders[triplet[1]].unbind(triplet, func) - self.__eventinfo[sequence][0] = None - return widget.unbind(self, sequence, funcid) - - def event_add(self, virtual, *sequences): - #print("event_add(%s, %s)" % (repr(virtual), repr(sequences)), - # file=sys.__stderr__) - if virtual not in self.__eventinfo: - self.__eventinfo[virtual] = [None, []] - - func, triplets = self.__eventinfo[virtual] - for seq in sequences: - triplet = _parse_sequence(seq) - if triplet is None: - #print("Tkinter event_add(%s)" % seq, file=sys.__stderr__) - widget.event_add(self, virtual, seq) - else: - if func is not None: - self.__binders[triplet[1]].bind(triplet, func) - triplets.append(triplet) - - def event_delete(self, virtual, *sequences): - if virtual not in self.__eventinfo: - return - func, triplets = self.__eventinfo[virtual] - for seq in sequences: - triplet = _parse_sequence(seq) - if triplet is None: - #print("Tkinter event_delete: %s" % seq, file=sys.__stderr__) - widget.event_delete(self, virtual, seq) - else: - if func is not None: - self.__binders[triplet[1]].unbind(triplet, func) - triplets.remove(triplet) - - def event_info(self, virtual=None): - if virtual is None or virtual not in self.__eventinfo: - return widget.event_info(self, virtual) - else: - return tuple(map(_triplet_to_sequence, - self.__eventinfo[virtual][1])) + \ - widget.event_info(self, virtual) - - def __del__(self): - for virtual in self.__eventinfo: - 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 - - _multicall_dict[widget] = MultiCall - return MultiCall - - -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) - text.bind("<>"%n[0], handler) - text.event_add("<>"%n[0], seq) - n[0] += 1 - bindseq("") - bindseq("") - bindseq("") - bindseq("") - bindseq("") - bindseq("") - bindseq("") - bindseq("") - bindseq("") - bindseq("") - bindseq("") - bindseq("") - root.mainloop() - -if __name__ == "__main__": - from idlelib.idle_test.htest import run - run(_multi_call) diff --git a/Lib/idlelib/MultiStatusBar.py b/Lib/idlelib/MultiStatusBar.py deleted file mode 100644 index e82ba9a..0000000 --- a/Lib/idlelib/MultiStatusBar.py +++ /dev/null @@ -1,47 +0,0 @@ -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/ObjectBrowser.py b/Lib/idlelib/ObjectBrowser.py deleted file mode 100644 index 7b57aa4..0000000 --- a/Lib/idlelib/ObjectBrowser.py +++ /dev/null @@ -1,143 +0,0 @@ -# XXX TO DO: -# - popup menu -# - support partial or total redisplay -# - more doc strings -# - tooltips - -# object browser - -# XXX TO DO: -# - for classes/modules, add "open source" to object browser - -import re - -from idlelib.TreeWidget import TreeItem, TreeNode, ScrolledCanvas - -from reprlib import Repr - -myrepr = Repr() -myrepr.maxstring = 100 -myrepr.maxother = 100 - -class ObjectTreeItem(TreeItem): - def __init__(self, labeltext, object, setfunction=None): - self.labeltext = labeltext - self.object = object - self.setfunction = setfunction - def GetLabelText(self): - return self.labeltext - def GetText(self): - return myrepr.repr(self.object) - def GetIconName(self): - if not self.IsExpandable(): - return "python" - def IsEditable(self): - return self.setfunction is not None - def SetText(self, text): - try: - value = eval(text) - self.setfunction(value) - except: - pass - else: - self.object = value - def IsExpandable(self): - return not not dir(self.object) - def GetSubList(self): - keys = dir(self.object) - sublist = [] - for key in keys: - try: - value = getattr(self.object, key) - except AttributeError: - continue - item = make_objecttreeitem( - str(key) + " =", - value, - lambda value, key=key, object=self.object: - setattr(object, key, value)) - sublist.append(item) - return sublist - -class ClassTreeItem(ObjectTreeItem): - def IsExpandable(self): - return True - def GetSubList(self): - sublist = ObjectTreeItem.GetSubList(self) - if len(self.object.__bases__) == 1: - item = make_objecttreeitem("__bases__[0] =", - self.object.__bases__[0]) - else: - item = make_objecttreeitem("__bases__ =", self.object.__bases__) - sublist.insert(0, item) - return sublist - -class AtomicObjectTreeItem(ObjectTreeItem): - def IsExpandable(self): - return 0 - -class SequenceTreeItem(ObjectTreeItem): - def IsExpandable(self): - return len(self.object) > 0 - def keys(self): - return range(len(self.object)) - def GetSubList(self): - sublist = [] - for key in self.keys(): - try: - value = self.object[key] - except KeyError: - continue - def setfunction(value, key=key, object=self.object): - object[key] = value - item = make_objecttreeitem("%r:" % (key,), value, setfunction) - sublist.append(item) - return sublist - -class DictTreeItem(SequenceTreeItem): - def keys(self): - keys = list(self.object.keys()) - try: - keys.sort() - except: - pass - return keys - -dispatch = { - int: AtomicObjectTreeItem, - float: AtomicObjectTreeItem, - str: AtomicObjectTreeItem, - tuple: SequenceTreeItem, - list: SequenceTreeItem, - dict: DictTreeItem, - type: ClassTreeItem, -} - -def make_objecttreeitem(labeltext, object, setfunction=None): - t = type(object) - if t in dispatch: - c = dispatch[t] - else: - c = ObjectTreeItem - return c(labeltext, object, setfunction) - - -def _object_browser(parent): - import sys - from tkinter import Tk - root = Tk() - root.title("Test ObjectBrowser") - width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) - root.geometry("+%d+%d"%(x, y + 150)) - root.configure(bd=0, bg="yellow") - root.focus_set() - sc = ScrolledCanvas(root, bg="white", highlightthickness=0, takefocus=1) - 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 idlelib.idle_test.htest import run - run(_object_browser) diff --git a/Lib/idlelib/OutputWindow.py b/Lib/idlelib/OutputWindow.py deleted file mode 100644 index e614f9b..0000000 --- a/Lib/idlelib/OutputWindow.py +++ /dev/null @@ -1,144 +0,0 @@ -from tkinter import * -from idlelib.EditorWindow import EditorWindow -import re -import tkinter.messagebox as 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("<>", 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"): - if isinstance(s, (bytes, bytes)): - s = s.decode(IOBinding.encoding, "replace") - self.text.insert(mark, s, tags) - self.text.see(mark) - self.text.update() - return len(s) - - def writelines(self, lines): - for line in lines: - self.write(line) - - def flush(self): - pass - - # Our own right-button menu - - rmenu_specs = [ - ("Cut", "<>", "rmenu_check_cut"), - ("Copy", "<>", "rmenu_check_copy"), - ("Paste", "<>", "rmenu_check_paste"), - (None, None, None), - ("Go to 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 OSError: - 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 deleted file mode 100644 index 19bad8c..0000000 --- a/Lib/idlelib/ParenMatch.py +++ /dev/null @@ -1,178 +0,0 @@ -"""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.configHandler import idleConf - -_openers = {')':'(',']':'[','}':'{'} -CHECK_DELAY = 100 # miliseconds - -class ParenMatch: - """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. - - The supported styles are: - - default -- When a right paren is typed, highlight the matching - left paren for 1/2 sec. - - expression -- When a right paren is typed, highlight the entire - expression from the left paren to the right paren. - - TODO: - - 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", "<>"), - ]) - ] - 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 = "<>" - # We want the restore event be called before the usual return and - # backspace events. - RESTORE_SEQUENCES = ("", "", - "", "") - - def __init__(self, editwin): - self.editwin = editwin - self.text = editwin.text - # Bind the check-restore event to the function restore_event, - # so that we can then use activate_restore (which calls event_add) - # and deactivate_restore (which calls event_delete). - editwin.text.bind(self.RESTORE_VIRTUAL_EVENT_NAME, - self.restore_event) - self.counter = 0 - self.is_restore_active = 0 - self.set_style(self.STYLE) - - def activate_restore(self): - 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): - 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): - indices = (HyperParser(self.editwin, "insert") - .get_surrounding_brackets()) - 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): - # If it was a shortcut and not really a closing paren, quit. - closer = self.text.get("insert-1c") - if closer not in _openers: - return - hp = HyperParser(self.editwin, "insert-1c") - if not hp.is_in_code(): - return - indices = hp.get_surrounding_brackets(_openers[closer], True) - if indices is None: - self.warn_mismatched() - return - self.activate_restore() - self.create_tag(indices) - self.set_timeout() - - def restore_event(self, event=None): - self.text.tag_delete("paren") - self.deactivate_restore() - self.counter += 1 # disable the last timer, if there is one. - - def handle_restore_timer(self, timer_count): - 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_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_expression(self, indices): - """Highlight the entire expression""" - if self.text.get(indices[1]) in (')', ']', '}'): - rightindex = indices[1]+"+1c" - else: - rightindex = indices[1] - self.text.tag_add("paren", indices[0], rightindex) - self.text.tag_config("paren", self.HILITE_CONFIG) - - # any one of the set_timeout_XXX methods can be used depending on - # the style - - def set_timeout_none(self): - """Highlight will remain until user input turns it off - or the insert has moved""" - # After CHECK_DELAY, call a function which disables the "paren" tag - # if the event is for the most recent timer and the insert has changed, - # or schedules another call for itself. - self.counter += 1 - def callme(callme, self=self, c=self.counter, - index=self.text.index("insert")): - if index != self.text.index("insert"): - self.handle_restore_timer(c) - else: - self.editwin.text_frame.after(CHECK_DELAY, callme, callme) - self.editwin.text_frame.after(CHECK_DELAY, callme, callme) - - def set_timeout_last(self): - """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 - self.editwin.text_frame.after( - self.FLASH_DELAY, - lambda self=self, c=self.counter: self.handle_restore_timer(c)) - - -if __name__ == '__main__': - import unittest - unittest.main('idlelib.idle_test.test_parenmatch', verbosity=2) diff --git a/Lib/idlelib/PathBrowser.py b/Lib/idlelib/PathBrowser.py deleted file mode 100644 index 9ab7632..0000000 --- a/Lib/idlelib/PathBrowser.py +++ /dev/null @@ -1,108 +0,0 @@ -import os -import sys -import importlib.machinery - -from idlelib.TreeWidget import TreeItem -from idlelib.ClassBrowser import ClassBrowser, ModuleBrowserTreeItem -from idlelib.PyShell import PyShellFileList - - -class PathBrowser(ClassBrowser): - - def __init__(self, flist, _htest=False): - """ - _htest - bool, change box location when running htest - """ - self._htest = _htest - self.init(flist) - - def settitle(self): - "Set window titles." - self.top.wm_title("Path Browser") - self.top.wm_iconname("Path Browser") - - def rootnode(self): - return PathBrowserTreeItem() - -class PathBrowserTreeItem(TreeItem): - - def GetText(self): - return "sys.path" - - def GetSubList(self): - sublist = [] - for dir in sys.path: - item = DirBrowserTreeItem(dir) - sublist.append(item) - return sublist - -class DirBrowserTreeItem(TreeItem): - - def __init__(self, dir, packages=[]): - self.dir = dir - self.packages = packages - - def GetText(self): - if not self.packages: - return self.dir - else: - return self.packages[-1] + ": package" - - def GetSubList(self): - try: - names = os.listdir(self.dir or os.curdir) - except OSError: - return [] - packages = [] - for name in names: - file = os.path.join(self.dir, name) - if self.ispackagedir(file): - nn = os.path.normcase(name) - packages.append((nn, name, file)) - packages.sort() - sublist = [] - for nn, name, file in packages: - item = DirBrowserTreeItem(file, self.packages + [name]) - sublist.append(item) - for nn, name in self.listmodules(names): - item = ModuleBrowserTreeItem(os.path.join(self.dir, name)) - sublist.append(item) - 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") - return os.path.exists(init) - - def listmodules(self, allnames): - modules = {} - suffixes = importlib.machinery.EXTENSION_SUFFIXES[:] - suffixes += importlib.machinery.SOURCE_SUFFIXES - suffixes += importlib.machinery.BYTECODE_SUFFIXES - sorted = [] - for suff in suffixes: - i = -len(suff) - for name in allnames[:]: - normed_name = os.path.normcase(name) - if normed_name[i:] == suff: - mod_name = name[:i] - if mod_name not in modules: - modules[mod_name] = None - sorted.append((normed_name, name)) - allnames.remove(name) - sorted.sort() - return sorted - -def _path_browser(parent): # htest # - flist = PyShellFileList(parent) - PathBrowser(flist, _htest=True) - parent.mainloop() - -if __name__ == "__main__": - from unittest import main - main('idlelib.idle_test.test_pathbrowser', verbosity=2, exit=False) - - from idlelib.idle_test.htest import run - run(_path_browser) diff --git a/Lib/idlelib/Percolator.py b/Lib/idlelib/Percolator.py deleted file mode 100644 index b8be2aa..0000000 --- a/Lib/idlelib/Percolator.py +++ /dev/null @@ -1,105 +0,0 @@ -from idlelib.WidgetRedirector import WidgetRedirector -from idlelib.Delegator import Delegator - - -class Percolator: - - def __init__(self, text): - # XXX would be nice to inherit from Delegator - self.text = text - self.redir = WidgetRedirector(text) - self.top = self.bottom = Delegator(text) - self.bottom.insert = self.redir.register("insert", self.insert) - self.bottom.delete = self.redir.register("delete", self.delete) - self.filters = [] - - def close(self): - 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.text = None - - def insert(self, index, chars, tags=None): - # Could go away if inheriting from Delegator - self.top.insert(index, chars, tags) - - def delete(self, index1, index2=None): - # Could go away if inheriting from Delegator - self.top.delete(index1, index2) - - def insertfilter(self, filter): - # Perhaps rename to pushfilter()? - assert isinstance(filter, Delegator) - assert filter.delegate is None - filter.setdelegate(self.top) - self.top = filter - - def removefilter(self, filter): - # XXX Perhaps should only support popfilter()? - assert isinstance(filter, Delegator) - assert filter.delegate is not None - f = self.top - if f is filter: - self.top = filter.delegate - filter.setdelegate(None) - else: - while f.delegate is not filter: - assert f is not self.bottom - f.resetcache() - f = f.delegate - f.setdelegate(filter.delegate) - filter.setdelegate(None) - - -def _percolator(parent): # htest # - 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) - self.delegate.insert(*args) - - def delete(self, *args): - print(self.name, ": delete", args) - self.delegate.delete(*args) - - box = tk.Toplevel(parent) - box.title("Test Percolator") - width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) - box.geometry("+%d+%d" % (x, y + 150)) - text = tk.Text(box) - p = Percolator(text) - pin = p.insertfilter - pout = p.removefilter - t1 = Tracer("t1") - t2 = Tracer("t2") - - def toggle1(): - (pin if var1.get() else pout)(t1) - def toggle2(): - (pin if var2.get() else pout)(t2) - - text.pack() - var1 = tk.IntVar() - cb1 = tk.Checkbutton(box, text="Tracer1", command=toggle1, variable=var1) - cb1.pack() - var2 = tk.IntVar() - cb2 = tk.Checkbutton(box, text="Tracer2", command=toggle2, variable=var2) - cb2.pack() - -if __name__ == "__main__": - import unittest - unittest.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 deleted file mode 100644 index 9ccbb25..0000000 --- a/Lib/idlelib/PyParse.py +++ /dev/null @@ -1,617 +0,0 @@ -import re -import sys -from collections import Mapping - -# Reason last stmt is continued (or C_NONE if it's not). -(C_NONE, C_BACKSLASH, C_STRING_FIRST_LINE, - C_STRING_NEXT_LINES, C_BRACKET) = range(5) - -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""" - ^ - [ \t]* - (?: while - | else - | def - | return - | assert - | break - | class - | continue - | elif - | try - | except - | raise - | import - | yield - ) - \b -""", re.VERBOSE | re.MULTILINE).search - -# Match blank line or non-indenting comment line. - -_junkre = re.compile(r""" - [ \t]* - (?: \# \S .* )? - \n -""", re.VERBOSE).match - -# Match any flavor of string; the terminating quote is optional -# so that we're robust in the face of incomplete program text. - -_match_stringre = re.compile(r""" - \""" [^"\\]* (?: - (?: \\. | "(?!"") ) - [^"\\]* - )* - (?: \""" )? - -| " [^"\\\n]* (?: \\. [^"\\\n]* )* "? - -| ''' [^'\\]* (?: - (?: \\. | '(?!'') ) - [^'\\]* - )* - (?: ''' )? - -| ' [^'\\\n]* (?: \\. [^'\\\n]* )* '? -""", re.VERBOSE | re.DOTALL).match - -# Match a line that starts with something interesting; -# used to find the first item of a bracket structure. - -_itemre = re.compile(r""" - [ \t]* - [^\s#\\] # if we match, m.end()-1 is the interesting char -""", re.VERBOSE).match - -# Match start of stmts that should be followed by a dedent. - -_closere = re.compile(r""" - \s* - (?: return - | break - | continue - | raise - | pass - ) - \b -""", re.VERBOSE).match - -# Chew up non-special chars as quickly as possible. If match is -# successful, m.end() less 1 is the index of the last boring char -# matched. If match is unsuccessful, the string starts with an -# interesting char. - -_chew_ordinaryre = re.compile(r""" - [^[\](){}#'"\\]+ -""", re.VERBOSE).match - - -class StringTranslatePseudoMapping(Mapping): - r"""Utility class to be used with str.translate() - - This Mapping class wraps a given dict. When a value for a key is - requested via __getitem__() or get(), the key is looked up in the - given dict. If found there, the value from the dict is returned. - Otherwise, the default value given upon initialization is returned. - - This allows using str.translate() to make some replacements, and to - replace all characters for which no replacement was specified with - a given character instead of leaving them as-is. - - For example, to replace everything except whitespace with 'x': - - >>> whitespace_chars = ' \t\n\r' - >>> preserve_dict = {ord(c): ord(c) for c in whitespace_chars} - >>> mapping = StringTranslatePseudoMapping(preserve_dict, ord('x')) - >>> text = "a + b\tc\nd" - >>> text.translate(mapping) - 'x x x\tx\nx' - """ - def __init__(self, non_defaults, default_value): - self._non_defaults = non_defaults - self._default_value = default_value - - def _get(key, _get=non_defaults.get, _default=default_value): - return _get(key, _default) - self._get = _get - - def __getitem__(self, item): - return self._get(item) - - def __len__(self): - return len(self._non_defaults) - - def __iter__(self): - return iter(self._non_defaults) - - def get(self, key, default=None): - return self._get(key) - - -class Parser: - - def __init__(self, indentwidth, tabwidth): - self.indentwidth = indentwidth - self.tabwidth = tabwidth - - def set_str(self, s): - assert len(s) == 0 or s[-1] == '\n' - self.str = s - 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): - str, pos = self.str, None - - if not is_char_in_string: - # no clue -- make the caller pass everything - return None - - # 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(str) - for tries in range(5): - i = str.rfind(":\n", 0, limit) - if i < 0: - break - 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 - limit = i - if pos is None: - # Nothing looks like a block-opener, or stuff does - # but is_char_in_string keeps returning true; most likely - # we're in or near a giant string, the colorizer hasn't - # caught up enough to be helpful, or there simply *aren't* - # any interesting stmts. In any of these cases we're - # 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(str) - if m and not is_char_in_string(m.start()): - pos = m.start() - return pos - - # Peeking back worked; look forward until _synchre no longer - # matches. - i = pos + 1 - while 1: - m = _synchre(str, i) - if m: - s, i = m.span() - if not is_char_in_string(s): - pos = s - else: - break - return pos - - # Throw away the start of the string. Intended to be called with - # find_good_parse_start's result. - - def set_lo(self, lo): - assert lo == 0 or self.str[lo-1] == '\n' - if lo > 0: - self.str = self.str[lo:] - - # Build a translation table to map uninteresting chars to 'x', open - # brackets to '(', close brackets to ')' while preserving quotes, - # backslashes, newlines and hashes. This is to be passed to - # str.translate() in _study1(). - _tran = {} - _tran.update((ord(c), ord('(')) for c in "({[") - _tran.update((ord(c), ord(')')) for c in ")}]") - _tran.update((ord(c), ord(c)) for c in "\"'\\\n#") - _tran = StringTranslatePseudoMapping(_tran, default_value=ord('x')) - - # As quickly as humanly possible , 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 - - # Map all uninteresting characters to "x", all open brackets - # 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. - str = self.str - str = str.translate(self._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 - # whether & why the last stmt is a continuation. - continuation = C_NONE - level = lno = 0 # level is nesting level; lno is line number - self.goodlines = goodlines = [0] - push_good = goodlines.append - i, n = 0, len(str) - while i < n: - ch = str[i] - i = i+1 - - # cases are checked in decreasing order of frequency - if ch == 'x': - continue - - if ch == '\n': - lno = lno + 1 - if level == 0: - push_good(lno) - # else we're in an unclosed bracket structure - continue - - if ch == '(': - level = level + 1 - continue - - if ch == ')': - if level: - level = level - 1 - # else the program is invalid, but we can't complain - continue - - if ch == '"' or ch == "'": - # consume the string - quote = ch - if str[i-1:i+2] == quote * 3: - quote = quote * 3 - firstlno = lno - w = len(quote) - 1 - i = i+w - while i < n: - ch = str[i] - i = i+1 - - if ch == 'x': - continue - - if str[i-1:i+w] == quote: - i = i+w - break - - if ch == '\n': - lno = lno + 1 - if w == 0: - # unterminated single-quoted string - if level == 0: - push_good(lno) - break - continue - - if ch == '\\': - assert i < n - if str[i] == '\n': - lno = lno + 1 - i = i+1 - continue - - # else comment char or paren inside string - - else: - # didn't break out of the loop, so we're still - # inside a string - if (lno - 1) == firstlno: - # before the previous \n in str, we were in the first - # line of the string - continuation = C_STRING_FIRST_LINE - else: - continuation = C_STRING_NEXT_LINES - continue # with outer loop - - if ch == '#': - # consume the comment - i = str.find('\n', i) - assert i >= 0 - continue - - assert ch == '\\' - assert i < n - if str[i] == '\n': - lno = lno + 1 - if i+1 == n: - continuation = C_BACKSLASH - i = i+1 - - # The last stmt may be continued for all 3 reasons. - # String continuation takes precedence over bracket - # continuation, which beats backslash continuation. - if (continuation != C_STRING_FIRST_LINE - and continuation != C_STRING_NEXT_LINES and level > 0): - continuation = C_BRACKET - self.continuation = continuation - - # Push the final line number as a sentinel value, regardless of - # whether it's continued. - assert (continuation == C_NONE) == (goodlines[-1] == lno) - if goodlines[-1] != lno: - push_good(lno) - - def get_continuation_type(self): - 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): - if self.study_level >= 2: - return - self._study1() - self.study_level = 2 - - # Set p and q to slice indices of last interesting stmt. - str, goodlines = self.str, self.goodlines - i = len(goodlines) - 1 - p = len(str) # index of newest line - while i: - assert p - # 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 = 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(str, p): - i = i-1 - else: - break - if i == 0: - # nothing but junk! - assert p == 0 - q = p - self.stmt_start, self.stmt_end = p, q - - # Analyze this stmt, to find the last open bracket (if any) - # and last interesting character (if any). - lastch = "" - stack = [] # stack of open bracket indices - push_stack = stack.append - bracketing = [(p, 0)] - while p < q: - # suck up all except ()[]{}'"#\\ - 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 str[i] in " \t\n": - i = i-1 - if i >= p: - lastch = str[i] - p = newp - if p >= q: - break - - ch = str[p] - - if ch in "([{": - push_stack(p) - bracketing.append((p, len(stack))) - lastch = ch - p = p+1 - continue - - if ch in ")]}": - if stack: - del stack[-1] - lastch = ch - p = p+1 - bracketing.append((p, len(stack))) - continue - - if ch == '"' or ch == "'": - # consume string - # Note that study1 did this with a Python loop, but - # we use a regexp here; the reason is speed in both - # cases; the string may be huge, but study1 pre-squashed - # strings to a couple of characters per line. study1 - # also needed to keep track of newlines, and we don't - # have to. - bracketing.append((p, len(stack)+1)) - lastch = ch - 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 = str.find('\n', p, q) + 1 - assert p > 0 - bracketing.append((p, len(stack))) - continue - - assert ch == '\\' - p = p+1 # beyond backslash - assert p < q - if str[p] != '\n': - # the program is invalid, but can't complain - lastch = ch + str[p] - p = p+1 # beyond escaped char - - # end while p < q: - - self.lastch = lastch - if stack: - self.lastopenbracketpos = stack[-1] - self.stmt_bracketing = tuple(bracketing) - - # Assuming continuation is C_BRACKET, return the number - # of spaces the next line should be indented. - - def compute_bracket_indent(self): - self._study2() - assert self.continuation == C_BRACKET - j = self.lastopenbracketpos - 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(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 = str.find('\n', j) + 1 - else: - # nothing interesting follows the bracket; - # reproduce the bracket line's indentation + a level - j = i = origi - while str[j] in " \t": - j = j+1 - extra = self.indentwidth - return len(str[i:j].expandtabs(self.tabwidth)) + extra - - # 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). - - def get_num_lines_in_stmt(self): - self._study1() - goodlines = self.goodlines - return goodlines[-1] - goodlines[-2] - - # 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. - - def compute_backslash_indent(self): - self._study2() - assert self.continuation == C_BACKSLASH - str = self.str - i = self.stmt_start - 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 = str.find('\n', startpos) + 1 - found = level = 0 - while i < endpos: - ch = str[i] - if ch in "([{": - level = level + 1 - i = i+1 - elif ch in ")]}": - if level: - level = level - 1 - i = i+1 - elif ch == '"' or ch == "'": - i = _match_stringre(str, i, endpos).end() - elif ch == '#': - break - elif level == 0 and ch == '=' and \ - (i == 0 or str[i-1] not in "=<>!") and \ - str[i+1] != '=': - found = 1 - break - else: - i = i+1 - - if found: - # 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*\\", str[i:endpos]) is None - - if not found: - # oh well ... settle for moving beyond the first chunk - # of non-whitespace chars - i = startpos - while str[i] not in " \t\n": - i = i+1 - - 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): - self._study2() - i, n = self.stmt_start, self.stmt_end - j = i - str = self.str - while j < n and str[j] in " \t": - j = j + 1 - return str[i:j] - - # Did the last interesting stmt open a block? - - def is_block_opener(self): - self._study2() - return self.lastch == ':' - - # Did the last interesting stmt close a block? - - def is_block_closer(self): - self._study2() - return _closere(self.str, self.stmt_start) is not None - - # index of last open bracket ({[, or None if none - lastopenbracketpos = None - - def get_last_open_bracket_pos(self): - self._study2() - 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 - - def get_last_stmt_bracketing(self): - self._study2() - return self.stmt_bracketing diff --git a/Lib/idlelib/PyShell.py b/Lib/idlelib/PyShell.py deleted file mode 100755 index 1bcc9b6..0000000 --- a/Lib/idlelib/PyShell.py +++ /dev/null @@ -1,1619 +0,0 @@ -#! /usr/bin/env python3 - -import getopt -import os -import os.path -import re -import socket -import subprocess -import sys -import threading -import time -import tokenize -import io - -import linecache -from code import InteractiveInterpreter -from platform import python_version, system - -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 tkinter.messagebox as 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 import Debugger -from idlelib import RemoteDebugger -from idlelib import macosxSupport - -HOST = '127.0.0.1' # python execution server on localhost loopback -PORT = 0 # someday pass in host, port for remote debug capability - -# 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): - """Show Idle-format warning (after replacing warnings.showwarning). - - The differences are the formatter called, the file=None replacement, - which can be None, the capture of the consequence AttributeError, - and the output of a hard-coded prompt. - """ - if file is None: - file = warning_stream - try: - file.write(idle_formatwarning( - message, category, filename, lineno, line=line)) - file.write(">>> ") - except (AttributeError, OSError): - pass # if file (probably __stderr__) is invalid, skip warning. - -_warnings_showwarning = None - -def capture_warnings(capture): - "Replace warning.showwarning with idle_showwarning, or reverse." - - global _warnings_showwarning - if capture: - if _warnings_showwarning is None: - _warnings_showwarning = warnings.showwarning - warnings.showwarning = idle_showwarning - else: - if _warnings_showwarning is not None: - warnings.showwarning = _warnings_showwarning - _warnings_showwarning = None - -capture_warnings(True) - -def extended_linecache_checkcache(filename=None, - orig_checkcache=linecache.checkcache): - """Extend linecache.checkcache to preserve the entries - - Rather than repeating the linecache code, patch it to save the - entries, call the original linecache.checkcache() - (skipping them), and then restore the saved entries. - - orig_checkcache is bound at definition time to the original - method, allowing it to be patched. - """ - cache = linecache.cache - save = {} - for key in list(cache): - if key[:1] + key[-1:] == '<>': - save[key] = cache.pop(key) - orig_checkcache(filename) - cache.update(save) - -# Patch linecache.checkcache(): -linecache.checkcache = extended_linecache_checkcache - - -class PyShellEditorWindow(EditorWindow): - "Regular text edit window in IDLE, supports breakpoints" - - def __init__(self, *args): - self.breakpoints = [] - EditorWindow.__init__(self, *args) - self.text.bind("<>", self.set_breakpoint_here) - self.text.bind("<>", self.clear_breakpoint_here) - self.text.bind("<>", self.flist.open_shell) - - 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): - self.restore_file_breaks() - old_hook() - self.io.set_filename_change_hook(filename_changed_hook) - if self.io.filename: - self.restore_file_breaks() - self.color_breakpoint_text() - - rmenu_specs = [ - ("Cut", "<>", "rmenu_check_cut"), - ("Copy", "<>", "rmenu_check_copy"), - ("Paste", "<>", "rmenu_check_paste"), - (None, None, None), - ("Set Breakpoint", "<>", None), - ("Clear Breakpoint", "<>", None) - ] - - def color_breakpoint_text(self, color=True): - "Turn colorizing of breakpoint text on or off" - if self.io is None: - # possible due to update in restore_file_breaks - return - if color: - theme = idleConf.CurrentTheme() - cfg = idleConf.GetHighlight(theme, "break") - else: - cfg = {'foreground': '', 'background': ''} - self.text.tag_config('BREAK', cfg) - - def set_breakpoint(self, lineno): - text = self.text - filename = self.io.filename - text.tag_add("BREAK", "%d.0" % lineno, "%d.0" % (lineno+1)) - try: - self.breakpoints.index(lineno) - except ValueError: # only add if missing, i.e. do once - self.breakpoints.append(lineno) - try: # update the subprocess debugger - debug = self.flist.pyshell.interp.debugger - debug.set_breakpoint_here(filename, lineno) - except: # but debugger may not be active right now.... - pass - - def set_breakpoint_here(self, event=None): - text = self.text - filename = self.io.filename - if not filename: - text.bell() - return - lineno = int(float(text.index("insert"))) - self.set_breakpoint(lineno) - - def clear_breakpoint_here(self, event=None): - text = self.text - filename = self.io.filename - if not filename: - text.bell() - return - lineno = int(float(text.index("insert"))) - try: - self.breakpoints.remove(lineno) - except: - pass - text.tag_remove("BREAK", "insert linestart",\ - "insert lineend +1char") - try: - debug = self.flist.pyshell.interp.debugger - debug.clear_breakpoint_here(filename, lineno) - except: - pass - - def clear_file_breaks(self): - if self.breakpoints: - text = self.text - filename = self.io.filename - if not filename: - text.bell() - return - self.breakpoints = [] - text.tag_remove("BREAK", "1.0", END) - try: - debug = self.flist.pyshell.interp.debugger - debug.clear_file_breaks(filename) - except: - pass - - def store_file_breaks(self): - "Save breakpoints when file is saved" - # XXX 13 Dec 2002 KBK Currently the file must be saved before it can - # be run. The breaks are saved at that time. If we introduce - # a temporary file save feature the save breaks functionality - # needs to be re-verified, since the breaks at the time the - # temp file is created may differ from the breaks at the last - # permanent save of the file. Currently, a break introduced - # after a save will be effective, but not persistent. - # This is necessary to keep the saved breaks synched with the - # saved file. - # - # Breakpoints are set as tagged ranges in the text. - # Since a modified file has to be saved before it is - # run, and since self.breakpoints (from which the subprocess - # debugger is loaded) is updated during the save, the visible - # breaks stay synched with the subprocess even if one of these - # unexpected breakpoint deletions occurs. - breaks = self.breakpoints - filename = self.io.filename - try: - with open(self.breakpointPath, "r") as fp: - lines = fp.readlines() - except OSError: - lines = [] - try: - with open(self.breakpointPath, "w") as new_file: - for line in lines: - if not line.startswith(filename + '='): - new_file.write(line) - self.update_breakpoints() - breaks = self.breakpoints - if breaks: - new_file.write(filename + '=' + str(breaks) + '\n') - except OSError as err: - if not getattr(self.root, "breakpoint_error_displayed", False): - self.root.breakpoint_error_displayed = True - tkMessageBox.showerror(title='IDLE Error', - message='Unable to update breakpoint list:\n%s' - % str(err), - parent=self.text) - - def restore_file_breaks(self): - self.text.update() # this enables setting "BREAK" tags to be visible - if self.io is None: - # can happen if IDLE closes due to the .update() call - return - filename = self.io.filename - if filename is None: - return - if os.path.isfile(self.breakpointPath): - with open(self.breakpointPath, "r") as fp: - lines = fp.readlines() - for line in lines: - if line.startswith(filename + '='): - breakpoint_linenumbers = eval(line[len(filename)+1:]) - for breakpoint_linenumber in breakpoint_linenumbers: - self.set_breakpoint(breakpoint_linenumber) - - def update_breakpoints(self): - "Retrieves all the breakpoints in the current window" - text = self.text - ranges = text.tag_ranges("BREAK") - linenumber_list = self.ranges_to_linenumbers(ranges) - self.breakpoints = linenumber_list - - def ranges_to_linenumbers(self, ranges): - lines = [] - for index in range(0, len(ranges), 2): - lineno = int(float(ranges[index].string)) - end = int(float(ranges[index+1].string)) - while lineno < end: - lines.append(lineno) - lineno += 1 - return lines - -# XXX 13 Dec 2002 KBK Not used currently -# def saved_change_hook(self): -# "Extend base method - clear breaks if module is modified" -# if not self.get_saved(): -# self.clear_file_breaks() -# EditorWindow.saved_change_hook(self) - - def _close(self): - "Extend base method - clear breaks when module is closed" - self.clear_file_breaks() - EditorWindow._close(self) - - -class PyShellFileList(FileList): - "Extend base class: IDLE supports a shell and breakpoints" - - # override FileList's class variable, instances return PyShellEditorWindow - # instead of EditorWindow when new edit windows are created. - EditorWindow = PyShellEditorWindow - - pyshell = None - - def open_shell(self, event=None): - if self.pyshell: - self.pyshell.top.wakeup() - else: - self.pyshell = PyShell(self) - if self.pyshell: - if not self.pyshell.begin(): - return None - return self.pyshell - - -class ModifiedColorDelegator(ColorDelegator): - "Extend base class: colorizer for the shell window itself" - - def __init__(self): - ColorDelegator.__init__(self) - self.LoadTagDefs() - - def recolorize_main(self): - self.tag_remove("TODO", "1.0", "iomark") - self.tag_add("SYNC", "1.0", "iomark") - ColorDelegator.recolorize_main(self) - - def LoadTagDefs(self): - ColorDelegator.LoadTagDefs(self) - theme = idleConf.CurrentTheme() - self.tagdefs.update({ - "stdin": {'background':None,'foreground':None}, - "stdout": idleConf.GetHighlight(theme, "stdout"), - "stderr": idleConf.GetHighlight(theme, "stderr"), - "console": idleConf.GetHighlight(theme, "console"), - }) - - def removecolors(self): - # Don't remove shell color tags before "iomark" - for tag in self.tagdefs: - self.tag_remove(tag, "iomark", "end") - -class ModifiedUndoDelegator(UndoDelegator): - "Extend base class: forbid insert/delete before the I/O mark" - - def insert(self, index, chars, tags=None): - try: - if self.delegate.compare(index, "<", "iomark"): - self.delegate.bell() - return - except TclError: - pass - UndoDelegator.insert(self, index, chars, tags) - - def delete(self, index1, index2=None): - try: - if self.delegate.compare(index1, "<", "iomark"): - self.delegate.bell() - return - except TclError: - pass - UndoDelegator.delete(self, index1, index2) - - -class MyRPCClient(rpc.RPCClient): - - def handle_EOF(self): - "Override the base class - just re-raise EOFError" - raise EOFError - - -class ModifiedInterpreter(InteractiveInterpreter): - - def __init__(self, tkconsole): - 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 - self.original_compiler_flags = self.compile.compiler.flags - - _afterid = None - rpcclt = None - rpcsubproc = 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) - - 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] - # 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') - if __name__ == 'idlelib.PyShell': - command = "__import__('idlelib.run').run.main(%r)" % (del_exitf,) - else: - command = "__import__('run').main(%r)" % (del_exitf,) - return [sys.executable] + w + ["-c", command, str(self.port)] - - def start_subprocess(self): - addr = (HOST, self.port) - # GUI makes several attempts to acquire socket, listens for connection - for i in range(3): - time.sleep(i) - try: - self.rpcclt = MyRPCClient(addr) - break - except OSError: - pass - else: - self.display_port_binding_error() - return None - # if PORT was 0, system will assign an 'ephemeral' port. Find it out: - self.port = self.rpcclt.listening_sock.getsockname()[1] - # if PORT was not 0, probably working with a remote execution server - if PORT != 0: - # To allow reconnection within the 2MSL wait (cf. Stevens TCP - # V1, 18.6), set SO_REUSEADDR. Note that this can be problematic - # on Windows since the implementation allows two active sockets on - # the same address! - self.rpcclt.listening_sock.setsockopt(socket.SOL_SOCKET, - socket.SO_REUSEADDR, 1) - self.spawn_subprocess() - #time.sleep(20) # test to simulate GUI not accepting connection - # Accept the connection from the Python execution server - self.rpcclt.listening_sock.settimeout(10) - try: - self.rpcclt.accept() - except socket.timeout: - self.display_no_subprocess_error() - return None - self.rpcclt.register("console", self.tkconsole) - self.rpcclt.register("stdin", self.tkconsole.stdin) - self.rpcclt.register("stdout", self.tkconsole.stdout) - self.rpcclt.register("stderr", self.tkconsole.stderr) - self.rpcclt.register("flist", self.tkconsole.flist) - self.rpcclt.register("linecache", linecache) - self.rpcclt.register("interp", self) - self.transfer_path(with_cwd=True) - self.poll_subprocess() - return self.rpcclt - - def restart_subprocess(self, with_cwd=False, filename=''): - if self.restarting: - return self.rpcclt - self.restarting = True - # close only the subprocess debugger - debug = self.getdebugger() - if debug: - try: - # Only close subprocess debugger, don't unregister gui_adap! - RemoteDebugger.close_subprocess_debugger(self.rpcclt) - except: - pass - # Kill subprocess, spawn a new one, accept connection. - self.rpcclt.close() - self.terminate_subprocess() - console = self.tkconsole - was_executing = console.executing - console.executing = False - self.spawn_subprocess() - try: - self.rpcclt.accept() - except socket.timeout: - self.display_no_subprocess_error() - return None - self.transfer_path(with_cwd=with_cwd) - console.stop_readline() - # annotate restart in shell window and mark it - console.text.delete("iomark", "end-1c") - 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: - console.showprompt() - # restart subprocess debugger - if debug: - # Restarted debugger connects to current instance of debug GUI - RemoteDebugger.restart_subprocess_debugger(self.rpcclt) - # reload remote debugger breakpoints for all PyShellEditWindows - debug.load_breakpoints() - self.compile.compiler.flags = self.original_compiler_flags - self.restarting = False - return self.rpcclt - - def __request_interrupt(self): - self.rpcclt.remotecall("exec", "interrupt_the_server", (), {}) - - def interrupt_subprocess(self): - threading.Thread(target=self.__request_interrupt).start() - - def kill_subprocess(self): - 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.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: - try: - self.rpcsubproc.wait() - except OSError: - return - - def transfer_path(self, with_cwd=False): - if with_cwd: # Issue 13506 - path = [''] # include Current Working Directory - path.extend(sys.path) - else: - path = sys.path - - self.runcommand("""if 1: - import sys as _sys - _sys.path = %r - del _sys - \n""" % (path,)) - - active_seq = None - - def poll_subprocess(self): - clt = self.rpcclt - if clt is None: - return - try: - response = clt.pollresponse(self.active_seq, wait=0.05) - except (EOFError, OSError, KeyboardInterrupt): - # lost connection or subprocess terminated itself, restart - # [the KBI is from rpc.SocketIO.handle_EOF()] - if self.tkconsole.closing: - return - response = None - self.restart_subprocess() - if response: - self.tkconsole.resetoutput() - self.active_seq = None - how, what = response - console = self.tkconsole.console - if how == "OK": - if what is not None: - print(repr(what), file=console) - elif how == "EXCEPTION": - if self.tkconsole.getvar("<>"): - self.remote_stack_viewer() - elif how == "ERROR": - 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: - try: - self.tkconsole.endexecuting() - except AttributeError: # shell may have closed - pass - # Reschedule myself - if not self.tkconsole.closing: - self._afterid = self.tkconsole.text.after( - self.tkconsole.pollinterval, self.poll_subprocess) - - debugger = None - - def setdebugger(self, debugger): - self.debugger = debugger - - def getdebugger(self): - return self.debugger - - def open_remote_stack_viewer(self): - """Initiate the remote stack viewer from a separate thread. - - This method is called from the subprocess, and by returning from this - method we allow the subprocess to unblock. After a bit the shell - requests the subprocess to open the remote stack viewer which returns a - static object looking at the last exception. It is queried through - the RPC mechanism. - - """ - self.tkconsole.text.after(300, self.remote_stack_viewer) - return - - def remote_stack_viewer(self): - from idlelib import RemoteObjectBrowser - oid = self.rpcclt.remotequeue("exec", "stackviewer", ("flist",), {}) - if oid is None: - self.tkconsole.root.bell() - return - 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'] - sc = ScrolledCanvas(top, bg=background, highlightthickness=0) - sc.frame.pack(expand=1, fill="both") - node = TreeNode(sc.canvas, None, item) - node.expand() - # XXX Should GC the remote tree when closing the window - - gid = 0 - - def execsource(self, source): - "Like runsource() but assumes complete exec source" - filename = self.stuffsource(source) - self.execfile(filename, source) - - def execfile(self, filename, source=None): - "Execute an existing file" - if source is None: - with tokenize.open(filename) as fp: - source = fp.read() - try: - code = compile(source, filename, "exec") - except (OverflowError, SyntaxError): - self.tkconsole.resetoutput() - print('*** Error in script or command!\n' - 'Traceback (most recent call last):', - file=self.tkconsole.stderr) - InteractiveInterpreter.showsyntaxerror(self, filename) - self.tkconsole.showprompt() - else: - self.runcode(code) - - def runsource(self, source): - "Extend base class method: Stuff the source in the line cache first" - filename = self.stuffsource(source) - self.more = 0 - self.save_warnings_filters = warnings.filters[:] - warnings.filterwarnings(action="error", category=SyntaxWarning) - # at the moment, InteractiveInterpreter expects str - assert isinstance(source, str) - #if isinstance(source, str): - # from idlelib import IOBinding - # try: - # source = 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" - filename = "" % self.gid - self.gid = self.gid + 1 - lines = source.split("\n") - linecache.cache[filename] = len(source)+1, 0, lines, filename - return filename - - def prepend_syspath(self, filename): - "Prepend sys.path with file's directory if not already included" - self.runcommand("""if 1: - _filename = %r - import sys as _sys - from os.path import dirname as _dirname - _dir = _dirname(_filename) - if not _dir in _sys.path: - _sys.path.insert(0, _dir) - del _filename, _sys, _dirname, _dir - \n""" % (filename,)) - - def showsyntaxerror(self, filename=None): - """Override Interactive Interpreter method: Use 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") - type, value, tb = sys.exc_info() - msg = getattr(value, 'msg', '') or value or "" - 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) - 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() - - def showtraceback(self): - "Extend base class method to reset output properly" - self.tkconsole.resetoutput() - self.checklinecache() - InteractiveInterpreter.showtraceback(self) - if self.tkconsole.getvar("<>"): - self.tkconsole.open_stack_viewer() - - def checklinecache(self): - c = linecache.cache - for key in list(c.keys()): - if key[:1] + key[-1:] != "<>": - del c[key] - - def runcommand(self, code): - "Run the code without invoking the debugger" - # The code better not raise an exception! - if self.tkconsole.executing: - self.display_executing_dialog() - return 0 - if self.rpcclt: - self.rpcclt.remotequeue("exec", "runcode", (code,), {}) - else: - exec(code, self.locals) - return 1 - - def runcode(self, code): - "Override base class method" - 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() - if not debugger and self.rpcclt is not None: - self.active_seq = self.rpcclt.asyncqueue("exec", "runcode", - (code,), {}) - elif debugger: - debugger.run(code, self.locals) - else: - exec(code, self.locals) - except SystemExit: - if not self.tkconsole.closing: - if tkMessageBox.askyesno( - "Exit?", - "Do you want to exit altogether?", - default="yes", - parent=self.tkconsole.text): - raise - else: - self.showtraceback() - else: - raise - except: - if use_subprocess: - print("IDLE internal error in runcode()", - file=self.tkconsole.stderr) - self.showtraceback() - self.tkconsole.endexecuting() - else: - if self.tkconsole.canceled: - self.tkconsole.canceled = False - print("KeyboardInterrupt", file=self.tkconsole.stderr) - else: - self.showtraceback() - finally: - if not use_subprocess: - try: - self.tkconsole.endexecuting() - except AttributeError: # shell may have closed - pass - - def write(self, s): - "Override base class method" - return self.tkconsole.stderr.write(s) - - def display_port_binding_error(self): - tkMessageBox.showerror( - "Port Binding Error", - "IDLE can't bind to a TCP/IP port, which is necessary to " - "communicate with its Python execution server. This might be " - "because no networking is installed on this computer. " - "Run IDLE with the -n command line switch to start without a " - "subprocess and refer to Help/IDLE Help 'Running without a " - "subprocess' for further details.", - parent=self.tkconsole.text) - - def display_no_subprocess_error(self): - tkMessageBox.showerror( - "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): - tkMessageBox.showerror( - "Already executing", - "The Python Shell window is already executing a command; " - "please wait until it is finished.", - parent=self.tkconsole.text) - - -class PyShell(OutputWindow): - - shell_title = "Python " + python_version() + " Shell" - - # Override classes - ColorDelegator = ModifiedColorDelegator - UndoDelegator = ModifiedUndoDelegator - - # Override menus - menu_specs = [ - ("file", "_File"), - ("edit", "_Edit"), - ("debug", "_Debug"), - ("options", "_Options"), - ("windows", "_Window"), - ("help", "_Help"), - ] - - - # New classes - from idlelib.IdleHistory import History - - def __init__(self, flist=None): - if use_subprocess: - ms = self.menu_specs - if ms[2][0] != "shell": - ms.insert(2, ("shell", "She_ll")) - self.interp = ModifiedInterpreter(self) - if flist is None: - root = Tk() - 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.context_use_ps1 = True - # - text = self.text - text.configure(wrap="char") - text.bind("<>", self.enter_callback) - text.bind("<>", self.linefeed_callback) - text.bind("<>", self.cancel_callback) - text.bind("<>", self.eof_callback) - text.bind("<>", self.open_stack_viewer) - text.bind("<>", self.toggle_debugger) - text.bind("<>", self.toggle_jit_stack_viewer) - if use_subprocess: - text.bind("<>", self.view_restart_mark) - text.bind("<>", self.restart_shell) - # - self.save_stdout = sys.stdout - self.save_stderr = sys.stderr - self.save_stdin = sys.stdin - 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 a textView someday, but must work w/o subproc - pydoc.pager = pydoc.plainpager - except: - sys.stderr = sys.__stderr__ - raise - # - self.history = self.History(self.text) - # - self.pollinterval = 50 # millisec - - def get_standard_extension_names(self): - return idleConf.GetExtensions(shell_only=True) - - reading = False - executing = False - canceled = False - endoffile = False - closing = False - _stop_readline_flag = False - - def set_warning_stream(self, stream): - global warning_stream - warning_stream = stream - - def get_warning_stream(self): - return warning_stream - - def toggle_debugger(self, event=None): - if self.executing: - tkMessageBox.showerror("Don't debug now", - "You can only toggle the debugger when idle", - parent=self.text) - self.set_debugger_indicator() - return "break" - else: - db = self.interp.getdebugger() - if db: - self.close_debugger() - else: - self.open_debugger() - - def set_debugger_indicator(self): - db = self.interp.getdebugger() - self.setvar("<>", not not db) - - def toggle_jit_stack_viewer(self, event=None): - pass # All we need is the variable - - def close_debugger(self): - db = self.interp.getdebugger() - if db: - self.interp.setdebugger(None) - db.close() - if self.interp.rpcclt: - RemoteDebugger.close_remote_debugger(self.interp.rpcclt) - self.resetoutput() - self.console.write("[DEBUG OFF]\n") - sys.ps1 = ">>> " - self.showprompt() - self.set_debugger_indicator() - - def open_debugger(self): - if self.interp.rpcclt: - dbg_gui = RemoteDebugger.start_remote_debugger(self.interp.rpcclt, - self) - else: - dbg_gui = Debugger.Debugger(self) - self.interp.setdebugger(dbg_gui) - dbg_gui.load_breakpoints() - sys.ps1 = "[DEBUG ON]\n>>> " - self.showprompt() - self.set_debugger_indicator() - - def beginexecuting(self): - "Helper for ModifiedInterpreter" - self.resetoutput() - self.executing = 1 - - def endexecuting(self): - "Helper for ModifiedInterpreter" - self.executing = 0 - self.canceled = 0 - self.showprompt() - - def close(self): - "Extend EditorWindow.close()" - if self.executing: - response = tkMessageBox.askokcancel( - "Kill?", - "Your program is still running!\n Do you want to kill it?", - default="ok", - parent=self.text) - if response is False: - return "cancel" - self.stop_readline() - self.canceled = True - self.closing = True - return EditorWindow.close(self) - - def _close(self): - "Extend EditorWindow._close(), shut down debugger and execution server" - self.close_debugger() - if use_subprocess: - self.interp.kill_subprocess() - # Restore std streams - sys.stdout = self.save_stdout - sys.stderr = self.save_stderr - sys.stdin = self.save_stdin - # Break cycles - self.interp = None - self.console = None - self.flist.pyshell = None - self.history = None - EditorWindow._close(self) - - def ispythonsource(self, filename): - "Override EditorWindow method: never remove the colorizer" - return True - - def short_title(self): - return self.shell_title - - COPYRIGHT = \ - 'Type "copyright", "credits" or "license()" for more information.' - - def begin(self): - self.text.mark_set("iomark", "insert") - self.resetoutput() - if use_subprocess: - nosub = '' - client = self.interp.start_subprocess() - if not client: - 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 - - 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? - return True - - def stop_readline(self): - if not self.reading: # no nested mainloop to exit. - return - self._stop_readline_flag = True - self.top.quit() - - def readline(self): - save = self.reading - try: - self.reading = 1 - self.top.mainloop() # nested mainloop() - finally: - self.reading = save - if self._stop_readline_flag: - self._stop_readline_flag = False - return "" - line = self.text.get("iomark", "end-1c") - if len(line) == 0: # may be EOF if we quit our mainloop with Ctrl-C - line = "\n" - self.resetoutput() - if self.canceled: - self.canceled = 0 - if not use_subprocess: - raise KeyboardInterrupt - if self.endoffile: - self.endoffile = 0 - line = "" - return line - - def isatty(self): - return True - - def cancel_callback(self, event=None): - try: - if self.text.compare("sel.first", "!=", "sel.last"): - return # Active selection -- always use default binding - except: - pass - if not (self.executing or self.reading): - self.resetoutput() - self.interp.write("KeyboardInterrupt\n") - self.showprompt() - return "break" - self.endoffile = 0 - self.canceled = 1 - if (self.executing and self.interp.rpcclt): - if self.interp.getdebugger(): - self.interp.restart_subprocess() - else: - self.interp.interrupt_subprocess() - if self.reading: - self.top.quit() # exit the nested mainloop() in readline() - return "break" - - def eof_callback(self, event): - if self.executing and not self.reading: - return # Let the default binding (delete next char) take over - if not (self.text.compare("iomark", "==", "insert") and - self.text.compare("insert", "==", "end-1c")): - return # Let the default binding (delete next char) take over - if not self.executing: - self.resetoutput() - self.close() - else: - self.canceled = 0 - self.endoffile = 1 - self.top.quit() - return "break" - - def linefeed_callback(self, event): - # Insert a linefeed without entering anything (still autoindented) - if self.reading: - self.text.insert("insert", "\n") - self.text.see("insert") - else: - self.newline_and_indent_event(event) - return "break" - - def enter_callback(self, event): - if self.executing and not self.reading: - return # Let the default binding (insert '\n') take over - # If some text is selected, recall the selection - # (but only if this before the I/O mark) - try: - sel = self.text.get("sel.first", "sel.last") - if sel: - if self.text.compare("sel.last", "<=", "iomark"): - self.recall(sel, event) - return "break" - except: - pass - # If we're strictly before the line containing iomark, recall - # the current line, less a leading prompt, less leading or - # trailing whitespace - if self.text.compare("insert", "<", "iomark linestart"): - # Check if there's a relevant stdin range -- if so, use it - prev = self.text.tag_prevrange("stdin", "insert") - if prev and self.text.compare("insert", "<", prev[1]): - self.recall(self.text.get(prev[0], prev[1]), event) - return "break" - next = self.text.tag_nextrange("stdin", "insert") - if next and self.text.compare("insert lineend", ">=", next[0]): - self.recall(self.text.get(next[0], next[1]), event) - return "break" - # No stdin mark -- just get the current line, less any prompt - indices = self.text.tag_nextrange("console", "insert linestart") - if indices and \ - self.text.compare(indices[0], "<=", "insert linestart"): - self.recall(self.text.get(indices[1], "insert lineend"), event) - else: - self.recall(self.text.get("insert linestart", "insert lineend"), event) - return "break" - # If we're between the beginning of the line and the iomark, i.e. - # in the prompt area, move to the end of the prompt - if self.text.compare("insert", "<", "iomark"): - self.text.mark_set("insert", "iomark") - # If we're in the current input and there's only whitespace - # beyond the cursor, erase that whitespace first - s = self.text.get("insert", "end-1c") - if s and not s.strip(): - self.text.delete("insert", "end-1c") - # If we're in the current input before its last line, - # insert a newline right at the insert point - if self.text.compare("insert", "<", "end-1c linestart"): - self.newline_and_indent_event(event) - return "break" - # We're in the last line; append a newline and submit it - self.text.mark_set("insert", "end-1c") - if self.reading: - self.text.insert("insert", "\n") - self.text.see("insert") - else: - self.newline_and_indent_event(event) - self.text.tag_add("stdin", "iomark", "end-1c") - self.text.update_idletasks() - if self.reading: - self.top.quit() # Break out of recursive mainloop() - else: - self.runit() - return "break" - - def recall(self, s, event): - # remove leading and trailing empty or whitespace lines - s = re.sub(r'^\s*\n', '' , s) - s = re.sub(r'\n\s*$', '', s) - lines = s.split('\n') - self.text.undo_block_start() - try: - self.text.tag_remove("sel", "1.0", "end") - self.text.mark_set("insert", "end-1c") - prefix = self.text.get("insert linestart", "insert") - if prefix.rstrip().endswith(':'): - self.newline_and_indent_event(event) - prefix = self.text.get("insert linestart", "insert") - self.text.insert("insert", lines[0].strip()) - if len(lines) > 1: - orig_base_indent = re.search(r'^([ \t]*)', lines[0]).group(0) - new_base_indent = re.search(r'^([ \t]*)', prefix).group(0) - for line in lines[1:]: - if line.startswith(orig_base_indent): - # replace orig base indentation with new indentation - line = new_base_indent + line[len(orig_base_indent):] - self.text.insert('insert', '\n'+line.rstrip()) - finally: - self.text.see("insert") - self.text.undo_block_stop() - - def runit(self): - line = self.text.get("iomark", "end-1c") - # Strip off last newline and surrounding whitespace. - # (To allow you to hit return twice to end a statement.) - i = len(line) - while i > 0 and line[i-1] in " \t": - i = i-1 - if i > 0 and line[i-1] == "\n": - i = i-1 - while i > 0 and line[i-1] in " \t": - i = i-1 - line = line[:i] - self.interp.runsource(line) - - def open_stack_viewer(self, event=None): - if self.interp.rpcclt: - return self.interp.remote_stack_viewer() - try: - sys.last_traceback - except: - tkMessageBox.showerror("No stack trace", - "There is no stack trace yet.\n" - "(sys.last_traceback is not defined)", - parent=self.text) - return - from idlelib.StackViewer import StackBrowser - StackBrowser(self.root, self.flist) - - def view_restart_mark(self, event=None): - self.text.see("iomark") - self.text.see("restart") - - def restart_shell(self, event=None): - "Callback for Run/Restart Shell Cntl-F6" - self.interp.restart_subprocess(with_cwd=True) - - def showprompt(self): - self.resetoutput() - 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 resetoutput(self): - source = self.text.get("iomark", "end-1c") - if self.history: - self.history.store(source) - if self.text.get("end-2c") != "\n": - self.text.insert("end-1c", "\n") - self.text.mark_set("iomark", "end-1c") - self.set_line_and_column() - - def write(self, s, tags=()): - if isinstance(s, str) and len(s) and max(s) > '\uffff': - # Tk doesn't support outputting non-BMP characters - # Let's assume what printed string is not very long, - # find first non-BMP character and construct informative - # UnicodeEncodeError exception. - for start, char in enumerate(s): - if char > '\uffff': - break - raise UnicodeEncodeError("UCS-2", char, start, start+1, - 'Non-BMP character not supported in Tk') - try: - self.text.mark_gravity("iomark", "right") - count = 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. - if self.canceled: - self.canceled = 0 - if not use_subprocess: - raise KeyboardInterrupt - return count - - def rmenu_check_cut(self): - try: - if self.text.compare('sel.first', '<', 'iomark'): - return 'disabled' - except TclError: # no selection, so the index 'sel.first' doesn't exist - return 'disabled' - return super().rmenu_check_cut() - - def rmenu_check_paste(self): - if self.text.compare('insert','<','iomark'): - return 'disabled' - return super().rmenu_check_paste() - -class PseudoFile(io.TextIOBase): - - def __init__(self, shell, tags, encoding=None): - self.shell = shell - self.tags = tags - 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) is not str: - if not isinstance(s, str): - raise TypeError('must be str, not ' + type(s).__name__) - # See issue #19481 - s = str.__str__(s) - 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): - 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() - - -usage_msg = """\ - -USAGE: idle [-deins] [-t title] [file]* - idle [-dns] [-t title] (-c cmd | -r file) [arg]* - 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) - -The following options will override the IDLE 'settings' configuration: - - -e open an edit window - -i open a shell window - -The following options imply -i and will open a shell: - - -c cmd run the command in a shell, or - -r file run script from file - - -d enable the debugger - -s run $IDLESTARTUP or $PYTHONSTARTUP before anything else - -t title set title of shell window - -A default edit window will be bypassed when -c, -r, or - are used. - -[arg]* are passed to the command (-c) or script (-r) in sys.argv[1:]. - -Examples: - -idle - Open an edit window or shell depending on IDLE's configuration. - -idle foo.py foobar.py - Edit the files, also open a shell if configured to start with shell. - -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" - Open a shell window and run the command, passing "-c" in sys.argv[0] - and "foo" in sys.argv[1]. - -idle -d -s -r foo.py "Hello World" - Open a shell window, run a startup script, enable the debugger, and - 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" - Open a shell window, run the script piped in, passing '' in sys.argv[0] - and "foobar" in sys.argv[1]. -""" - -def main(): - global flist, root, use_subprocess - - capture_warnings(True) - use_subprocess = True - enable_shell = False - enable_edit = False - debug = False - cmd = None - script = None - startup = False - try: - opts, args = getopt.getopt(sys.argv[1:], "c:deihnr:st:") - except getopt.error as msg: - print("Error: %s\n%s" % (msg, usage_msg), file=sys.stderr) - sys.exit(2) - for o, a in opts: - if o == '-c': - cmd = a - enable_shell = True - if o == '-d': - debug = True - enable_shell = True - if o == '-e': - enable_edit = True - if o == '-h': - sys.stdout.write(usage_msg) - sys.exit() - 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) - sys.exit() - enable_shell = True - if o == '-s': - startup = True - enable_shell = True - if o == '-t': - PyShell.shell_title = a - enable_shell = True - if args and args[0] == '-': - cmd = sys.stdin.read() - enable_shell = True - # process sys.argv and sys.path: - for i in range(len(sys.path)): - sys.path[i] = os.path.abspath(sys.path[i]) - if args and args[0] == '-': - sys.argv = [''] + args[1:] - elif cmd: - sys.argv = ['-c'] + args - elif script: - sys.argv = [script] + args - elif args: - enable_edit = True - pathx = [] - for filename in args: - pathx.append(os.path.dirname(filename)) - for dir in pathx: - dir = os.path.abspath(dir) - if not dir in sys.path: - sys.path.insert(0, dir) - else: - dir = os.getcwd() - if dir not in sys.path: - sys.path.insert(0, dir) - # check the IDLE settings configuration (but command line overrides) - edit_start = idleConf.GetOption('main', 'General', - 'editor-on-startup', type='bool') - enable_edit = enable_edit or edit_start - enable_shell = enable_shell or not enable_edit - # start editor and/or shell windows: - root = Tk(className="Idle") - - # set application icon - icondir = os.path.join(os.path.dirname(__file__), 'Icons') - if system() == 'Windows': - iconfile = os.path.join(icondir, 'idle.ico') - root.wm_iconbitmap(default=iconfile) - elif TkVersion >= 8.5: - 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(file=iconfile) for iconfile in iconfiles] - root.wm_iconphoto(True, *icons) - - fixwordbreaks(root) - root.withdraw() - flist = PyShellFileList(root) - macosxSupport.setupApp(root, flist) - - if macosxSupport.isAquaTk(): - # There are some screwed up <2> class bindings for text - # widgets defined in Tk which we need to do away with. - # See issue #24801. - root.unbind_class('Text', '') - root.unbind_class('Text', '') - root.unbind_class('Text', '<>') - - if enable_edit: - if not (cmd or script): - for filename in args[:]: - if flist.open(filename) is None: - # filename is a directory actually, disconsider it - args.remove(filename) - if not args: - flist.new() - - if enable_shell: - shell = flist.open_shell() - if not shell: - return # couldn't open shell - 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 - # there are open files. - shell.top.lower() - else: - shell = flist.pyshell - - # Handle remaining options. If any of these are set, enable_shell - # was set also, so shell must be true to reach here. - if debug: - shell.open_debugger() - if startup: - filename = os.environ.get("IDLESTARTUP") or \ - os.environ.get("PYTHONSTARTUP") - if filename and os.path.isfile(filename): - shell.interp.execfile(filename) - if cmd or script: - shell.interp.runcommand("""if 1: - import sys as _sys - _sys.argv = %r - del _sys - \n""" % (sys.argv,)) - if cmd: - shell.interp.execsource(cmd) - elif script: - shell.interp.prepend_syspath(script) - shell.interp.execfile(script) - elif shell: - # If there is a shell window and no cmd or script in progress, - # 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.interp.runcommand("print('%s')" % tkversionwarning) - - while flist.inversedict: # keep IDLE running while files are open. - root.mainloop() - root.destroy() - 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/RemoteDebugger.py b/Lib/idlelib/RemoteDebugger.py deleted file mode 100644 index be2262f..0000000 --- a/Lib/idlelib/RemoteDebugger.py +++ /dev/null @@ -1,388 +0,0 @@ -"""Support for remote Python debugging. - -Some ASCII art to describe the structure: - - IN PYTHON SUBPROCESS # IN IDLE PROCESS - # - # oid='gui_adapter' - +----------+ # +------------+ +-----+ - | GUIProxy |--remote#call-->| GUIAdapter |--calls-->| GUI | -+-----+--calls-->+----------+ # +------------+ +-----+ -| Idb | # / -+-----+<-calls--+------------+ # +----------+<--calls-/ - | IdbAdapter |<--remote#call--| IdbProxy | - +------------+ # +----------+ - oid='idb_adapter' # - -The purpose of the Proxy and Adapter classes is to translate certain -arguments and return values that cannot be transported through the RPC -barrier, in particular frame and traceback objects. - -""" - -import types -from idlelib import Debugger - -debugging = 0 - -idb_adap_oid = "idb_adapter" -gui_adap_oid = "gui_adapter" - -#======================================= -# -# In the PYTHON subprocess: - -frametable = {} -dicttable = {} -codetable = {} -tracebacktable = {} - -def wrap_frame(frame): - fid = id(frame) - frametable[fid] = frame - return fid - -def wrap_info(info): - "replace info[2], a traceback instance, by its ID" - if info is None: - return None - else: - traceback = info[2] - assert isinstance(traceback, types.TracebackType) - traceback_id = id(traceback) - tracebacktable[traceback_id] = traceback - modified_info = (info[0], info[1], traceback_id) - return modified_info - -class GUIProxy: - - def __init__(self, conn, gui_adap_oid): - self.conn = conn - self.oid = gui_adap_oid - - def interaction(self, message, frame, info=None): - # calls rpc.SocketIO.remotecall() via run.MyHandler instance - # pass frame and traceback object IDs instead of the objects themselves - self.conn.remotecall(self.oid, "interaction", - (message, wrap_frame(frame), wrap_info(info)), - {}) - -class IdbAdapter: - - def __init__(self, idb): - self.idb = idb - - #----------called by an IdbProxy---------- - - def set_step(self): - self.idb.set_step() - - def set_quit(self): - self.idb.set_quit() - - def set_continue(self): - self.idb.set_continue() - - def set_next(self, fid): - frame = frametable[fid] - self.idb.set_next(frame) - - def set_return(self, fid): - frame = frametable[fid] - self.idb.set_return(frame) - - def get_stack(self, fid, tbid): - frame = frametable[fid] - if tbid is None: - tb = None - else: - tb = tracebacktable[tbid] - stack, i = self.idb.get_stack(frame, tb) - stack = [(wrap_frame(frame2), k) for frame2, k in stack] - return stack, i - - def run(self, cmd): - import __main__ - self.idb.run(cmd, __main__.__dict__) - - def set_break(self, filename, lineno): - msg = self.idb.set_break(filename, lineno) - return msg - - def clear_break(self, filename, lineno): - msg = self.idb.clear_break(filename, lineno) - return msg - - def clear_all_file_breaks(self, filename): - msg = self.idb.clear_all_file_breaks(filename) - return msg - - #----------called by a FrameProxy---------- - - def frame_attr(self, fid, name): - frame = frametable[fid] - return getattr(frame, name) - - def frame_globals(self, fid): - frame = frametable[fid] - dict = frame.f_globals - did = id(dict) - dicttable[did] = dict - return did - - def frame_locals(self, fid): - frame = frametable[fid] - dict = frame.f_locals - did = id(dict) - dicttable[did] = dict - return did - - def frame_code(self, fid): - frame = frametable[fid] - code = frame.f_code - cid = id(code) - codetable[cid] = code - return cid - - #----------called by a CodeProxy---------- - - def code_name(self, cid): - code = codetable[cid] - return code.co_name - - def code_filename(self, cid): - code = codetable[cid] - return code.co_filename - - #----------called by a DictProxy---------- - - def dict_keys(self, did): - raise NotImplemented("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()) - - def dict_item(self, did, key): - dict = dicttable[did] - value = dict[key] - value = repr(value) ### can't pickle module 'builtins' - return value - -#----------end class IdbAdapter---------- - - -def start_debugger(rpchandler, gui_adap_oid): - """Start the debugger and its RPC link in the Python subprocess - - Start the subprocess side of the split debugger and set up that side of the - RPC link by instantiating the GUIProxy, Idb debugger, and IdbAdapter - objects and linking them together. Register the IdbAdapter with the - RPCServer to handle RPC requests from the split debugger GUI via the - IdbProxy. - - """ - gui_proxy = GUIProxy(rpchandler, gui_adap_oid) - idb = Debugger.Idb(gui_proxy) - idb_adap = IdbAdapter(idb) - rpchandler.register(idb_adap_oid, idb_adap) - return idb_adap_oid - - -#======================================= -# -# In the IDLE process: - - -class FrameProxy: - - def __init__(self, conn, fid): - self._conn = conn - self._fid = fid - self._oid = "idb_adapter" - self._dictcache = {} - - def __getattr__(self, name): - if name[:1] == "_": - raise AttributeError(name) - if name == "f_code": - return self._get_f_code() - if name == "f_globals": - return self._get_f_globals() - if name == "f_locals": - return self._get_f_locals() - return self._conn.remotecall(self._oid, "frame_attr", - (self._fid, name), {}) - - def _get_f_code(self): - cid = self._conn.remotecall(self._oid, "frame_code", (self._fid,), {}) - return CodeProxy(self._conn, self._oid, cid) - - def _get_f_globals(self): - did = self._conn.remotecall(self._oid, "frame_globals", - (self._fid,), {}) - return self._get_dict_proxy(did) - - def _get_f_locals(self): - did = self._conn.remotecall(self._oid, "frame_locals", - (self._fid,), {}) - return self._get_dict_proxy(did) - - def _get_dict_proxy(self, did): - if did in self._dictcache: - return self._dictcache[did] - dp = DictProxy(self._conn, self._oid, did) - self._dictcache[did] = dp - return dp - - -class CodeProxy: - - def __init__(self, conn, oid, cid): - self._conn = conn - self._oid = oid - self._cid = cid - - def __getattr__(self, name): - if name == "co_name": - return self._conn.remotecall(self._oid, "code_name", - (self._cid,), {}) - if name == "co_filename": - return self._conn.remotecall(self._oid, "code_filename", - (self._cid,), {}) - - -class DictProxy: - - def __init__(self, conn, oid, did): - self._conn = conn - 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,), {}) - - 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) - - -class GUIAdapter: - - def __init__(self, conn, gui): - self.conn = conn - self.gui = gui - - def interaction(self, 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) - - -class IdbProxy: - - def __init__(self, conn, shell, oid): - self.oid = oid - self.conn = conn - self.shell = shell - - 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)) - return value - - def run(self, cmd, locals): - # Ignores locals on purpose! - seq = self.conn.asyncqueue(self.oid, "run", (cmd,), {}) - self.shell.interp.active_seq = seq - - def get_stack(self, frame, tbid): - # passing frame and traceback IDs, not the objects themselves - stack, i = self.call("get_stack", frame._fid, tbid) - stack = [(FrameProxy(self.conn, fid), k) for fid, k in stack] - return stack, i - - def set_continue(self): - self.call("set_continue") - - def set_step(self): - self.call("set_step") - - def set_next(self, frame): - self.call("set_next", frame._fid) - - def set_return(self, frame): - self.call("set_return", frame._fid) - - def set_quit(self): - self.call("set_quit") - - def set_break(self, filename, lineno): - msg = self.call("set_break", filename, lineno) - return msg - - def clear_break(self, filename, lineno): - msg = self.call("clear_break", filename, lineno) - return msg - - def clear_all_file_breaks(self, filename): - msg = self.call("clear_all_file_breaks", filename) - return msg - -def start_remote_debugger(rpcclt, pyshell): - """Start the subprocess debugger, initialize the debugger GUI and RPC link - - Request the RPCServer start the Python subprocess debugger and link. Set - up the Idle side of the split debugger by instantiating the IdbProxy, - debugger GUI, and debugger GUIAdapter objects and linking them together. - - Register the GUIAdapter with the RPCClient to handle debugger GUI - interaction requests coming from the subprocess debugger via the GUIProxy. - - The IdbAdapter will pass execution and environment requests coming from the - Idle debugger GUI to the subprocess debugger via the IdbProxy. - - """ - global idb_adap_oid - - 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_adap = GUIAdapter(rpcclt, gui) - rpcclt.register(gui_adap_oid, gui_adap) - return gui - -def close_remote_debugger(rpcclt): - """Shut down subprocess debugger and Idle side of debugger RPC link - - 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().) - - """ - close_subprocess_debugger(rpcclt) - rpcclt.unregister(gui_adap_oid) - -def close_subprocess_debugger(rpcclt): - rpcclt.remotecall("exec", "stop_the_debugger", (idb_adap_oid,), {}) - -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' diff --git a/Lib/idlelib/RemoteObjectBrowser.py b/Lib/idlelib/RemoteObjectBrowser.py deleted file mode 100644 index 8031aae..0000000 --- a/Lib/idlelib/RemoteObjectBrowser.py +++ /dev/null @@ -1,36 +0,0 @@ -from idlelib import rpc - -def remote_object_tree_item(item): - wrapper = WrappedObjectTreeItem(item) - oid = id(wrapper) - rpc.objecttable[oid] = wrapper - return oid - -class WrappedObjectTreeItem: - # Lives in PYTHON subprocess - - def __init__(self, item): - self.__item = item - - def __getattr__(self, name): - value = getattr(self.__item, name) - return value - - def _GetSubList(self): - sub_list = self.__item._GetSubList() - return list(map(remote_object_tree_item, sub_list)) - -class StubObjectTreeItem: - # Lives in IDLE process - - def __init__(self, sockio, oid): - self.sockio = sockio - self.oid = oid - - def __getattr__(self, name): - value = rpc.MethodProxy(self.sockio, self.oid, name) - return value - - def _GetSubList(self): - sub_list = self.sockio.remotecall(self.oid, "_GetSubList", (), {}) - return [StubObjectTreeItem(self.sockio, oid) for oid in sub_list] diff --git a/Lib/idlelib/ReplaceDialog.py b/Lib/idlelib/ReplaceDialog.py deleted file mode 100644 index f2ea22e..0000000 --- a/Lib/idlelib/ReplaceDialog.py +++ /dev/null @@ -1,241 +0,0 @@ -"""Replace dialog for IDLE. Inherits SearchDialogBase for GUI. -Uses idlelib.SearchEngine for search capability. -Defines various replace related functions like replace, replace all, -replace+find. -""" -from tkinter import * - -from idlelib import SearchEngine -from idlelib.SearchDialogBase import SearchDialogBase -import re - - -def replace(text): - """Returns a singleton ReplaceDialog instance.The single dialog - saves user entries and preferences across instances.""" - root = text._root() - engine = SearchEngine.get(root) - if not hasattr(engine, "_replacedialog"): - engine._replacedialog = ReplaceDialog(root, engine) - dialog = engine._replacedialog - dialog.open(text) - - -class ReplaceDialog(SearchDialogBase): - - title = "Replace Dialog" - icon = "Replace" - - def __init__(self, root, engine): - SearchDialogBase.__init__(self, root, engine) - self.replvar = StringVar(root) - - def open(self, text): - """Display the replace dialog""" - SearchDialogBase.open(self, text) - try: - first = text.index("sel.first") - except TclError: - first = None - try: - last = text.index("sel.last") - except TclError: - last = None - first = first or text.index("insert") - last = last or first - self.show_hit(first, last) - self.ok = 1 - - def create_entries(self): - """Create label and text entry widgets""" - SearchDialogBase.create_entries(self) - self.replent = self.make_entry("Replace with:", self.replvar)[0] - - def create_command_buttons(self): - 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, 1) - self.make_button("Replace All", self.replace_all) - - def find_it(self, event=None): - self.do_find(0) - - def replace_it(self, event=None): - if self.do_find(self.ok): - self.do_replace() - - def default_command(self, event=None): - "Replace and 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(0) - - def _replace_expand(self, m, repl): - """ Helper function for expanding a regular expression - in the replace field, if needed. """ - if self.engine.isre(): - try: - new = m.expand(repl) - except re.error: - self.engine.report_error(repl, 'Invalid Replace Expression') - new = None - else: - new = repl - - return new - - def replace_all(self, event=None): - """Replace all instances of patvar with replvar in text""" - prog = self.engine.getprog() - if not prog: - return - repl = self.replvar.get() - text = self.text - res = self.engine.search_text(text, prog) - if not res: - text.bell() - return - text.tag_remove("sel", "1.0", "end") - text.tag_remove("hit", "1.0", "end") - line = res[0] - col = res[1].start() - if self.engine.iswrap(): - line = 1 - col = 0 - ok = 1 - first = last = None - # XXX ought to replace circular instead of top-to-bottom when wrapping - text.undo_block_start() - while 1: - res = self.engine.search_forward(text, prog, line, col, 0, ok) - if not res: - break - line, m = res - chars = text.get("%d.0" % line, "%d.0" % (line+1)) - orig = m.group() - new = self._replace_expand(m, repl) - if new is None: - break - i, j = m.span() - first = "%d.%d" % (line, i) - last = "%d.%d" % (line, j) - if new == orig: - text.mark_set("insert", last) - else: - text.mark_set("insert", first) - if first != last: - text.delete(first, last) - if new: - text.insert(first, new) - col = i + len(new) - ok = 0 - text.undo_block_stop() - if first and last: - self.show_hit(first, last) - self.close() - - 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: - 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 = 1 - return True - - def do_replace(self): - prog = self.engine.getprog() - if not prog: - return False - text = self.text - try: - first = pos = text.index("sel.first") - last = text.index("sel.last") - except TclError: - pos = None - if not pos: - first = last = pos = text.index("insert") - 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: - return False - new = self._replace_expand(m, self.replvar.get()) - if new is None: - return False - text.mark_set("insert", first) - text.undo_block_start() - if m.group(): - text.delete(first, last) - if new: - text.insert(first, new) - text.undo_block_stop() - self.show_hit(first, text.index("insert")) - self.ok = 0 - return True - - def show_hit(self, first, last): - """Highlight text from 'first' to 'last'. - 'first', 'last' - Text indices""" - text = self.text - text.mark_set("insert", first) - text.tag_remove("sel", "1.0", "end") - text.tag_add("sel", first, last) - text.tag_remove("hit", "1.0", "end") - if first == last: - text.tag_add("hit", first) - else: - text.tag_add("hit", first, last) - text.see("insert") - text.update_idletasks() - - def close(self, event=None): - SearchDialogBase.close(self, event) - self.text.tag_remove("hit", "1.0", "end") - - -def _replace_dialog(parent): # htest # - """htest wrapper function""" - box = Toplevel(parent) - box.title("Test ReplaceDialog") - width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) - box.geometry("+%d+%d"%(x, y + 150)) - - # mock undo delegator methods - def undo_block_start(): - pass - - def undo_block_stop(): - pass - - text = Text(box, inactiveselectbackground='gray') - 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() - - def show_replace(): - text.tag_add(SEL, "1.0", END) - replace(text) - text.tag_remove(SEL, "1.0", END) - - button = Button(box, text="Replace", command=show_replace) - button.pack() - -if __name__ == '__main__': - import unittest - unittest.main('idlelib.idle_test.test_replacedialog', - 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 deleted file mode 100644 index 2ce3c7e..0000000 --- a/Lib/idlelib/RstripExtension.py +++ /dev/null @@ -1,33 +0,0 @@ -'Provides "Strip trailing whitespace" under the "Format" menu.' - -class RstripExtension: - - menudefs = [ - ('format', [None, ('Strip trailing whitespace', '<>'), ] ), ] - - def __init__(self, editwin): - self.editwin = editwin - self.editwin.text.bind("<>", 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 deleted file mode 100644 index 5cb818d..0000000 --- a/Lib/idlelib/ScriptBinding.py +++ /dev/null @@ -1,206 +0,0 @@ -"""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 tabnanny -import tokenize -import tkinter.messagebox as tkMessageBox -from idlelib import PyShell - -from idlelib.configHandler import idleConf -from idlelib import macosxSupport - -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', '<>'), - ('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('<>', 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): - # 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 "" - 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 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). - self.editwin.text_frame.after(200, - lambda: self.editwin.text_frame.event_generate('<>')) - return 'break' - else: - return self._run_module_event(event) - - 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= - self.editwin._filename_to_unicode(filename)) - dirname = os.path.dirname(filename) - # XXX Too often this discards arguments the user just set... - interp.runcommand("""if 1: - __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' - - 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 deleted file mode 100644 index 53576b5..0000000 --- a/Lib/idlelib/ScrolledList.py +++ /dev/null @@ -1,145 +0,0 @@ -from tkinter import * -from idlelib import macosxSupport - -class ScrolledList: - - default = "(None)" - - def __init__(self, master, **options): - # Create top frame, with scrollbar and listbox - self.master = master - self.frame = frame = Frame(master) - self.frame.pack(fill="both", expand=1) - self.vbar = vbar = Scrollbar(frame, name="vbar") - self.vbar.pack(side="right", fill="y") - self.listbox = listbox = Listbox(frame, exportselection=0, - background="white") - if options: - listbox.configure(options) - listbox.pack(expand=1, fill="both") - # Tie listbox and scrollbar together - vbar["command"] = listbox.yview - listbox["yscrollcommand"] = vbar.set - # Bind events to the list box - listbox.bind("", self.click_event) - listbox.bind("", self.double_click_event) - if macosxSupport.isAquaTk(): - listbox.bind("", self.popup_event) - listbox.bind("", self.popup_event) - else: - listbox.bind("", self.popup_event) - listbox.bind("", self.up_event) - listbox.bind("", self.down_event) - # Mark as empty - self.clear() - - def close(self): - self.frame.destroy() - - def clear(self): - self.listbox.delete(0, "end") - self.empty = 1 - self.listbox.insert("end", self.default) - - def append(self, item): - if self.empty: - self.listbox.delete(0, "end") - self.empty = 0 - self.listbox.insert("end", str(item)) - - def get(self, index): - return self.listbox.get(index) - - def click_event(self, event): - self.listbox.activate("@%d,%d" % (event.x, event.y)) - index = self.listbox.index("active") - self.select(index) - self.on_select(index) - return "break" - - def double_click_event(self, event): - index = self.listbox.index("active") - self.select(index) - self.on_double(index) - return "break" - - menu = None - - def popup_event(self, event): - if not self.menu: - self.make_menu() - menu = self.menu - self.listbox.activate("@%d,%d" % (event.x, event.y)) - index = self.listbox.index("active") - self.select(index) - menu.tk_popup(event.x_root, event.y_root) - - def make_menu(self): - menu = Menu(self.listbox, tearoff=0) - self.menu = menu - self.fill_menu() - - def up_event(self, event): - index = self.listbox.index("active") - if self.listbox.selection_includes(index): - index = index - 1 - else: - index = self.listbox.size() - 1 - if index < 0: - self.listbox.bell() - else: - self.select(index) - self.on_select(index) - return "break" - - def down_event(self, event): - index = self.listbox.index("active") - if self.listbox.selection_includes(index): - index = index + 1 - else: - index = 0 - if index >= self.listbox.size(): - self.listbox.bell() - else: - self.select(index) - self.on_select(index) - return "break" - - def select(self, index): - self.listbox.focus_set() - self.listbox.activate(index) - self.listbox.selection_clear(0, "end") - self.listbox.selection_set(index) - self.listbox.see(index) - - # Methods to override for specific actions - - def fill_menu(self): - pass - - def on_select(self, index): - pass - - def on_double(self, index): - pass - - -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)) - - scrolled_list = MyScrolledList(root) - for i in range(30): - scrolled_list.append("Item %02d" % i) - - 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 deleted file mode 100644 index 765d53f..0000000 --- a/Lib/idlelib/SearchDialog.py +++ /dev/null @@ -1,97 +0,0 @@ -from tkinter import * - -from idlelib import SearchEngine -from idlelib.SearchDialogBase import SearchDialogBase - -def _setup(text): - "Create or find the singleton SearchDialog instance." - root = text._root() - engine = SearchEngine.get(root) - if not hasattr(engine, "_searchdialog"): - engine._searchdialog = SearchDialog(root, engine) - return engine._searchdialog - -def find(text): - "Handle the editor edit menu item and corresponding event." - pat = text.get("sel.first", "sel.last") - return _setup(text).open(text, pat) # Open is inherited from SDBase. - -def find_again(text): - "Handle the editor edit menu item and corresponding event." - return _setup(text).find_again(text) - -def find_selection(text): - "Handle the editor edit menu item and corresponding event." - 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): # htest # - '''Display search test box.''' - box = Toplevel(parent) - box.title("Test SearchDialog") - width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) - box.geometry("+%d+%d"%(x, y + 150)) - text = Text(box, 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(box, text="Search (selection ignored)", command=show_find) - button.pack() - -if __name__ == '__main__': - import unittest - unittest.main('idlelib.idle_test.test_searchdialog', - verbosity=2, exit=False) - from idlelib.idle_test.htest import run - run(_search_dialog) diff --git a/Lib/idlelib/SearchDialogBase.py b/Lib/idlelib/SearchDialogBase.py deleted file mode 100644 index 5fa84e2..0000000 --- a/Lib/idlelib/SearchDialogBase.py +++ /dev/null @@ -1,184 +0,0 @@ -'''Define SearchDialogBase used by Search, Replace, and Grep dialogs.''' - -from tkinter import (Toplevel, Frame, Entry, Label, Button, - Checkbutton, Radiobutton) - -class SearchDialogBase: - '''Create most of a 3 or 4 row, 3 column search dialog. - - The left and wide middle column contain: - 1 or 2 labeled text entry lines (make_entry, create_entries); - a row of standard Checkbuttons (make_frame, create_option_buttons), - each of which corresponds to a search engine Variable; - a row of dialog-specific Check/Radiobuttons (create_other_buttons). - - The narrow right column contains command buttons - (make_button, create_command_buttons). - These are bound to functions that execute the command. - - Except for command buttons, this base class is not limited to items - common to all three subclasses. Rather, it is the Find dialog minus - the "Find Next" command, its execution function, and the - default_command attribute needed in create_widgets. The other - dialogs override attributes and methods, the latter to replace and - add widgets. - ''' - - title = "Search Dialog" # replace in subclasses - icon = "Search" - needwrapbutton = 1 # not in Find in Files - - def __init__(self, root, engine): - '''Initialize root, engine, and top attributes. - - top (level widget): set in create_widgets() called from open(). - text (Text searched): set in open(), only used in subclasses(). - ent (ry): created in make_entry() called from create_entry(). - row (of grid): 0 in create_widgets(), +1 in make_entry/frame(). - default_command: set in subclasses, used in create_widgers(). - - title (of dialog): class attribute, override in subclasses. - icon (of dialog): ditto, use unclear if cannot minimize dialog. - ''' - self.root = root - self.engine = engine - self.top = None - - def open(self, text, searchphrase=None): - "Make dialog visible on top of others and ready to use." - self.text = text - if not self.top: - self.create_widgets() - else: - self.top.deiconify() - self.top.tkraise() - if searchphrase: - self.ent.delete(0,"end") - self.ent.insert("end",searchphrase) - self.ent.focus_set() - self.ent.selection_range(0, "end") - self.ent.icursor(0) - self.top.grab_set() - - def close(self, event=None): - "Put dialog away for later use." - if self.top: - self.top.grab_release() - self.top.withdraw() - - def create_widgets(self): - '''Create basic 3 row x 3 col search (find) dialog. - - Other dialogs override subsidiary create_x methods as needed. - Replace and Find-in-Files add another entry row. - ''' - top = Toplevel(self.root) - top.bind("", self.default_command) - top.bind("", self.close) - top.protocol("WM_DELETE_WINDOW", self.close) - top.wm_title(self.title) - top.wm_iconname(self.icon) - self.top = top - - self.row = 0 - self.top.grid_columnconfigure(0, pad=2, weight=0) - self.top.grid_columnconfigure(1, pad=2, minsize=100, weight=100) - - self.create_entries() # row 0 (and maybe 1), cols 0, 1 - self.create_option_buttons() # next row, cols 0, 1 - self.create_other_buttons() # next row, cols 0, 1 - self.create_command_buttons() # col 2, all rows - - def make_entry(self, label_text, var): - '''Return (entry, label), . - - entry - gridded labeled Entry for text entry. - label - Label widget, returned for testing. - ''' - label = Label(self.top, text=label_text) - label.grid(row=self.row, column=0, sticky="nw") - entry = Entry(self.top, textvariable=var, exportselection=0) - entry.grid(row=self.row, column=1, sticky="nwe") - self.row = self.row + 1 - return entry, label - - def create_entries(self): - "Create one or more entry lines with make_entry." - self.ent = self.make_entry("Find:", self.engine.patvar)[0] - - def make_frame(self,labeltext=None): - '''Return (frame, label). - - frame - gridded labeled Frame for option or other buttons. - label - Label widget, returned for testing. - ''' - if labeltext: - label = Label(self.top, text=labeltext) - label.grid(row=self.row, column=0, sticky="nw") - else: - label = '' - frame = Frame(self.top) - frame.grid(row=self.row, column=1, columnspan=1, sticky="nwe") - self.row = self.row + 1 - return frame, label - - def create_option_buttons(self): - '''Return (filled frame, options) for testing. - - Options is a list of SearchEngine booleanvar, label pairs. - A gridded frame from make_frame is filled with a Checkbutton - for each pair, bound to the var, with the corresponding label. - ''' - frame = self.make_frame("Options")[0] - engine = self.engine - options = [(engine.revar, "Regular expression"), - (engine.casevar, "Match case"), - (engine.wordvar, "Whole word")] - if self.needwrapbutton: - options.append((engine.wrapvar, "Wrap around")) - for var, label in options: - btn = Checkbutton(frame, anchor="w", variable=var, text=label) - btn.pack(side="left", fill="both") - if var.get(): - btn.select() - return frame, options - - def create_other_buttons(self): - '''Return (frame, others) for testing. - - Others is a list of value, label pairs. - A gridded frame from make_frame is filled with radio buttons. - ''' - frame = self.make_frame("Direction")[0] - var = self.engine.backvar - others = [(1, 'Up'), (0, 'Down')] - for val, label in others: - btn = Radiobutton(frame, anchor="w", - variable=var, value=val, text=label) - btn.pack(side="left", fill="both") - if var.get() == val: - btn.select() - return frame, others - - def make_button(self, label, command, isdef=0): - "Return command button gridded in command frame." - b = Button(self.buttonframe, - text=label, command=command, - default=isdef and "active" or "normal") - cols,rows=self.buttonframe.grid_size() - b.grid(pady=1,row=rows,column=0,sticky="ew") - self.buttonframe.grid(rowspan=rows+1) - return b - - def create_command_buttons(self): - "Place buttons in vertical command frame gridded on right." - f = self.buttonframe = Frame(self.top) - f.grid(row=0,column=2,padx=2,pady=2,ipadx=2,ipady=2) - - b = self.make_button("close", self.close) - b.lower() - -if __name__ == '__main__': - import unittest - unittest.main( - 'idlelib.idle_test.test_searchdialogbase', verbosity=2) diff --git a/Lib/idlelib/SearchEngine.py b/Lib/idlelib/SearchEngine.py deleted file mode 100644 index 37883bf..0000000 --- a/Lib/idlelib/SearchEngine.py +++ /dev/null @@ -1,233 +0,0 @@ -'''Define SearchEngine for search dialogs.''' -import re -from tkinter import StringVar, BooleanVar, TclError -import tkinter.messagebox as tkMessageBox - -def get(root): - '''Return the singleton SearchEngine instance for the process. - - The single SearchEngine saves settings between dialog instances. - If there is not a SearchEngine already, make one. - ''' - if not hasattr(root, "_searchengine"): - root._searchengine = SearchEngine(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.""" - - def __init__(self, root): - '''Initialize Variables that save search state. - - The dialogs bind these to the UI elements present in the dialogs. - ''' - self.root = root # need for report_error() - self.patvar = StringVar(root, '') # search pattern - self.revar = BooleanVar(root, False) # regular expression? - self.casevar = BooleanVar(root, False) # match case? - self.wordvar = BooleanVar(root, False) # match whole word? - self.wrapvar = BooleanVar(root, True) # wrap around buffer? - self.backvar = BooleanVar(root, False) # search backwards? - - # Access methods - - def getpat(self): - return self.patvar.get() - - def setpat(self, pat): - self.patvar.set(pat) - - def isre(self): - return self.revar.get() - - def iscase(self): - return self.casevar.get() - - def isword(self): - return self.wordvar.get() - - def iswrap(self): - return self.wrapvar.get() - - def isback(self): - return self.backvar.get() - - # Higher level access methods - - def setcookedpat(self, pat): - "Set pattern after escaping if re." - # called only in SearchDialog.py: 66 - if self.isre(): - pat = re.escape(pat) - self.setpat(pat) - - def getcookedpat(self): - pat = self.getpat() - if not self.isre(): # if True, see setcookedpat - pat = re.escape(pat) - if self.isword(): - pat = r"\b%s\b" % pat - return pat - - def getprog(self): - "Return compiled cooked search pattern." - pat = self.getpat() - if not pat: - self.report_error(pat, "Empty regular expression") - return None - pat = self.getcookedpat() - flags = 0 - if not self.iscase(): - flags = flags | re.IGNORECASE - try: - prog = re.compile(pat, flags) - except re.error as what: - args = what.args - msg = args[0] - col = args[1] if len(args) >= 2 else -1 - self.report_error(pat, msg, col) - return None - return prog - - def report_error(self, pat, msg, col=-1): - # Derived class could override this with something fancier - msg = "Error: " + str(msg) - if pat: - msg = msg + "\nPattern: " + str(pat) - if col >= 0: - msg = msg + "\nOffset: " + str(col) - tkMessageBox.showerror("Regular expression error", - msg, master=self.root) - - def search_text(self, text, prog=None, ok=0): - '''Return (lineno, matchobj) or None for forward/backward search. - - This function calls the right function with the right arguments. - It directly return the result of that call. - - Text is a text widget. Prog is a precompiled pattern. - The ok parameter is a bit complicated as it has two effects. - - If there is a selection, the search begin at either end, - depending on the direction setting and ok, with ok meaning that - the search starts with the selection. Otherwise, search begins - at the insert mark. - - To aid progress, the search functions do not return an empty - match at the starting position unless ok is True. - ''' - - if not prog: - prog = self.getprog() - if not prog: - return None # Compilation failed -- stop - wrap = self.wrapvar.get() - first, last = get_selection(text) - if self.isback(): - if ok: - start = last - else: - start = first - line, col = get_line_col(start) - res = self.search_backward(text, prog, line, col, wrap, ok) - else: - if ok: - start = first - else: - start = last - line, col = get_line_col(start) - res = self.search_forward(text, prog, line, col, wrap, ok) - return res - - def search_forward(self, text, prog, line, col, wrap, ok=0): - wrapped = 0 - startline = line - chars = text.get("%d.0" % line, "%d.0" % (line+1)) - while chars: - m = prog.search(chars[:-1], col) - if m: - if ok or m.end() > col: - return line, m - line = line + 1 - if wrapped and line > startline: - break - col = 0 - ok = 1 - chars = text.get("%d.0" % line, "%d.0" % (line+1)) - if not chars and wrap: - wrapped = 1 - wrap = 0 - line = 1 - chars = text.get("1.0", "2.0") - return None - - def search_backward(self, text, prog, line, col, wrap, ok=0): - wrapped = 0 - startline = line - chars = text.get("%d.0" % line, "%d.0" % (line+1)) - while 1: - m = search_reverse(prog, chars[:-1], col) - if m: - if ok or m.start() < col: - return line, m - line = line - 1 - if wrapped and line < startline: - break - ok = 1 - if line <= 0: - if not wrap: - break - wrapped = 1 - wrap = 0 - pos = text.index("end-1c") - line, col = map(int, pos.split(".")) - chars = text.get("%d.0" % line, "%d.0" % (line+1)) - col = len(chars) - 1 - return None - -def search_reverse(prog, chars, col): - '''Search backwards and return an re match object or None. - - This is done by searching forwards until there is no match. - Prog: compiled re object with a search method returning a match. - Chars: line of text, without \\n. - Col: stop index for the search; the limit for match.end(). - ''' - m = prog.search(chars) - if not m: - return None - found = None - i, j = m.span() # m.start(), m.end() == match slice indexes - while i < col and j <= col: - found = m - if i == j: - j = j+1 - m = prog.search(chars, j) - if not m: - break - i, j = m.span() - return found - -def get_selection(text): - '''Return tuple of 'line.col' indexes from selection or insert mark. - ''' - try: - first = text.index("sel.first") - last = text.index("sel.last") - except TclError: - first = last = None - if not first: - first = text.index("insert") - if not last: - last = first - return first, last - -def get_line_col(index): - '''Return (line, col) tuple of ints from 'line.col' string.''' - line, col = map(int, index.split(".")) # Fails on invalid index - return line, col - -if __name__ == "__main__": - 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 deleted file mode 100644 index ccc755c..0000000 --- a/Lib/idlelib/StackViewer.py +++ /dev/null @@ -1,151 +0,0 @@ -import os -import sys -import linecache -import re -import tkinter as tk - -from idlelib.TreeWidget import TreeNode, TreeItem, ScrolledCanvas -from idlelib.ObjectBrowser import ObjectTreeItem, make_objecttreeitem -from idlelib.PyShell import PyShellFileList - -def StackBrowser(root, flist=None, tb=None, top=None): - if top is None: - top = tk.Toplevel(root) - sc = ScrolledCanvas(top, bg="white", highlightthickness=0) - sc.frame.pack(expand=1, fill="both") - item = StackTreeItem(flist, tb) - node = TreeNode(sc.canvas, None, item) - node.expand() - -class StackTreeItem(TreeItem): - - def __init__(self, flist=None, tb=None): - self.flist = flist - self.stack = self.get_stack(tb) - self.text = self.get_exception() - - def get_stack(self, tb): - if tb is None: - tb = sys.last_traceback - stack = [] - if tb and tb.tb_frame is None: - tb = tb.tb_next - while tb is not None: - stack.append((tb.tb_frame, tb.tb_lineno)) - tb = tb.tb_next - return stack - - def get_exception(self): - type = sys.last_type - value = sys.last_value - if hasattr(type, "__name__"): - type = type.__name__ - s = str(type) - if value is not None: - s = s + ": " + str(value) - return s - - def GetText(self): - return self.text - - def GetSubList(self): - sublist = [] - for info in self.stack: - item = FrameTreeItem(info, self.flist) - sublist.append(item) - return sublist - -class FrameTreeItem(TreeItem): - - def __init__(self, info, flist): - self.info = info - self.flist = flist - - def GetText(self): - frame, lineno = self.info - try: - modname = frame.f_globals["__name__"] - except: - modname = "?" - code = frame.f_code - filename = code.co_filename - funcname = code.co_name - sourceline = linecache.getline(filename, lineno) - sourceline = sourceline.strip() - if funcname in ("?", "", None): - item = "%s, line %d: %s" % (modname, lineno, sourceline) - else: - item = "%s.%s(...), line %d: %s" % (modname, funcname, - lineno, sourceline) - return item - - def GetSubList(self): - frame, lineno = self.info - sublist = [] - if frame.f_globals is not frame.f_locals: - item = VariablesTreeItem("", frame.f_locals, self.flist) - sublist.append(item) - item = VariablesTreeItem("", frame.f_globals, self.flist) - sublist.append(item) - return sublist - - def OnDoubleClick(self): - if self.flist: - frame, lineno = self.info - filename = frame.f_code.co_filename - if os.path.isfile(filename): - self.flist.gotofileline(filename, lineno) - -class VariablesTreeItem(ObjectTreeItem): - - def GetText(self): - return self.labeltext - - def GetLabelText(self): - return None - - def IsExpandable(self): - return len(self.object) > 0 - - def GetSubList(self): - sublist = [] - for key in self.object.keys(): - try: - value = self.object[key] - except KeyError: - continue - def setfunction(value, key=key, object=self.object): - object[key] = value - item = make_objecttreeitem(key + " =", value, setfunction) - sublist.append(item) - return sublist - - def keys(self): # unused, left for possible 3rd party use - return list(self.object.keys()) - -def _stack_viewer(parent): - root = tk.Tk() - root.title("Test StackViewer") - width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) - root.geometry("+%d+%d"%(x, y + 150)) - flist = PyShellFileList(root) - try: # to obtain a traceback object - intentional_name_error - except NameError: - exc_type, exc_value, exc_tb = sys.exc_info() - - # inject stack trace to sys - sys.last_type = exc_type - sys.last_value = exc_value - sys.last_traceback = exc_tb - - StackBrowser(root, flist=flist, top=root, tb=exc_tb) - - # restore sys to original state - del sys.last_type - del sys.last_value - del sys.last_traceback - -if __name__ == '__main__': - from idlelib.idle_test.htest import run - run(_stack_viewer) diff --git a/Lib/idlelib/ToolTip.py b/Lib/idlelib/ToolTip.py deleted file mode 100644 index 964107e..0000000 --- a/Lib/idlelib/ToolTip.py +++ /dev/null @@ -1,97 +0,0 @@ -# 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("", self.enter) - self._id2 = self.button.bind("", self.leave) - self._id3 = self.button.bind("", 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/TreeWidget.py b/Lib/idlelib/TreeWidget.py deleted file mode 100644 index a19578f..0000000 --- a/Lib/idlelib/TreeWidget.py +++ /dev/null @@ -1,466 +0,0 @@ -# XXX TO DO: -# - popup menu -# - support partial or total redisplay -# - key bindings (instead of quick-n-dirty bindings on Canvas): -# - up/down arrow keys to move focus around -# - ditto for page up/down, home/end -# - left/right arrows to expand/collapse & move out/in -# - more doc strings -# - add icons for "file", "module", "class", "method"; better "python" icon -# - callback for selection??? -# - multiple-item selection -# - tooltips -# - redo geometry without magic numbers -# - keep track of object ids to allow more careful cleaning -# - optimize tree redraw after expand of subnode - -import os -from tkinter import * - -from idlelib import ZoomHeight -from idlelib.configHandler import idleConf - -ICONDIR = "Icons" - -# Look for Icons subdirectory in the same directory as this module -try: - _icondir = os.path.join(os.path.dirname(__file__), ICONDIR) -except NameError: - _icondir = ICONDIR -if os.path.isdir(_icondir): - ICONDIR = _icondir -elif not os.path.isdir(ICONDIR): - raise RuntimeError("can't find icon directory (%r)" % (ICONDIR,)) - -def listicons(icondir=ICONDIR): - """Utility to display the available icons.""" - root = Tk() - import glob - list = glob.glob(os.path.join(icondir, "*.gif")) - list.sort() - images = [] - row = column = 0 - for file in list: - name = os.path.splitext(os.path.basename(file))[0] - image = PhotoImage(file=file, master=root) - images.append(image) - label = Label(root, image=image, bd=1, relief="raised") - label.grid(row=row, column=column) - label = Label(root, text=name) - label.grid(row=row+1, column=column) - column = column + 1 - if column >= 10: - row = row+2 - column = 0 - root.images = images - - -class TreeNode: - - def __init__(self, canvas, parent, item): - self.canvas = canvas - self.parent = parent - self.item = item - self.state = 'collapsed' - self.selected = False - self.children = [] - self.x = self.y = None - self.iconimages = {} # cache of PhotoImage instances for icons - - def destroy(self): - for c in self.children[:]: - self.children.remove(c) - c.destroy() - self.parent = None - - def geticonimage(self, name): - try: - return self.iconimages[name] - except KeyError: - pass - file, ext = os.path.splitext(name) - ext = ext or ".gif" - fullname = os.path.join(ICONDIR, file + ext) - image = PhotoImage(master=self.canvas, file=fullname) - self.iconimages[name] = image - return image - - def select(self, event=None): - if self.selected: - return - self.deselectall() - self.selected = True - self.canvas.delete(self.image_id) - self.drawicon() - self.drawtext() - - def deselect(self, event=None): - if not self.selected: - return - self.selected = False - self.canvas.delete(self.image_id) - self.drawicon() - self.drawtext() - - def deselectall(self): - if self.parent: - self.parent.deselectall() - else: - self.deselecttree() - - def deselecttree(self): - if self.selected: - self.deselect() - for child in self.children: - child.deselecttree() - - def flip(self, event=None): - if self.state == 'expanded': - self.collapse() - else: - self.expand() - self.item.OnDoubleClick() - return "break" - - def expand(self, event=None): - if not self.item._IsExpandable(): - return - if self.state != 'expanded': - self.state = 'expanded' - self.update() - self.view() - - def collapse(self, event=None): - if self.state != 'collapsed': - self.state = 'collapsed' - self.update() - - def view(self): - top = self.y - 2 - bottom = self.lastvisiblechild().y + 17 - height = bottom - top - visible_top = self.canvas.canvasy(0) - visible_height = self.canvas.winfo_height() - visible_bottom = self.canvas.canvasy(visible_height) - if visible_top <= top and bottom <= visible_bottom: - return - x0, y0, x1, y1 = self.canvas._getints(self.canvas['scrollregion']) - if top >= visible_top and height <= visible_height: - fraction = top + height - visible_height - else: - fraction = top - fraction = float(fraction) / y1 - self.canvas.yview_moveto(fraction) - - def lastvisiblechild(self): - if self.children and self.state == 'expanded': - return self.children[-1].lastvisiblechild() - else: - return self - - def update(self): - if self.parent: - self.parent.update() - else: - oldcursor = self.canvas['cursor'] - self.canvas['cursor'] = "watch" - self.canvas.update() - self.canvas.delete(ALL) # XXX could be more subtle - self.draw(7, 2) - x0, y0, x1, y1 = self.canvas.bbox(ALL) - self.canvas.configure(scrollregion=(0, 0, x1, y1)) - self.canvas['cursor'] = oldcursor - - def draw(self, x, y): - # XXX This hard-codes too many geometry constants! - dy = 20 - self.x, self.y = x, y - self.drawicon() - self.drawtext() - if self.state != 'expanded': - return y + dy - # draw children - if not self.children: - sublist = self.item._GetSubList() - if not sublist: - # _IsExpandable() was mistaken; that's allowed - return y+17 - for item in sublist: - child = self.__class__(self.canvas, self, item) - self.children.append(child) - cx = x+20 - cy = y + dy - cylast = 0 - for child in self.children: - cylast = cy - self.canvas.create_line(x+9, cy+7, cx, cy+7, fill="gray50") - cy = child.draw(cx, cy) - if child.item._IsExpandable(): - if child.state == 'expanded': - iconname = "minusnode" - callback = child.collapse - else: - iconname = "plusnode" - callback = child.expand - image = self.geticonimage(iconname) - id = self.canvas.create_image(x+9, cylast+7, image=image) - # XXX This leaks bindings until canvas is deleted: - self.canvas.tag_bind(id, "<1>", callback) - self.canvas.tag_bind(id, "", lambda x: None) - id = self.canvas.create_line(x+9, y+10, x+9, cylast+7, - ##stipple="gray50", # XXX Seems broken in Tk 8.0.x - fill="gray50") - self.canvas.tag_lower(id) # XXX .lower(id) before Python 1.5.2 - return cy - - def drawicon(self): - if self.selected: - imagename = (self.item.GetSelectedIconName() or - self.item.GetIconName() or - "openfolder") - else: - imagename = self.item.GetIconName() or "folder" - image = self.geticonimage(imagename) - id = self.canvas.create_image(self.x, self.y, anchor="nw", image=image) - self.image_id = id - self.canvas.tag_bind(id, "<1>", self.select) - self.canvas.tag_bind(id, "", self.flip) - - def drawtext(self): - textx = self.x+20-1 - texty = self.y-4 - labeltext = self.item.GetLabelText() - if labeltext: - id = self.canvas.create_text(textx, texty, anchor="nw", - text=labeltext) - self.canvas.tag_bind(id, "<1>", self.select) - self.canvas.tag_bind(id, "", self.flip) - x0, y0, x1, y1 = self.canvas.bbox(id) - textx = max(x1, 200) + 10 - text = self.item.GetText() or "" - try: - self.entry - except AttributeError: - pass - else: - self.edit_finish() - try: - self.label - except AttributeError: - # padding carefully selected (on Windows) to match Entry widget: - self.label = Label(self.canvas, text=text, bd=0, padx=2, pady=2) - theme = idleConf.CurrentTheme() - if self.selected: - self.label.configure(idleConf.GetHighlight(theme, 'hilite')) - else: - self.label.configure(idleConf.GetHighlight(theme, 'normal')) - id = self.canvas.create_window(textx, texty, - anchor="nw", window=self.label) - self.label.bind("<1>", self.select_or_edit) - self.label.bind("", self.flip) - self.text_id = id - - def select_or_edit(self, event=None): - if self.selected and self.item.IsEditable(): - self.edit(event) - else: - self.select(event) - - def edit(self, event=None): - self.entry = Entry(self.label, bd=0, highlightthickness=1, width=0) - self.entry.insert(0, self.label['text']) - self.entry.selection_range(0, END) - self.entry.pack(ipadx=5) - self.entry.focus_set() - self.entry.bind("", self.edit_finish) - self.entry.bind("", self.edit_cancel) - - def edit_finish(self, event=None): - try: - entry = self.entry - del self.entry - except AttributeError: - return - text = entry.get() - entry.destroy() - if text and text != self.item.GetText(): - self.item.SetText(text) - text = self.item.GetText() - self.label['text'] = text - self.drawtext() - self.canvas.focus_set() - - def edit_cancel(self, event=None): - try: - entry = self.entry - del self.entry - except AttributeError: - return - entry.destroy() - self.drawtext() - self.canvas.focus_set() - - -class TreeItem: - - """Abstract class representing tree items. - - Methods should typically be overridden, otherwise a default action - is used. - - """ - - def __init__(self): - """Constructor. Do whatever you need to do.""" - - def GetText(self): - """Return text string to display.""" - - def GetLabelText(self): - """Return label text string to display in front of text (if any).""" - - expandable = None - - def _IsExpandable(self): - """Do not override! Called by TreeNode.""" - if self.expandable is None: - self.expandable = self.IsExpandable() - return self.expandable - - def IsExpandable(self): - """Return whether there are subitems.""" - return 1 - - def _GetSubList(self): - """Do not override! Called by TreeNode.""" - if not self.IsExpandable(): - return [] - sublist = self.GetSubList() - if not sublist: - self.expandable = 0 - return sublist - - def IsEditable(self): - """Return whether the item's text may be edited.""" - - def SetText(self, text): - """Change the item's text (if it is editable).""" - - def GetIconName(self): - """Return name of icon to be displayed normally.""" - - def GetSelectedIconName(self): - """Return name of icon to be displayed when selected.""" - - def GetSubList(self): - """Return list of items forming sublist.""" - - def OnDoubleClick(self): - """Called on a double-click on the item.""" - - -# Example application - -class FileTreeItem(TreeItem): - - """Example TreeItem subclass -- browse the file system.""" - - def __init__(self, path): - self.path = path - - def GetText(self): - return os.path.basename(self.path) or self.path - - def IsEditable(self): - return os.path.basename(self.path) != "" - - def SetText(self, text): - newpath = os.path.dirname(self.path) - newpath = os.path.join(newpath, text) - if os.path.dirname(newpath) != os.path.dirname(self.path): - return - try: - os.rename(self.path, newpath) - self.path = newpath - except OSError: - pass - - def GetIconName(self): - if not self.IsExpandable(): - return "python" # XXX wish there was a "file" icon - - def IsExpandable(self): - return os.path.isdir(self.path) - - def GetSubList(self): - try: - names = os.listdir(self.path) - except OSError: - return [] - names.sort(key = os.path.normcase) - sublist = [] - for name in names: - item = FileTreeItem(os.path.join(self.path, name)) - sublist.append(item) - return sublist - - -# 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 - self.master = master - self.frame = Frame(master) - self.frame.rowconfigure(0, weight=1) - self.frame.columnconfigure(0, weight=1) - self.canvas = Canvas(self.frame, **opts) - self.canvas.grid(row=0, column=0, sticky="nsew") - self.vbar = Scrollbar(self.frame, name="vbar") - self.vbar.grid(row=0, column=1, sticky="nse") - self.hbar = Scrollbar(self.frame, name="hbar", orient="horizontal") - self.hbar.grid(row=1, column=0, sticky="ews") - self.canvas['yscrollcommand'] = self.vbar.set - self.vbar['command'] = self.canvas.yview - self.canvas['xscrollcommand'] = self.hbar.set - self.hbar['command'] = self.canvas.xview - self.canvas.bind("", self.page_up) - self.canvas.bind("", self.page_down) - self.canvas.bind("", self.unit_up) - self.canvas.bind("", self.unit_down) - #if isinstance(master, Toplevel) or isinstance(master, Tk): - self.canvas.bind("", self.zoom_height) - self.canvas.focus_set() - def page_up(self, event): - self.canvas.yview_scroll(-1, "page") - return "break" - def page_down(self, event): - self.canvas.yview_scroll(1, "page") - return "break" - def unit_up(self, event): - self.canvas.yview_scroll(-1, "unit") - return "break" - def unit_down(self, event): - self.canvas.yview_scroll(1, "unit") - return "break" - def zoom_height(self, event): - ZoomHeight.zoom_height(self.master) - return "break" - - -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(os.getcwd()) - node = TreeNode(sc.canvas, None, item) - node.expand() - root.mainloop() - -if __name__ == '__main__': - from idlelib.idle_test.htest import run - run(_tree_widget) diff --git a/Lib/idlelib/UndoDelegator.py b/Lib/idlelib/UndoDelegator.py deleted file mode 100644 index 1c2502d..0000000 --- a/Lib/idlelib/UndoDelegator.py +++ /dev/null @@ -1,368 +0,0 @@ -import string -from tkinter import * - -from idlelib.Delegator import Delegator - -#$ event <> -#$ win -#$ unix - -#$ event <> -#$ win -#$ unix - -#$ event <> -#$ win -#$ unix - - -class UndoDelegator(Delegator): - - max_undo = 1000 - - def __init__(self): - Delegator.__init__(self) - self.reset_undo() - - def setdelegate(self, delegate): - if self.delegate is not None: - self.unbind("<>") - self.unbind("<>") - self.unbind("<>") - Delegator.setdelegate(self, delegate) - if delegate is not None: - self.bind("<>", self.undo_event) - self.bind("<>", self.redo_event) - self.bind("<>", self.dump_event) - - 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()) - pprint(self.undolist[self.pointer:]) - return "break" - - def reset_undo(self): - self.was_saved = -1 - self.pointer = 0 - self.undolist = [] - self.undoblock = 0 # or a CommandSequence instance - self.set_saved(1) - - def set_saved(self, flag): - if flag: - self.saved = self.pointer - else: - self.saved = -1 - self.can_merge = False - self.check_saved() - - def get_saved(self): - return self.saved == self.pointer - - saved_change_hook = None - - def set_saved_change_hook(self, hook): - self.saved_change_hook = hook - - was_saved = -1 - - def check_saved(self): - is_saved = self.get_saved() - if is_saved != self.was_saved: - self.was_saved = is_saved - if self.saved_change_hook: - self.saved_change_hook() - - def insert(self, index, chars, tags=None): - self.addcmd(InsertCommand(index, chars, tags)) - - def delete(self, index1, index2=None): - self.addcmd(DeleteCommand(index1, index2)) - - # Clients should call undo_block_start() and undo_block_stop() - # around a sequence of editing cmds to be treated as a unit by - # undo & redo. Nested matching calls are OK, and the inner calls - # then act like nops. OK too if no editing cmds, or only one - # editing cmd, is issued in between: if no cmds, the whole - # sequence has no effect; and if only one cmd, that cmd is entered - # directly into the undo list, as if undo_block_xxx hadn't been - # called. The intent of all that is to make this scheme easy - # to use: all the client has to worry about is making sure each - # _start() call is matched by a _stop() call. - - def undo_block_start(self): - if self.undoblock == 0: - self.undoblock = CommandSequence() - self.undoblock.bump_depth() - - def undo_block_stop(self): - if self.undoblock.bump_depth(-1) == 0: - cmd = self.undoblock - self.undoblock = 0 - if len(cmd) > 0: - if len(cmd) == 1: - # no need to wrap a single cmd - cmd = cmd.getcmd(0) - # this blk of cmds, or single cmd, has already - # been done, so don't execute it again - self.addcmd(cmd, 0) - - def addcmd(self, cmd, execute=True): - if execute: - cmd.do(self.delegate) - if self.undoblock != 0: - self.undoblock.append(cmd) - return - if self.can_merge and self.pointer > 0: - lastcmd = self.undolist[self.pointer-1] - if lastcmd.merge(cmd): - return - self.undolist[self.pointer:] = [cmd] - if self.saved > self.pointer: - self.saved = -1 - self.pointer = self.pointer + 1 - if len(self.undolist) > self.max_undo: - ##print "truncating undo list" - del self.undolist[0] - self.pointer = self.pointer - 1 - if self.saved >= 0: - self.saved = self.saved - 1 - self.can_merge = True - self.check_saved() - - def undo_event(self, event): - if self.pointer == 0: - self.bell() - return "break" - cmd = self.undolist[self.pointer - 1] - cmd.undo(self.delegate) - self.pointer = self.pointer - 1 - self.can_merge = False - self.check_saved() - return "break" - - def redo_event(self, event): - if self.pointer >= len(self.undolist): - self.bell() - return "break" - cmd = self.undolist[self.pointer] - cmd.redo(self.delegate) - self.pointer = self.pointer + 1 - self.can_merge = False - self.check_saved() - return "break" - - -class Command: - - # Base class for Undoable commands - - tags = None - - def __init__(self, index1, index2, chars, tags=None): - self.marks_before = {} - self.marks_after = {} - self.index1 = index1 - self.index2 = index2 - self.chars = chars - if tags: - self.tags = tags - - def __repr__(self): - s = self.__class__.__name__ - t = (self.index1, self.index2, self.chars, self.tags) - if self.tags is None: - t = t[:-1] - return s + repr(t) - - def do(self, text): - pass - - def redo(self, text): - pass - - def undo(self, text): - pass - - def merge(self, cmd): - return 0 - - def save_marks(self, text): - marks = {} - for name in text.mark_names(): - if name != "insert" and name != "current": - marks[name] = text.index(name) - return marks - - def set_marks(self, text, marks): - for name, index in marks.items(): - text.mark_set(name, index) - - -class InsertCommand(Command): - - # Undoable insert command - - def __init__(self, index1, chars, tags=None): - Command.__init__(self, index1, None, chars, tags) - - def do(self, text): - self.marks_before = self.save_marks(text) - self.index1 = text.index(self.index1) - if text.compare(self.index1, ">", "end-1c"): - # Insert before the final newline - self.index1 = text.index("end-1c") - text.insert(self.index1, self.chars, self.tags) - self.index2 = text.index("%s+%dc" % (self.index1, len(self.chars))) - self.marks_after = self.save_marks(text) - ##sys.__stderr__.write("do: %s\n" % self) - - def redo(self, text): - text.mark_set('insert', self.index1) - text.insert(self.index1, self.chars, self.tags) - self.set_marks(text, self.marks_after) - text.see('insert') - ##sys.__stderr__.write("redo: %s\n" % self) - - def undo(self, text): - text.mark_set('insert', self.index1) - text.delete(self.index1, self.index2) - self.set_marks(text, self.marks_before) - text.see('insert') - ##sys.__stderr__.write("undo: %s\n" % self) - - def merge(self, cmd): - if self.__class__ is not cmd.__class__: - return False - if self.index2 != cmd.index1: - return False - if self.tags != cmd.tags: - return False - if len(cmd.chars) != 1: - return False - if self.chars and \ - self.classify(self.chars[-1]) != self.classify(cmd.chars): - return False - self.index2 = cmd.index2 - self.chars = self.chars + cmd.chars - return True - - alphanumeric = string.ascii_letters + string.digits + "_" - - def classify(self, c): - if c in self.alphanumeric: - return "alphanumeric" - if c == "\n": - return "newline" - return "punctuation" - - -class DeleteCommand(Command): - - # Undoable delete command - - def __init__(self, index1, index2=None): - Command.__init__(self, index1, index2, None, None) - - def do(self, text): - self.marks_before = self.save_marks(text) - self.index1 = text.index(self.index1) - if self.index2: - self.index2 = text.index(self.index2) - else: - self.index2 = text.index(self.index1 + " +1c") - if text.compare(self.index2, ">", "end-1c"): - # Don't delete the final newline - self.index2 = text.index("end-1c") - self.chars = text.get(self.index1, self.index2) - text.delete(self.index1, self.index2) - self.marks_after = self.save_marks(text) - ##sys.__stderr__.write("do: %s\n" % self) - - def redo(self, text): - text.mark_set('insert', self.index1) - text.delete(self.index1, self.index2) - self.set_marks(text, self.marks_after) - text.see('insert') - ##sys.__stderr__.write("redo: %s\n" % self) - - def undo(self, text): - text.mark_set('insert', self.index1) - text.insert(self.index1, self.chars) - self.set_marks(text, self.marks_before) - 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 - - def __init__(self): - self.cmds = [] - self.depth = 0 - - def __repr__(self): - s = self.__class__.__name__ - strs = [] - for cmd in self.cmds: - strs.append(" %r" % (cmd,)) - return s + "(\n" + ",\n".join(strs) + "\n)" - - def __len__(self): - return len(self.cmds) - - def append(self, cmd): - self.cmds.append(cmd) - - def getcmd(self, i): - return self.cmds[i] - - def redo(self, text): - for cmd in self.cmds: - cmd.redo(text) - - def undo(self, text): - cmds = self.cmds[:] - cmds.reverse() - for cmd in cmds: - cmd.undo(text) - - def bump_depth(self, incr=1): - self.depth = self.depth + incr - return self.depth - - -def _undo_delegator(parent): # htest # - import re - import tkinter as tk - from idlelib.Percolator import Percolator - undowin = tk.Toplevel() - undowin.title("Test UndoDelegator") - width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) - undowin.geometry("+%d+%d"%(x, y + 150)) - - text = Text(undowin, 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.pack(side='left') - redo = Button(undowin, text="Redo", command=lambda:d.redo_event(None)) - redo.pack(side='left') - dump = Button(undowin, text="Dump", command=lambda:d.dump_event(None)) - dump.pack(side='left') - -if __name__ == "__main__": - import unittest - unittest.main('idlelib.idle_test.test_undodelegator', verbosity=2, - exit=False) - from idlelib.idle_test.htest import run - run(_undo_delegator) diff --git a/Lib/idlelib/WidgetRedirector.py b/Lib/idlelib/WidgetRedirector.py deleted file mode 100644 index b66be9e..0000000 --- a/Lib/idlelib/WidgetRedirector.py +++ /dev/null @@ -1,176 +0,0 @@ -from tkinter import TclError - -class WidgetRedirector: - """Support for redirecting arbitrary widget subcommands. - - Some Tk operations don't normally pass through tkinter. For example, if a - character is inserted into a Text widget by pressing a key, a default Tk - binding to the widget's 'insert' operation is activated, and the Tk library - processes the insert without calling back into tkinter. - - Although a binding to could be made via tkinter, what we really want - to do is to hook the Tk 'insert' operation itself. For one thing, we want - a text.insert call in idle code to have the same effect as a key press. - - When a widget is instantiated, a Tcl command is created whose name is the - same as the pathname widget._w. This command is used to invoke the various - widget operations, e.g. insert (for a Text widget). We are going to hook - this command and provide a facility ('register') to intercept the widget - operation. We will also intercept method calls on the tkinter class - instance that represents the tk widget. - - In IDLE, WidgetRedirector is used in Percolator to intercept Text - commands. The function being registered provides access to the top - of a Percolator chain. At the bottom of the chain is a call to the - original Tk widget operation. - """ - def __init__(self, widget): - '''Initialize attributes and setup redirection. - - _operations: dict mapping operation name to new function. - widget: the widget whose tcl command is to be intercepted. - tk: widget.tk, a convenience attribute, probably not needed. - orig: new name of the original tcl command. - - Since renaming to orig fails with TclError when orig already - exists, only one WidgetDirector can exist for a given widget. - ''' - self._operations = {} - self.widget = widget # widget instance - self.tk = tk = widget.tk # widget's root - w = widget._w # widget's (full) Tk pathname - self.orig = w + "_orig" - # Rename the Tcl command within Tcl: - tk.call("rename", w, self.orig) - # Create a new Tcl command whose name is the widget's pathname, and - # whose action is to dispatch on the operation passed to the widget: - tk.createcommand(w, self.dispatch) - - def __repr__(self): - return "%s(%s<%s>)" % (self.__class__.__name__, - self.widget.__class__.__name__, - self.widget._w) - - def close(self): - "Unregister operations and revert redirection created by .__init__." - for operation in list(self._operations): - self.unregister(operation) - widget = self.widget - tk = widget.tk - w = widget._w - # Restore the original widget Tcl command. - tk.deletecommand(w) - tk.call("rename", self.orig, w) - del self.widget, self.tk # Should not be needed - # if instance is deleted after close, as in Percolator. - - def register(self, operation, function): - '''Return OriginalCommand(operation) after registering function. - - Registration adds an operation: function pair to ._operations. - It also adds a widget function attribute that masks the tkinter - class instance method. Method masking operates independently - from command dispatch. - - If a second function is registered for the same operation, the - first function is replaced in both places. - ''' - self._operations[operation] = function - setattr(self.widget, operation, function) - return OriginalCommand(self, operation) - - def unregister(self, operation): - '''Return the function for the operation, or None. - - Deleting the instance attribute unmasks the class attribute. - ''' - if operation in self._operations: - function = self._operations[operation] - del self._operations[operation] - try: - delattr(self.widget, operation) - except AttributeError: - pass - return function - else: - return None - - def dispatch(self, operation, *args): - '''Callback from Tcl which runs when the widget is referenced. - - If an operation has been registered in self._operations, apply the - associated function to the args passed into Tcl. Otherwise, pass the - operation through to Tk via the original Tcl function. - - 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 ColorDelegator.py. - - ''' - m = self._operations.get(operation) - try: - if m: - return m(*args) - else: - return self.tk.call((self.orig, operation) + args) - except TclError: - return "" - - -class OriginalCommand: - '''Callable for original tk command that has been redirected. - - Returned by .register; can be used in the function registered. - redir = WidgetRedirector(text) - def my_insert(*args): - print("insert", args) - original_insert(*args) - original_insert = redir.register("insert", my_insert) - ''' - - def __init__(self, redir, operation): - '''Create .tk_call and .orig_and_operation for .__call__ method. - - .redir and .operation store the input args for __repr__. - .tk and .orig copy attributes of .redir (probably not needed). - ''' - self.redir = redir - self.operation = operation - self.tk = redir.tk # redundant with self.redir - self.orig = redir.orig # redundant with self.redir - # These two could be deleted after checking recipient code. - self.tk_call = redir.tk.call - self.orig_and_operation = (redir.orig, operation) - - def __repr__(self): - return "%s(%r, %r)" % (self.__class__.__name__, - 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 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) - def my_insert(*args): - print("insert", args) - original_insert(*args) - original_insert = redir.register("insert", my_insert) - root.mainloop() - -if __name__ == "__main__": - import unittest - unittest.main('idlelib.idle_test.test_widgetredir', - verbosity=2, exit=False) - from idlelib.idle_test.htest import run - run(_widget_redirector) diff --git a/Lib/idlelib/WindowList.py b/Lib/idlelib/WindowList.py deleted file mode 100644 index bc74348..0000000 --- a/Lib/idlelib/WindowList.py +++ /dev/null @@ -1,90 +0,0 @@ -from tkinter import * - -class WindowList: - - def __init__(self): - self.dict = {} - self.callbacks = [] - - def add(self, window): - window.after_idle(self.call_callbacks) - self.dict[str(window)] = window - - def delete(self, window): - try: - del self.dict[str(window)] - except KeyError: - # Sometimes, destroy() is called twice - pass - self.call_callbacks() - - def add_windows_to_menu(self, menu): - list = [] - for key in self.dict: - window = self.dict[key] - try: - title = window.get_title() - except TclError: - continue - list.append((title, key, window)) - list.sort() - for title, key, window in list: - menu.add_command(label=title, command=window.wakeup) - - def register_callback(self, callback): - self.callbacks.append(callback) - - def unregister_callback(self, callback): - try: - self.callbacks.remove(callback) - except ValueError: - pass - - def call_callbacks(self): - for callback in self.callbacks: - try: - callback() - except: - t, v, tb = sys.exc_info() - print("warning: callback failed in WindowList", t, ":", v) - -registry = WindowList() - -add_windows_to_menu = registry.add_windows_to_menu -register_callback = registry.register_callback -unregister_callback = registry.unregister_callback - - -class ListedToplevel(Toplevel): - - def __init__(self, master, **kw): - Toplevel.__init__(self, master, kw) - registry.add(self) - self.focused_widget = self - - def destroy(self): - registry.delete(self) - Toplevel.destroy(self) - # If this is Idle's last window then quit the mainloop - # (Needed for clean exit on Windows 98) - if not registry.dict: - self.quit() - - def update_windowlist_registry(self, window): - registry.call_callbacks() - - def get_title(self): - # Subclass can override - return self.wm_title() - - def wakeup(self): - try: - if self.wm_state() == "iconic": - self.wm_withdraw() - self.wm_deiconify() - self.tkraise() - self.focused_widget.focus_set() - except TclError: - # This can happen when the window menu was torn off. - # Simply ignore it. - pass diff --git a/Lib/idlelib/ZoomHeight.py b/Lib/idlelib/ZoomHeight.py deleted file mode 100644 index a5d679e..0000000 --- a/Lib/idlelib/ZoomHeight.py +++ /dev/null @@ -1,51 +0,0 @@ -# Sample extension: zoom a window to maximum height - -import re -import sys - -from idlelib import macosxSupport - -class ZoomHeight: - - menudefs = [ - ('windows', [ - ('_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/aboutDialog.py b/Lib/idlelib/aboutDialog.py deleted file mode 100644 index 3112e6a..0000000 --- a/Lib/idlelib/aboutDialog.py +++ /dev/null @@ -1,149 +0,0 @@ -"""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('',self.Ok) #dismiss dialog - self.bind('',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', 'ascii') - - 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.destroy() - -if __name__ == '__main__': - from idlelib.idle_test.htest import run - run(AboutDialog) diff --git a/Lib/idlelib/autocomplete.py b/Lib/idlelib/autocomplete.py new file mode 100644 index 0000000..b9ec539 --- /dev/null +++ b/Lib/idlelib/autocomplete.py @@ -0,0 +1,233 @@ +"""AutoComplete.py - An IDLE extension for automatically completing names. + +This extension can complete either attribute names of file names. It can pop +a window with all available names, for the user to select from. +""" +import os +import sys +import string + +from idlelib.configHandler import idleConf + +# This string includes all chars that may be in an identifier +ID_CHARS = string.ascii_letters + string.digits + "_" + +# 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", "<>"), + ]) + ] + + popupwait = idleConf.GetOption("extensions", "AutoComplete", + "popupwait", type="int", default=0) + + def __init__(self, editwin=None): + self.editwin = editwin + 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. + self._delayed_completion_id = None + self._delayed_completion_index = None + + def _make_autocomplete_window(self): + return AutoCompleteWindow.AutoCompleteWindow(self.text) + + def _remove_autocomplete_window(self, event=None): + if self.autocompletewindow: + self.autocompletewindow.hide_window() + self.autocompletewindow = None + + def force_open_completions_event(self, event): + """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): + """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(False, True, True) + if opened: + return "break" + + 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: + return + self.open_completions(*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 + start of completion, won't open completions and return False. + If mode is given, will open a completion list only in this mode. + """ + # Cancel another delayed call, if it exists. + if self._delayed_completion_id is not None: + self.text.after_cancel(self._delayed_completion_id) + self._delayed_completion_id = None + + 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==COMPLETE_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. + self._remove_autocomplete_window() + mode = COMPLETE_FILES + # Find last separator or string start + while i and curline[i-1] not in "'\"" + SEPS: + i -= 1 + comp_start = curline[i:j] + j = i + # Find string start + while i and curline[i-1] not in "'\"": + i -= 1 + comp_what = curline[i:j] + elif hp.is_in_code() and (not mode or mode==COMPLETE_ATTRIBUTES): + self._remove_autocomplete_window() + mode = COMPLETE_ATTRIBUTES + while i and (curline[i-1] in ID_CHARS or ord(curline[i-1]) > 127): + i -= 1 + comp_start = curline[i:j] + 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 + else: + comp_what = "" + else: + return + + if complete and not comp_what and not comp_start: + return + comp_lists = self.fetch_completions(comp_what, mode) + if not comp_lists[0]: + return + self.autocompletewindow = self._make_autocomplete_window() + return not self.autocompletewindow.show_window( + comp_lists, "insert-%dc" % len(comp_start), + complete, mode, userWantsWin) + + def fetch_completions(self, what, mode): + """Return a pair of lists of completions for something. The first list + is a sublist of the second. Both are sorted. + + If there is a Python subprocess, get the comp. list there. Otherwise, + either fetch_completions() 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. + """ + try: + rpcclt = self.editwin.flist.pyshell.interp.rpcclt + except: + rpcclt = None + if rpcclt: + return rpcclt.remotecall("exec", "get_the_completion_list", + (what, mode), {}) + else: + if mode == COMPLETE_ATTRIBUTES: + if what == "": + namespace = __main__.__dict__.copy() + namespace.update(__main__.__builtins__.__dict__) + bigl = eval("dir()", namespace) + bigl.sort() + if "__all__" in bigl: + smalll = sorted(eval("__all__", namespace)) + else: + smalll = [s for s in bigl if s[:1] != '_'] + else: + try: + entity = self.get_entity(what) + bigl = dir(entity) + bigl.sort() + if "__all__" in bigl: + smalll = sorted(entity.__all__) + else: + smalll = [s for s in bigl if s[:1] != '_'] + except: + return [], [] + + elif mode == COMPLETE_FILES: + if what == "": + what = "." + try: + expandedpath = os.path.expanduser(what) + bigl = os.listdir(expandedpath) + bigl.sort() + smalll = [s for s in bigl if s[:1] != '.'] + except OSError: + return [], [] + + if not smalll: + smalll = bigl + return smalll, bigl + + def get_entity(self, name): + """Lookup name in a namespace spanning sys.modules and __main.dict__""" + namespace = sys.modules.copy() + namespace.update(__main__.__dict__) + return eval(name, namespace) + + +if __name__ == '__main__': + from unittest import main + main('idlelib.idle_test.test_autocomplete', verbosity=2) diff --git a/Lib/idlelib/autocomplete_w.py b/Lib/idlelib/autocomplete_w.py new file mode 100644 index 0000000..2ee6878 --- /dev/null +++ b/Lib/idlelib/autocomplete_w.py @@ -0,0 +1,416 @@ +""" +An auto-completion window for IDLE, used by the AutoComplete extension +""" +from tkinter import * +from idlelib.MultiCall import MC_SHIFT +from idlelib.AutoComplete import COMPLETE_FILES, COMPLETE_ATTRIBUTES + +HIDE_VIRTUAL_EVENT_NAME = "<>" +HIDE_SEQUENCES = ("", "") +KEYPRESS_VIRTUAL_EVENT_NAME = "<>" +# We need to bind event beyond so that the function will be called +# before the default specific IDLE function +KEYPRESS_SEQUENCES = ("", "", "", "", + "", "", "", "", + "", "") +KEYRELEASE_VIRTUAL_EVENT_NAME = "<>" +KEYRELEASE_SEQUENCE = "" +LISTUPDATE_SEQUENCE = "" +WINCONFIG_SEQUENCE = "" +DOUBLECLICK_SEQUENCE = "" + +class AutoCompleteWindow: + + def __init__(self, widget): + # The widget (Text) on which we place the AutoCompleteWindow + self.widget = widget + # The widgets we create + self.autocompletewindow = self.listbox = self.scrollbar = None + # The default foreground and background of a selection. Saved because + # they are changed to the regular colors of list items when the + # completion start is not a prefix of the selected completion + self.origselforeground = self.origselbackground = None + # The list of completions + self.completions = None + # A list with more completions, or None + self.morecompletions = None + # 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 + # The index of the start of the completion + self.startindex = None + # The last typed start, used so that when the selection changes, + # the new start will be as close as possible to the last typed one. + self.lasttypedstart = None + # Do we have an indication that the user wants the completion window + # (for example, he clicked the list) + self.userwantswindow = None + # event ids + 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 + + def _change_start(self, newstart): + min_len = min(len(self.start), len(newstart)) + i = 0 + while i < min_len and self.start[i] == newstart[i]: + i += 1 + if i < len(self.start): + self.widget.delete("%s+%dc" % (self.startindex, i), + "%s+%dc" % (self.startindex, len(self.start))) + if i < len(newstart): + self.widget.insert("%s+%dc" % (self.startindex, i), + newstart[i:]) + self.start = newstart + + 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 + one.""" + i = 0; j = len(self.completions) + while j > i: + m = (i + j) // 2 + if self.completions[m] >= s: + j = m + else: + i = m + 1 + return min(i, len(self.completions)-1) + + 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.""" + first = self._binary_search(s) + if self.completions[first][:len(s)] != s: + # There is not even one completion which s is a prefix of. + return s + # Find the end of the range of completions where s is a prefix of. + i = first + 1 + j = len(self.completions) + while j > i: + m = (i + j) // 2 + if self.completions[m][:len(s)] != s: + j = m + else: + i = m + 1 + last = i-1 + + if first == last: # only one possible completion + return self.completions[first] + + # We should return the maximum prefix of first and last + first_comp = self.completions[first] + last_comp = self.completions[last] + min_len = min(len(first_comp), len(last_comp)) + i = len(s) + while i < min_len and first_comp[i] == last_comp[i]: + i += 1 + return first_comp[:i] + + def _selection_changed(self): + """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) + + lts = self.lasttypedstart + selstart = self.completions[cursel] + if self._binary_search(lts) == cursel: + newstart = lts + else: + min_len = min(len(lts), len(selstart)) + i = 0 + while i < min_len and lts[i] == selstart[i]: + i += 1 + newstart = selstart[:i] + self._change_start(newstart) + + if self.completions[cursel][:len(self.start)] == self.start: + # start is a prefix of the selected completion + self.listbox.configure(selectbackground=self.origselbackground, + selectforeground=self.origselforeground) + else: + self.listbox.configure(selectbackground=self.listbox.cget("bg"), + selectforeground=self.listbox.cget("fg")) + # If there are more completions, show them, and call me again. + if self.morecompletions: + self.completions = self.morecompletions + self.morecompletions = None + self.listbox.delete(0, END) + for item in self.completions: + self.listbox.insert(END, item) + self.listbox.select_set(self._binary_search(self.start)) + self._selection_changed() + + 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.""" + # Handle the start we already have + self.completions, self.morecompletions = comp_lists + self.mode = mode + self.startindex = self.widget.index(index) + self.start = self.widget.get(self.startindex, "insert") + if complete: + completed = self._complete_string(self.start) + start = self.start + self._change_start(completed) + i = self._binary_search(completed) + if self.completions[i] == completed and \ + (i == len(self.completions)-1 or + self.completions[i+1][:len(completed)] != completed): + # There is exactly one matching completion + return completed == start + self.userwantswindow = userWantsWin + self.lasttypedstart = self.start + + # Put widgets in place + self.autocompletewindow = acw = Toplevel(self.widget) + # Put it in a position so that it is not seen. + acw.wm_geometry("+10000+10000") + # Make it float + acw.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. + acw.tk.call("::tk::unsupported::MacWindowStyle", "style", acw._w, + "help", "noActivates") + except TclError: + pass + self.scrollbar = scrollbar = Scrollbar(acw, orient=VERTICAL) + self.listbox = listbox = Listbox(acw, yscrollcommand=scrollbar.set, + exportselection=False, bg="white") + for item in self.completions: + listbox.insert(END, item) + self.origselforeground = listbox.cget("selectforeground") + self.origselbackground = listbox.cget("selectbackground") + scrollbar.config(command=listbox.yview) + scrollbar.pack(side=RIGHT, fill=Y) + listbox.pack(side=LEFT, fill=BOTH, expand=True) + acw.lift() # work around bug in Tk 8.5.18+ (issue #24570) + + # Initialize the listbox selection + self.listbox.select_set(self._binary_search(self.start)) + self._selection_changed() + + # bind events + 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: + self.widget.event_add(KEYPRESS_VIRTUAL_EVENT_NAME, seq) + self.keyreleaseid = self.widget.bind(KEYRELEASE_VIRTUAL_EVENT_NAME, + self.keyrelease_event) + self.widget.event_add(KEYRELEASE_VIRTUAL_EVENT_NAME,KEYRELEASE_SEQUENCE) + self.listupdateid = listbox.bind(LISTUPDATE_SEQUENCE, + self.listselect_event) + self.winconfigid = acw.bind(WINCONFIG_SEQUENCE, self.winconfig_event) + self.doubleclickid = listbox.bind(DOUBLECLICK_SEQUENCE, + self.doubleclick_event) + + def winconfig_event(self, event): + if not self.is_active(): + return + # Position the completion list window + text = self.widget + text.see(self.startindex) + x, y, cx, cy = text.bbox(self.startindex) + acw = self.autocompletewindow + 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)) + new_y = text.winfo_rooty() + y + if (text_height - (y + cy) >= acw_height # enough height below + or y < acw_height): # not enough height above + # place acw below current line + new_y += cy + else: + # place acw above current line + new_y -= acw_height + acw.wm_geometry("+%d+%d" % (new_x, new_y)) + + def hide_event(self, event): + if not self.is_active(): + return + self.hide_window() + + def listselect_event(self, event): + 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 + cursel = int(self.listbox.curselection()[0]) + self._change_start(self.completions[cursel]) + self.hide_window() + + def keypress_event(self, event): + if not self.is_active(): + return + keysym = event.keysym + if hasattr(event, "mc_state"): + state = event.mc_state + else: + state = 0 + if keysym != "Tab": + self.lastkey_was_tab = False + if (len(keysym) == 1 or keysym in ("underscore", "BackSpace") + or (self.mode == COMPLETE_FILES and keysym in + ("period", "minus"))) \ + and not (state & ~MC_SHIFT): + # Normal editing of text + if len(keysym) == 1: + self._change_start(self.start + keysym) + elif keysym == "underscore": + self._change_start(self.start + '_') + elif keysym == "period": + self._change_start(self.start + '.') + elif keysym == "minus": + self._change_start(self.start + '-') + else: + # keysym == "BackSpace" + if len(self.start) == 0: + self.hide_window() + return + self._change_start(self.start[:-1]) + 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" + + elif keysym == "Return": + self.hide_window() + return + + elif (self.mode == COMPLETE_ATTRIBUTES and keysym in + ("period", "space", "parenleft", "parenright", "bracketleft", + "bracketright")) or \ + (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 + # completing file names, put the whole + # selected completion. Anyway, close the list. + cursel = int(self.listbox.curselection()[0]) + if self.completions[cursel][:len(self.start)] == self.start \ + and (self.mode == COMPLETE_ATTRIBUTES or self.start): + self._change_start(self.completions[cursel]) + self.hide_window() + return + + elif keysym in ("Home", "End", "Prior", "Next", "Up", "Down") and \ + not state: + # Move the selection in the listbox + self.userwantswindow = True + cursel = int(self.listbox.curselection()[0]) + if keysym == "Home": + newsel = 0 + elif keysym == "End": + newsel = len(self.completions)-1 + elif keysym in ("Prior", "Next"): + jump = self.listbox.nearest(self.listbox.winfo_height()) - \ + self.listbox.nearest(0) + if keysym == "Prior": + newsel = max(0, cursel-jump) + else: + assert keysym == "Next" + newsel = min(len(self.completions)-1, cursel+jump) + elif keysym == "Up": + newsel = max(0, cursel-1) + else: + assert keysym == "Down" + newsel = min(len(self.completions)-1, cursel+1) + self.listbox.select_clear(cursel) + self.listbox.select_set(newsel) + self._selection_changed() + self._change_start(self.completions[newsel]) + return "break" + + elif (keysym == "Tab" and not state): + if self.lastkey_was_tab: + # two tabs in a row; insert current selection and close acw + cursel = int(self.listbox.curselection()[0]) + self._change_start(self.completions[cursel]) + self.hide_window() + return "break" + else: + # first tab; let AutoComplete handle the completion + self.userwantswindow = True + self.lastkey_was_tab = True + return + + elif any(s in keysym for s in ("Shift", "Control", "Alt", + "Meta", "Command", "Option")): + # A modifier key, so ignore + return + + 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" + + else: + # Unknown event, close the window and let it through. + self.hide_window() + return + + def keyrelease_event(self, event): + if not self.is_active(): + return + if self.widget.index("insert") != \ + self.widget.index("%s+%dc" % (self.startindex, len(self.start))): + # If we didn't catch an event which moved the insert, close window + self.hide_window() + + def is_active(self): + return self.autocompletewindow is not None + + def complete(self): + self._change_start(self._complete_string(self.start)) + # The selection doesn't change. + + def hide_window(self): + if not self.is_active(): + return + + # unbind events + 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 + for seq in KEYPRESS_SEQUENCES: + self.widget.event_delete(KEYPRESS_VIRTUAL_EVENT_NAME, seq) + self.widget.unbind(KEYPRESS_VIRTUAL_EVENT_NAME, self.keypressid) + self.keypressid = None + self.widget.event_delete(KEYRELEASE_VIRTUAL_EVENT_NAME, + KEYRELEASE_SEQUENCE) + self.widget.unbind(KEYRELEASE_VIRTUAL_EVENT_NAME, self.keyreleaseid) + self.keyreleaseid = None + self.listbox.unbind(LISTUPDATE_SEQUENCE, self.listupdateid) + self.listupdateid = None + self.autocompletewindow.unbind(WINCONFIG_SEQUENCE, self.winconfigid) + self.winconfigid = None + + # destroy widgets + self.scrollbar.destroy() + self.scrollbar = None + self.listbox.destroy() + self.listbox = None + self.autocompletewindow.destroy() + self.autocompletewindow = None diff --git a/Lib/idlelib/autoexpand.py b/Lib/idlelib/autoexpand.py new file mode 100644 index 0000000..7059054 --- /dev/null +++ b/Lib/idlelib/autoexpand.py @@ -0,0 +1,104 @@ +'''Complete the current word before the cursor with words in the editor. + +Each menu selection or shortcut key selection replaces the word with a +different word with the same prefix. The search for matches begins +before the target and moves toward the top of the editor. It then starts +after the cursor and moves down. It then returns to the original word and +the cycle starts again. + +Changing the current text line or leaving the cursor in a different +place before requesting the next selection causes AutoExpand to reset +its state. + +This is an extension file and there is only one instance of AutoExpand. +''' +import string +import re + +###$ event <> +###$ win +###$ unix + +class AutoExpand: + + menudefs = [ + ('edit', [ + ('E_xpand Word', '<>'), + ]), + ] + + wordchars = string.ascii_letters + string.digits + "_" + + def __init__(self, editwin): + self.text = editwin.text + self.state = None + + def expand_word_event(self, event): + "Replace the current word with the next expansion." + curinsert = self.text.index("insert") + curline = self.text.get("insert linestart", "insert lineend") + if not self.state: + words = self.getwords() + index = 0 + else: + words, index, insert, line = self.state + if insert != curinsert or line != curline: + words = self.getwords() + index = 0 + if not words: + 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.text.bell() # Warn we cycled around + self.text.insert("insert", newword) + curinsert = self.text.index("insert") + curline = self.text.get("insert linestart", "insert lineend") + self.state = words, index, curinsert, curline + return "break" + + def getwords(self): + "Return a list of words that match the prefix before the cursor." + word = self.getprevword() + if not word: + return [] + before = self.text.get("1.0", "insert wordstart") + wbefore = re.findall(r"\b" + word + r"\w+\b", before) + del before + after = self.text.get("insert wordend", "end") + wafter = re.findall(r"\b" + word + r"\w+\b", after) + del after + if not wbefore and not wafter: + return [] + words = [] + dict = {} + # search backwards through words before + wbefore.reverse() + for w in wbefore: + if dict.get(w): + continue + words.append(w) + dict[w] = w + # search onwards through words after + for w in wafter: + if dict.get(w): + continue + words.append(w) + dict[w] = w + words.append(word) + return words + + def getprevword(self): + "Return the word prefix before the cursor." + line = self.text.get("insert linestart", "insert") + i = len(line) + while i > 0 and line[i-1] in self.wordchars: + i = i-1 + return line[i:] + +if __name__ == '__main__': + import unittest + unittest.main('idlelib.idle_test.test_autoexpand', verbosity=2) diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py new file mode 100644 index 0000000..d09c52f --- /dev/null +++ b/Lib/idlelib/browser.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("", 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/calltip_w.py b/Lib/idlelib/calltip_w.py new file mode 100644 index 0000000..8e68a76 --- /dev/null +++ b/Lib/idlelib/calltip_w.py @@ -0,0 +1,161 @@ +"""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 = "<>" +HIDE_SEQUENCES = ("", "") +CHECKHIDE_VIRTUAL_EVENT_NAME = "<>" +CHECKHIDE_SEQUENCES = ("", "") +CHECKHIDE_TIME = 100 # miliseconds + +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.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("<>", "(") + text.event_add("<>", ")") + text.bind("<>", calltip_show) + text.bind("<>", 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..81bd5f1 --- /dev/null +++ b/Lib/idlelib/calltips.py @@ -0,0 +1,175 @@ +"""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 inspect +import re +import sys +import textwrap +import types + +from idlelib import CallTipWindow +from idlelib.HyperParser import HyperParser + +class CallTips: + + menudefs = [ + ('edit', [ + ("Show call tip", "<>"), + ]) + ] + + 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 CallTipWindow.CallTip(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) + + 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.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: + 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.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 + +# 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('(?<=\()\w*\,?\s*') +_default_callable_argspec = "See source or doc" + + +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 = "" + try: + ob_call = ob.__call__ + except BaseException: + return argspec + if isinstance(ob, type): + fob = ob.__init__ + elif isinstance(ob_call, types.MethodType): + fob = ob_call + else: + fob = ob + if isinstance(fob, (types.FunctionType, types.MethodType)): + argspec = inspect.formatargspec(*inspect.getfullargspec(fob)) + if (isinstance(ob, (type, types.MethodType)) or + isinstance(ob_call, types.MethodType)): + argspec = _first_param.sub("", 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_calltips', verbosity=2) diff --git a/Lib/idlelib/codecontext.py b/Lib/idlelib/codecontext.py new file mode 100644 index 0000000..7d25ada --- /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 tkinter.constants import TOP, LEFT, X, W, SUNKEN +import re +from sys import maxsize 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', '<>')])] + 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('<>', 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 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 vertical padding + padx = 0 + for widget in widgets: + padx += widget.tk.getint(widget.pack_info()['padx']) + padx += widget.tk.getint(widget.cget('padx')) + # Calculate the required border width + border = 0 + for widget in widgets: + border += widget.tk.getint(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 range(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/colorizer.py new file mode 100644 index 0000000..9f31349 --- /dev/null +++ b/Lib/idlelib/colorizer.py @@ -0,0 +1,256 @@ +import time +import re +import keyword +import builtins +from idlelib.Delegator import Delegator +from idlelib.configHandler import idleConf + +DEBUG = False + +def any(name, alternates): + "Return a named group pattern matching list of alternates." + return "(?P<%s>" % name + "|".join(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] + # self.file = open("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"(\br|u|ur|R|U|UR|Ur|uR|b|B|br|Br|bR|BR|rb|rB|Rb|RB)?" + sqstring = stringprefix + r"'[^'\\\n]*(\\.[^'\\\n]*)*'?" + dqstring = stringprefix + r'"[^"\\\n]*(\\.[^"\\\n]*)*"?' + sq3string = stringprefix + r"'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?" + dq3string = stringprefix + r'"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?' + string = any("STRING", [sq3string, dq3string, sqstring, dqstring]) + return kw + "|" + builtin + "|" + comment + "|" + string +\ + "|" + any("SYNC", [r"\n"]) + +prog = re.compile(make_pat(), re.S) +idprog = re.compile(r"\s+(\w+)", re.S) + +class ColorDelegator(Delegator): + + def __init__(self): + Delegator.__init__(self) + self.prog = prog + self.idprog = idprog + self.LoadTagDefs() + + def setdelegate(self, delegate): + if self.delegate is not None: + self.unbind("<>") + Delegator.setdelegate(self, delegate) + if delegate is not None: + self.config_colors() + self.bind("<>", self.toggle_colorize_event) + self.notify_range("1.0", "end") + else: + # No delegate - stop any colorizing + self.stop_colorizing = True + self.allow_colorizing = False + + def config_colors(self): + for tag, cnf in self.tagdefs.items(): + if cnf: + self.tag_configure(tag, **cnf) + self.tag_raise('sel') + + def LoadTagDefs(self): + theme = idleConf.CurrentTheme() + self.tagdefs = { + "COMMENT": idleConf.GetHighlight(theme, "comment"), + "KEYWORD": idleConf.GetHighlight(theme, "keyword"), + "BUILTIN": idleConf.GetHighlight(theme, "builtin"), + "STRING": idleConf.GetHighlight(theme, "string"), + "DEFINITION": idleConf.GetHighlight(theme, "definition"), + "SYNC": {'background':None,'foreground':None}, + "TODO": {'background':None,'foreground':None}, + "ERROR": idleConf.GetHighlight(theme, "error"), + # The following is used by ReplaceDialog: + "hit": idleConf.GetHighlight(theme, "hit"), + } + + if DEBUG: print('tagdefs',self.tagdefs) + + def insert(self, index, chars, tags=None): + index = self.index(index) + self.delegate.insert(index, chars, tags) + self.notify_range(index, index + "+%dc" % len(chars)) + + def delete(self, index1, index2=None): + 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): + self.tag_add("TODO", index1, index2) + if self.after_id: + if DEBUG: print("colorizing already scheduled") + return + if self.colorizing: + self.stop_colorizing = True + if DEBUG: print("stop colorizing") + if self.allow_colorizing: + if DEBUG: print("schedule colorizing") + self.after_id = self.after(1, self.recolorize) + + 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") + 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): + if self.after_id: + after_id = self.after_id + self.after_id = None + if DEBUG: print("cancel scheduled recolorizer") + self.after_cancel(after_id) + if self.allow_colorizing and self.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") + return "break" + + def recolorize(self): + self.after_id = None + if not self.delegate: + if DEBUG: print("no delegate") + return + if not self.allow_colorizing: + if DEBUG: print("auto colorizing is off") + return + if self.colorizing: + if DEBUG: print("already colorizing") + return + try: + self.stop_colorizing = False + self.colorizing = True + if DEBUG: print("colorizing...") + t0 = time.perf_counter() + self.recolorize_main() + t1 = time.perf_counter() + 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") + 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): + next = "1.0" + while True: + item = self.tag_nextrange("TODO", next) + if not item: + break + head, tail = item + self.tag_remove("SYNC", head, tail) + item = self.tag_prevrange("SYNC", head) + if item: + head = item[1] + else: + head = "1.0" + + chars = "" + next = head + lines_to_get = 1 + ok = False + while not ok: + mark = next + next = self.index(mark + "+%d lines linestart" % + lines_to_get) + lines_to_get = min(lines_to_get * 2, 100) + ok = "SYNC" in self.tag_names(next + "-1c") + line = self.get(mark, next) + ##print head, "get", mark, next, "->", repr(line) + if not line: + return + for tag in self.tagdefs: + self.tag_remove(tag, mark, next) + chars = chars + line + m = self.prog.search(chars) + while m: + for key, value in m.groupdict().items(): + if value: + a, b = m.span(key) + self.tag_add(key, + head + "+%dc" % a, + head + "+%dc" % b) + if value in ("def", "class"): + m1 = self.idprog.match(chars, b) + if m1: + a, b = m1.span(1) + self.tag_add("DEFINITION", + head + "+%dc" % a, + head + "+%dc" % b) + m = self.prog.search(chars, m.end()) + if "SYNC" in self.tag_names(next + "-1c"): + head = next + chars = "" + else: + ok = False + if not ok: + # We're in an inconsistent state, and the call to + # update may tell us to stop. It may also change + # the correct value for "next" (since this is a + # line.col string, not a true mark). So leave a + # crumb telling the next invocation to resume here + # in case update tells us to leave. + self.tag_add("TODO", next) + self.update() + if self.stop_colorizing: + if DEBUG: print("colorizing stopped") + return + + def removecolors(self): + for tag in self.tagdefs: + self.tag_remove(tag, "1.0", "end") + +def _color_delegator(parent): # htest # + from tkinter import Toplevel, Text + from idlelib.Percolator import Percolator + + top = Toplevel(parent) + top.title("Test ColorDelegator") + top.geometry("200x100+%d+%d" % (parent.winfo_rootx() + 200, + parent.winfo_rooty() + 150)) + source = "if somename: x = 'abc' # comment\nprint\n" + text = Text(top, background="white") + text.pack(expand=1, fill="both") + text.insert("insert", source) + text.focus_set() + + p = Percolator(text) + d = ColorDelegator() + p.insertfilter(d) + +if __name__ == "__main__": + from idlelib.idle_test.htest import run + run(_color_delegator) diff --git a/Lib/idlelib/config.py b/Lib/idlelib/config.py new file mode 100644 index 0000000..8ac1f60 --- /dev/null +++ b/Lib/idlelib/config.py @@ -0,0 +1,760 @@ +"""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 +idle. This is to allow IDLE to continue to function in spite of errors in +the retrieval of config information. When a default is returned instead of +a requested config value, a message is printed to stderr to aid in +configuration problem notification and resolution. +""" +# TODOs added Oct 2014, tjr + +import os +import sys + +from configparser import ConfigParser +from tkinter import TkVersion +from tkinter.font import Font, nametofont + +class InvalidConfigType(Exception): pass +class InvalidConfigSet(Exception): pass +class InvalidFgBg(Exception): pass +class InvalidTheme(Exception): pass + +class IdleConfParser(ConfigParser): + """ + A ConfigParser specialised for idle configuration file handling + """ + def __init__(self, cfgFile, cfgDefaults=None): + """ + cfgFile - string, fully specified configuration file name + """ + self.file = cfgFile + ConfigParser.__init__(self, defaults=cfgDefaults, strict=False) + + def Get(self, section, option, type=None, default=None, raw=False): + """ + Get an option value for given section/option or return default. + If type is specified, return as type. + """ + # TODO Use default as fallback, at least if not None + # Should also print Warning(file, section, option). + # Currently may raise ValueError + if not self.has_option(section, option): + return default + if type == 'bool': + return self.getboolean(section, option) + elif type == 'int': + return self.getint(section, option) + else: + return self.get(section, option, raw=raw) + + def GetOptionList(self, section): + "Return a list of options for given section, else []." + if self.has_section(section): + return self.options(section) + else: #return a default value + return [] + + def Load(self): + "Load the configuration file from disk." + 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. + + Add section if required. False means option already had value. + """ + if self.has_option(section, option): + if self.get(section, option) == value: + return False + else: + self.set(section, option, value) + return True + else: + if not self.has_section(section): + self.add_section(section) + self.set(section, option, value) + return True + + 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. + + 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. + + """ + if not self.IsEmpty(): + fname = self.file + try: + cfgFile = open(fname, 'w') + except OSError: + 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. + + Default config files, self.defaultCfg -- + for config_type in self.config_types: + (idle install dir)/config-{config-type}.def + + User config files, self.userCfg -- + for config_type in self.config_types: + (user home dir)/.idlerc/config-{config-type}.cfg + """ + def __init__(self): + self.config_types = ('main', 'extensions', 'highlight', 'keys') + self.defaultCfg = {} + self.userCfg = {} + self.cfg = {} # TODO use to select userCfg vs defaultCfg + self.CreateConfigHandlers() + self.LoadCfgFiles() + + + def CreateConfigHandlers(self): + "Populate default and user config parser dictionaries." + #build idle install path + if __name__ != '__main__': # we were imported + idleDir=os.path.dirname(__file__) + else: # we were exec'ed (for testing only) + idleDir=os.path.abspath(sys.path[0]) + userDir=self.GetUserCfgDir() + + 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. + + Creates it if required. + """ + cfgDir = '.idlerc' + userDir = os.path.expanduser('~') + if userDir != '~': # expanduser() found user home dir + if not os.path.exists(userDir): + warn = ('\n Warning: os.path.expanduser("~") points to\n ' + + userDir + ',\n but the path does not exist.') + try: + print(warn, file=sys.stderr) + except OSError: + pass + userDir = '~' + if userDir == "~": # still no path to home! + # traditionally IDLE has defaulted to os.getcwd(), is this adequate? + userDir = os.getcwd() + userDir = os.path.join(userDir, cfgDir) + if not os.path.exists(userDir): + try: + os.mkdir(userDir) + except OSError: + warn = ('\n Warning: unable to create user config directory\n' + + userDir + '\n Check path and permissions.\n Exiting!\n') + print(warn, file=sys.stderr) + raise SystemExit + # TODO continue without userDIr instead of exit + return userDir + + def GetOption(self, configType, section, option, default=None, type=None, + warn_on_default=True, raw=False): + """Return a value for configType section option, or default. + + If type is not None, return a value of that type. Also pass raw + to the config parser. First try to return a valid value + (including type) from a user configuration. If that fails, try + the default configuration. If that fails, return default, with a + default of None. + + Warn if either user or default configurations have an invalid value. + Warn if default is returned and warn_on_default is True. + """ + try: + if self.userCfg[configType].has_option(section, option): + return self.userCfg[configType].Get(section, option, + type=type, raw=raw) + except ValueError: + warning = ('\n Warning: configHandler.py - IdleConf.GetOption -\n' + ' invalid %r value for configuration option %r\n' + ' from section %r: %r' % + (type, option, section, + self.userCfg[configType].Get(section, option, raw=raw))) + try: + print(warning, file=sys.stderr) + except OSError: + pass + try: + if self.defaultCfg[configType].has_option(section,option): + return self.defaultCfg[configType].Get( + section, option, type=type, raw=raw) + except ValueError: + pass + #returning default, print warning + if warn_on_default: + 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)) + try: + print(warning, file=sys.stderr) + except OSError: + pass + return default + + def SetOption(self, configType, section, option, value): + """Set section option to value in user config file.""" + self.userCfg[configType].SetOption(section, option, value) + + def GetSectionList(self, configSet, configType): + """Return sections for configSet configType configuration. + + configSet must be either 'user' or 'default' + configType must be in self.config_types. + """ + if not (configType in self.config_types): + raise InvalidConfigType('Invalid configType specified') + if configSet == 'user': + cfgParser = self.userCfg[configType] + elif configSet == 'default': + cfgParser=self.defaultCfg[configType] + else: + raise InvalidConfigSet('Invalid configSet specified') + return cfgParser.sections() + + def GetHighlight(self, theme, element, fgBg=None): + """Return individual theme element highlight color(s). + + fgBg - string ('fg' or 'bg') or None. + If None, return a dictionary containing fg and bg colors with + keys 'foreground' and 'background'. Otherwise, only return + fg or bg color, as specified. Colors are intended to be + appropriate for passing to Tkinter in, e.g., a tag_config call). + """ + if self.defaultCfg['highlight'].has_section(theme): + themeDict = self.GetThemeDict('default', theme) + 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. + + type - string, 'default' or 'user' theme type + themeName - string, theme name + Values are loaded over ultimate fallback defaults to guarantee + that all theme elements are present in a newly created theme. + """ + if type == 'user': + cfgParser = self.userCfg['highlight'] + elif type == 'default': + cfgParser = self.defaultCfg['highlight'] + else: + raise InvalidTheme('Invalid theme type specified') + # Provide foreground and background colors for each theme + # element (other than cursor) even though some values are not + # yet used by idle, to allow for their use in the future. + # Default values are generally black and white. + # TODO copy theme from a class attribute. + theme ={'normal-foreground':'#000000', + 'normal-background':'#ffffff', + 'keyword-foreground':'#000000', + 'keyword-background':'#ffffff', + 'builtin-foreground':'#000000', + 'builtin-background':'#ffffff', + 'comment-foreground':'#000000', + 'comment-background':'#ffffff', + 'string-foreground':'#000000', + 'string-background':'#ffffff', + 'definition-foreground':'#000000', + 'definition-background':'#ffffff', + 'hilite-foreground':'#000000', + 'hilite-background':'gray', + 'break-foreground':'#ffffff', + 'break-background':'#000000', + 'hit-foreground':'#ffffff', + 'hit-background':'#000000', + 'error-foreground':'#ffffff', + 'error-background':'#000000', + #cursor (only foreground can be set) + 'cursor-foreground':'#000000', + #shell window + 'stdout-foreground':'#000000', + 'stdout-background':'#ffffff', + 'stderr-foreground':'#000000', + 'stderr-background':'#ffffff', + 'console-foreground':'#000000', + 'console-background':'#ffffff' } + for element in theme: + if not cfgParser.has_option(themeName, element): + # Print warning that will return a default color + warning = ('\n Warning: configHandler.IdleConf.GetThemeDict' + ' -\n problem retrieving theme element %r' + '\n from theme %r.\n' + ' returning default color: %r' % + (element, themeName, theme[element])) + try: + print(warning, file=sys.stderr) + except OSError: + 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. + + idlelib.config-main.def includes this section + [Theme] + default= 1 + name= IDLE Classic + name2= + # name2 set in user config-main.cfg for themes added after 2015 Oct 1 + + Item name2 is needed because setting name to a new builtin + causes older IDLEs to display multiple error messages or quit. + See https://bugs.python.org/issue25313. + When default = True, name2 takes precedence over name, + while older IDLEs will just use name. + """ + default = self.GetOption('main', 'Theme', 'default', + type='bool', default=True) + if default: + theme = self.GetOption('main', 'Theme', 'name2', default='') + if default and not theme or not default: + theme = self.GetOption('main', 'Theme', 'name', default='') + source = self.defaultCfg if default else self.userCfg + if source['highlight'].has_section(theme): + return theme + else: + return "IDLE Classic" + + def CurrentKeys(self): + "Return the name of the currently active key set." + return self.GetOption('main', 'Keys', 'name', default='') + + def GetExtensions(self, active_only=True, editor_only=False, shell_only=False): + """Return extensions in default and user config-extensions files. + + If active_only True, only return active (enabled) extensions + and optionally only editor or shell extensions. + If active_only False, return all extensions. + """ + extns = self.RemoveKeyBindNames( + self.GetSectionList('default', 'extensions')) + userExtns = self.RemoveKeyBindNames( + self.GetSectionList('user', 'extensions')) + for extn in userExtns: + if extn not in extns: #user has added own extension + extns.append(extn) + if active_only: + activeExtns = [] + 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 if both, contradictory + if editor_only: + option = "enable_editor" + else: + option = "enable_shell" + if self.GetOption('extensions', extn,option, + default=True, type='bool', + warn_on_default=False): + activeExtns.append(extn) + else: + activeExtns.append(extn) + return activeExtns + else: + return extns + + def RemoveKeyBindNames(self, extnNameList): + "Return extnNameList with keybinding section names removed." + # TODO Easier to return filtered copy with list comp + names = extnNameList + kbNameIndicies = [] + for name in names: + if name.endswith(('_bindings', '_cfgBindings')): + kbNameIndicies.append(names.index(name)) + kbNameIndicies.sort(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. + + virtualEvent - string, name of the virtual event to test for, + without the enclosing '<< >>' + """ + extName = None + vEvent = '<<' + virtualEvent + '>>' + for extn in self.GetExtensions(active_only=0): + for event in self.GetExtensionKeys(extn): + if event == vEvent: + extName = extn # TODO return here? + return extName + + def GetExtensionKeys(self, extensionName): + """Return dict: {configurable extensionName event : active keybinding}. + + Events come from default config extension_cfgBindings section. + Keybindings come from GetCurrentKeySet() active key dict, + where previously used bindings are disabled. + """ + keysName = extensionName + '_cfgBindings' + activeKeys = self.GetCurrentKeySet() + extKeys = {} + if self.defaultCfg['extensions'].has_section(keysName): + eventNames = self.defaultCfg['extensions'].GetOptionList(keysName) + for eventName in eventNames: + event = '<<' + eventName + '>>' + binding = activeKeys[event] + extKeys[event] = binding + return extKeys + + def __GetRawExtensionKeys(self,extensionName): + """Return dict {configurable extensionName event : keybinding list}. + + Events come from default config extension_cfgBindings section. + Keybindings list come from the splitting of GetOption, which + tries user config before default config. + """ + keysName = extensionName+'_cfgBindings' + extKeys = {} + if self.defaultCfg['extensions'].has_section(keysName): + eventNames = self.defaultCfg['extensions'].GetOptionList(keysName) + for eventName in eventNames: + binding = self.GetOption( + 'extensions', keysName, eventName, default='').split() + event = '<<' + eventName + '>>' + extKeys[event] = binding + return extKeys + + def GetExtensionBindings(self, extensionName): + """Return dict {extensionName event : active or defined keybinding}. + + Augment self.GetExtensionKeys(extensionName) with mapping of non- + configurable events (from default config) to GetOption splits, + as in self.__GetRawExtensionKeys. + """ + bindsName = extensionName + '_bindings' + extBinds = self.GetExtensionKeys(extensionName) + #add the non-configurable bindings + if self.defaultCfg['extensions'].has_section(bindsName): + eventNames = self.defaultCfg['extensions'].GetOptionList(bindsName) + for eventName in eventNames: + binding = self.GetOption( + 'extensions', bindsName, eventName, default='').split() + event = '<<' + eventName + '>>' + extBinds[event] = binding + + return extBinds + + def GetKeyBinding(self, keySetName, eventStr): + """Return the keybinding list for keySetName eventStr. + + keySetName - name of key binding set (config-keys section). + eventStr - virtual event, including brackets, as in '<>'. + """ + eventName = eventStr[2:-2] #trim off the angle brackets + binding = self.GetOption('keys', keySetName, eventName, default='').split() + return binding + + def GetCurrentKeySet(self): + "Return CurrentKeys with 'darwin' modifications." + result = self.GetKeySet(self.CurrentKeys()) + + if sys.platform == "darwin": + # OS X Tk variants do not support the "Alt" keyboard modifier. + # So replace all keybingings that use "Alt" with ones that + # use the "Option" keyboard modifier. + # TODO (Ned?): the "Option" modifier does not work properly for + # Cocoa Tk and XQuartz Tk so we should not use it + # in default OS X KeySets. + for k, v in result.items(): + v2 = [ x.replace('>' + """ + return ('<<'+virtualEvent+'>>') in self.GetCoreKeys() + +# TODO make keyBindins a file or class attribute used for test above +# and copied in function below + + def GetCoreKeys(self, keySetName=None): + """Return dict of core virtual-key keybindings for keySetName. + + The default keySetName None corresponds to the keyBindings base + dict. If keySetName is not None, bindings from the config + file(s) are loaded _over_ these defaults, so if there is a + problem getting any core binding there will be an 'ultimate last + resort fallback' to the CUA-ish bindings defined here. + """ + keyBindings={ + '<>': ['', ''], + '<>': ['', ''], + '<>': ['', ''], + '<>': ['', ''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': ['', ''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': ['', ''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': [''] + } + if keySetName: + 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 OSError: + pass + return keyBindings + + def GetExtraHelpSourceList(self, configSet): + """Return list of extra help sources from a given configSet. + + Valid configSets are 'user' or 'default'. Return a list of tuples of + the form (menu_item , path_to_help_file , option), or return the empty + list. 'option' is the sequence number of the help resource. 'option' + values determine the position of the menu items on the Help menu, + therefore the returned list must be sorted by 'option'. + + """ + helpSources = [] + if configSet == 'user': + cfgParser = self.userCfg['main'] + elif configSet == 'default': + cfgParser = self.defaultCfg['main'] + else: + raise InvalidConfigSet('Invalid configSet specified') + options=cfgParser.GetOptionList('HelpFiles') + for option in options: + value=cfgParser.Get('HelpFiles', option, default=';') + if value.find(';') == -1: #malformed config entry with no ';' + menuItem = '' #make these empty + helpPath = '' #so value won't be added to list + else: #config entry contains ';' as expected + value=value.split(';') + menuItem=value[0].strip() + helpPath=value[1].strip() + if menuItem and helpPath: #neither are empty strings + helpSources.append( (menuItem,helpPath,option) ) + helpSources.sort(key=lambda x: x[2]) + return helpSources + + def GetAllExtraHelpSourcesList(self): + """Return a list of the details of all additional help sources. + + Tuples in the list are those of GetExtraHelpSourceList. + """ + allHelpSources = (self.GetExtraHelpSourceList('default') + + self.GetExtraHelpSourceList('user') ) + return allHelpSources + + def GetFont(self, root, configType, section): + """Retrieve a font from configuration (font, font-size, font-bold) + Intercept the special value 'TkFixedFont' and substitute + the actual font, factoring in some tweaks if needed for + appearance sakes. + + The 'root' parameter can normally be any valid Tkinter widget. + + Return a tuple (family, size, weight) suitable for passing + to tkinter.Font + """ + family = self.GetOption(configType, section, 'font', default='courier') + size = self.GetOption(configType, section, 'font-size', type='int', + default='10') + bold = self.GetOption(configType, section, 'font-bold', default=0, + type='bool') + if (family == 'TkFixedFont'): + if TkVersion < 8.5: + family = 'Courier' + else: + f = Font(name='TkFixedFont', exists=True, root=root) + actualFont = Font.actual(f) + family = actualFont['family'] + size = actualFont['size'] + if size <= 0: + size = 10 # if font in pixels, ignore actual size + bold = actualFont['weight']=='bold' + return (family, size, 'bold' if bold else 'normal') + + def LoadCfgFiles(self): + "Load all configuration files." + for key in self.defaultCfg: + self.defaultCfg[key].Load() + self.userCfg[key].Load() #same keys + + def SaveUserCfgFiles(self): + "Write all loaded user configuration files to disk." + for key in self.userCfg: + self.userCfg[key].Save() + + +idleConf = IdleConf() + +# TODO Revise test output, write expanded unittest +### module test +if __name__ == '__main__': + def dumpCfg(cfg): + print('\n', cfg, '\n') + for key in cfg: + sections = cfg[key].sections() + print(key) + print(sections) + for section in sections: + options = cfg[key].options(section) + print(section) + print(options) + for option in options: + print(option, '=', cfg[key].Get(section, option)) + dumpCfg(idleConf.defaultCfg) + dumpCfg(idleConf.userCfg) + print(idleConf.userCfg['main'].Get('Theme', 'name')) + #print idleConf.userCfg['highlight'].GetDefHighlight('Foo','normal') diff --git a/Lib/idlelib/configDialog.py b/Lib/idlelib/configDialog.py deleted file mode 100644 index b702253..0000000 --- a/Lib/idlelib/configDialog.py +++ /dev/null @@ -1,1434 +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. - -""" -from tkinter import * -import tkinter.messagebox as tkMessageBox -import tkinter.colorchooser as tkColorChooser -import tkinter.font as 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('', self.Cancel) #dismiss dialog, no save - #self.bind('', self.Apply) #apply changes, save - #self.bind('', 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( - '', 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('', lambda e: 'break') - text.bind('', lambda e: 'break') - textAndTags=( - ('#you can click here', 'comment'), ('\n', 'normal'), - ('#to choose items', 'comment'), ('\n', 'normal'), - ('def', 'keyword'), (' ', 'normal'), - ('func', 'definition'), ('(param):\n ', 'normal'), - ('"""string"""', 'string'), ('\n var0 = ', 'normal'), - ("'string'", 'string'), ('\n var1 = ', 'normal'), - ("'selected'", 'hilite'), ('\n var2 = ', 'normal'), - ("'found'", 'hit'), ('\n var3 = ', 'normal'), - ('list', 'builtin'), ('(', 'normal'), - ('None', 'keyword'), (')\n', 'normal'), - (' breakpoint("line")', 'break'), ('\n\n', 'normal'), - (' error ', 'error'), (' ', 'normal'), - ('cursor |', 'cursor'), ('\n ', 'normal'), - ('shell', 'console'), (' ', 'normal'), - ('stdout', 'stdout'), (' ', 'normal'), - ('stderr', 'stderr'), ('\n', 'normal')) - for txTa in textAndTags: - text.insert(END, txTa[0], txTa[1]) - for element in self.themeElements: - def tem(event, elem=element): - event.widget.winfo_toplevel().highlightTarget.set(elem) - text.tag_bind( - self.themeElements[element][0], '', 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('', 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) - 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) - #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('', 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) - 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) - #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): - "Remove callbacks to prevent memory leaks." - 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 = list(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 = list(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 - #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.Apply() - 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 - #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.Apply() - self.SetThemeType() - - def GetColour(self): - target = self.highlightTarget.get() - prevColour = self.frameColourSet.cget('bg') - rgbTuplet, colourString = tkColorChooser.askcolor( - parent=self, title='Pick new colour for : '+target, - initialcolor=prevColour) - if colourString and (colourString != prevColour): - #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'), 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 = list(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.keys() - 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.destroy() - - def Ok(self): - self.Apply() - 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('<>', 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_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_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/configHandler.py b/Lib/idlelib/configHandler.py deleted file mode 100644 index 8ac1f60..0000000 --- a/Lib/idlelib/configHandler.py +++ /dev/null @@ -1,760 +0,0 @@ -"""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 -idle. This is to allow IDLE to continue to function in spite of errors in -the retrieval of config information. When a default is returned instead of -a requested config value, a message is printed to stderr to aid in -configuration problem notification and resolution. -""" -# TODOs added Oct 2014, tjr - -import os -import sys - -from configparser import ConfigParser -from tkinter import TkVersion -from tkinter.font import Font, nametofont - -class InvalidConfigType(Exception): pass -class InvalidConfigSet(Exception): pass -class InvalidFgBg(Exception): pass -class InvalidTheme(Exception): pass - -class IdleConfParser(ConfigParser): - """ - A ConfigParser specialised for idle configuration file handling - """ - def __init__(self, cfgFile, cfgDefaults=None): - """ - cfgFile - string, fully specified configuration file name - """ - self.file = cfgFile - ConfigParser.__init__(self, defaults=cfgDefaults, strict=False) - - def Get(self, section, option, type=None, default=None, raw=False): - """ - Get an option value for given section/option or return default. - If type is specified, return as type. - """ - # TODO Use default as fallback, at least if not None - # Should also print Warning(file, section, option). - # Currently may raise ValueError - if not self.has_option(section, option): - return default - if type == 'bool': - return self.getboolean(section, option) - elif type == 'int': - return self.getint(section, option) - else: - return self.get(section, option, raw=raw) - - def GetOptionList(self, section): - "Return a list of options for given section, else []." - if self.has_section(section): - return self.options(section) - else: #return a default value - return [] - - def Load(self): - "Load the configuration file from disk." - 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. - - Add section if required. False means option already had value. - """ - if self.has_option(section, option): - if self.get(section, option) == value: - return False - else: - self.set(section, option, value) - return True - else: - if not self.has_section(section): - self.add_section(section) - self.set(section, option, value) - return True - - 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. - - 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. - - """ - if not self.IsEmpty(): - fname = self.file - try: - cfgFile = open(fname, 'w') - except OSError: - 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. - - Default config files, self.defaultCfg -- - for config_type in self.config_types: - (idle install dir)/config-{config-type}.def - - User config files, self.userCfg -- - for config_type in self.config_types: - (user home dir)/.idlerc/config-{config-type}.cfg - """ - def __init__(self): - self.config_types = ('main', 'extensions', 'highlight', 'keys') - self.defaultCfg = {} - self.userCfg = {} - self.cfg = {} # TODO use to select userCfg vs defaultCfg - self.CreateConfigHandlers() - self.LoadCfgFiles() - - - def CreateConfigHandlers(self): - "Populate default and user config parser dictionaries." - #build idle install path - if __name__ != '__main__': # we were imported - idleDir=os.path.dirname(__file__) - else: # we were exec'ed (for testing only) - idleDir=os.path.abspath(sys.path[0]) - userDir=self.GetUserCfgDir() - - 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. - - Creates it if required. - """ - cfgDir = '.idlerc' - userDir = os.path.expanduser('~') - if userDir != '~': # expanduser() found user home dir - if not os.path.exists(userDir): - warn = ('\n Warning: os.path.expanduser("~") points to\n ' + - userDir + ',\n but the path does not exist.') - try: - print(warn, file=sys.stderr) - except OSError: - pass - userDir = '~' - if userDir == "~": # still no path to home! - # traditionally IDLE has defaulted to os.getcwd(), is this adequate? - userDir = os.getcwd() - userDir = os.path.join(userDir, cfgDir) - if not os.path.exists(userDir): - try: - os.mkdir(userDir) - except OSError: - warn = ('\n Warning: unable to create user config directory\n' + - userDir + '\n Check path and permissions.\n Exiting!\n') - print(warn, file=sys.stderr) - raise SystemExit - # TODO continue without userDIr instead of exit - return userDir - - def GetOption(self, configType, section, option, default=None, type=None, - warn_on_default=True, raw=False): - """Return a value for configType section option, or default. - - If type is not None, return a value of that type. Also pass raw - to the config parser. First try to return a valid value - (including type) from a user configuration. If that fails, try - the default configuration. If that fails, return default, with a - default of None. - - Warn if either user or default configurations have an invalid value. - Warn if default is returned and warn_on_default is True. - """ - try: - if self.userCfg[configType].has_option(section, option): - return self.userCfg[configType].Get(section, option, - type=type, raw=raw) - except ValueError: - warning = ('\n Warning: configHandler.py - IdleConf.GetOption -\n' - ' invalid %r value for configuration option %r\n' - ' from section %r: %r' % - (type, option, section, - self.userCfg[configType].Get(section, option, raw=raw))) - try: - print(warning, file=sys.stderr) - except OSError: - pass - try: - if self.defaultCfg[configType].has_option(section,option): - return self.defaultCfg[configType].Get( - section, option, type=type, raw=raw) - except ValueError: - pass - #returning default, print warning - if warn_on_default: - 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)) - try: - print(warning, file=sys.stderr) - except OSError: - pass - return default - - def SetOption(self, configType, section, option, value): - """Set section option to value in user config file.""" - self.userCfg[configType].SetOption(section, option, value) - - def GetSectionList(self, configSet, configType): - """Return sections for configSet configType configuration. - - configSet must be either 'user' or 'default' - configType must be in self.config_types. - """ - if not (configType in self.config_types): - raise InvalidConfigType('Invalid configType specified') - if configSet == 'user': - cfgParser = self.userCfg[configType] - elif configSet == 'default': - cfgParser=self.defaultCfg[configType] - else: - raise InvalidConfigSet('Invalid configSet specified') - return cfgParser.sections() - - def GetHighlight(self, theme, element, fgBg=None): - """Return individual theme element highlight color(s). - - fgBg - string ('fg' or 'bg') or None. - If None, return a dictionary containing fg and bg colors with - keys 'foreground' and 'background'. Otherwise, only return - fg or bg color, as specified. Colors are intended to be - appropriate for passing to Tkinter in, e.g., a tag_config call). - """ - if self.defaultCfg['highlight'].has_section(theme): - themeDict = self.GetThemeDict('default', theme) - 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. - - type - string, 'default' or 'user' theme type - themeName - string, theme name - Values are loaded over ultimate fallback defaults to guarantee - that all theme elements are present in a newly created theme. - """ - if type == 'user': - cfgParser = self.userCfg['highlight'] - elif type == 'default': - cfgParser = self.defaultCfg['highlight'] - else: - raise InvalidTheme('Invalid theme type specified') - # Provide foreground and background colors for each theme - # element (other than cursor) even though some values are not - # yet used by idle, to allow for their use in the future. - # Default values are generally black and white. - # TODO copy theme from a class attribute. - theme ={'normal-foreground':'#000000', - 'normal-background':'#ffffff', - 'keyword-foreground':'#000000', - 'keyword-background':'#ffffff', - 'builtin-foreground':'#000000', - 'builtin-background':'#ffffff', - 'comment-foreground':'#000000', - 'comment-background':'#ffffff', - 'string-foreground':'#000000', - 'string-background':'#ffffff', - 'definition-foreground':'#000000', - 'definition-background':'#ffffff', - 'hilite-foreground':'#000000', - 'hilite-background':'gray', - 'break-foreground':'#ffffff', - 'break-background':'#000000', - 'hit-foreground':'#ffffff', - 'hit-background':'#000000', - 'error-foreground':'#ffffff', - 'error-background':'#000000', - #cursor (only foreground can be set) - 'cursor-foreground':'#000000', - #shell window - 'stdout-foreground':'#000000', - 'stdout-background':'#ffffff', - 'stderr-foreground':'#000000', - 'stderr-background':'#ffffff', - 'console-foreground':'#000000', - 'console-background':'#ffffff' } - for element in theme: - if not cfgParser.has_option(themeName, element): - # Print warning that will return a default color - warning = ('\n Warning: configHandler.IdleConf.GetThemeDict' - ' -\n problem retrieving theme element %r' - '\n from theme %r.\n' - ' returning default color: %r' % - (element, themeName, theme[element])) - try: - print(warning, file=sys.stderr) - except OSError: - 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. - - idlelib.config-main.def includes this section - [Theme] - default= 1 - name= IDLE Classic - name2= - # name2 set in user config-main.cfg for themes added after 2015 Oct 1 - - Item name2 is needed because setting name to a new builtin - causes older IDLEs to display multiple error messages or quit. - See https://bugs.python.org/issue25313. - When default = True, name2 takes precedence over name, - while older IDLEs will just use name. - """ - default = self.GetOption('main', 'Theme', 'default', - type='bool', default=True) - if default: - theme = self.GetOption('main', 'Theme', 'name2', default='') - if default and not theme or not default: - theme = self.GetOption('main', 'Theme', 'name', default='') - source = self.defaultCfg if default else self.userCfg - if source['highlight'].has_section(theme): - return theme - else: - return "IDLE Classic" - - def CurrentKeys(self): - "Return the name of the currently active key set." - return self.GetOption('main', 'Keys', 'name', default='') - - def GetExtensions(self, active_only=True, editor_only=False, shell_only=False): - """Return extensions in default and user config-extensions files. - - If active_only True, only return active (enabled) extensions - and optionally only editor or shell extensions. - If active_only False, return all extensions. - """ - extns = self.RemoveKeyBindNames( - self.GetSectionList('default', 'extensions')) - userExtns = self.RemoveKeyBindNames( - self.GetSectionList('user', 'extensions')) - for extn in userExtns: - if extn not in extns: #user has added own extension - extns.append(extn) - if active_only: - activeExtns = [] - 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 if both, contradictory - if editor_only: - option = "enable_editor" - else: - option = "enable_shell" - if self.GetOption('extensions', extn,option, - default=True, type='bool', - warn_on_default=False): - activeExtns.append(extn) - else: - activeExtns.append(extn) - return activeExtns - else: - return extns - - def RemoveKeyBindNames(self, extnNameList): - "Return extnNameList with keybinding section names removed." - # TODO Easier to return filtered copy with list comp - names = extnNameList - kbNameIndicies = [] - for name in names: - if name.endswith(('_bindings', '_cfgBindings')): - kbNameIndicies.append(names.index(name)) - kbNameIndicies.sort(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. - - virtualEvent - string, name of the virtual event to test for, - without the enclosing '<< >>' - """ - extName = None - vEvent = '<<' + virtualEvent + '>>' - for extn in self.GetExtensions(active_only=0): - for event in self.GetExtensionKeys(extn): - if event == vEvent: - extName = extn # TODO return here? - return extName - - def GetExtensionKeys(self, extensionName): - """Return dict: {configurable extensionName event : active keybinding}. - - Events come from default config extension_cfgBindings section. - Keybindings come from GetCurrentKeySet() active key dict, - where previously used bindings are disabled. - """ - keysName = extensionName + '_cfgBindings' - activeKeys = self.GetCurrentKeySet() - extKeys = {} - if self.defaultCfg['extensions'].has_section(keysName): - eventNames = self.defaultCfg['extensions'].GetOptionList(keysName) - for eventName in eventNames: - event = '<<' + eventName + '>>' - binding = activeKeys[event] - extKeys[event] = binding - return extKeys - - def __GetRawExtensionKeys(self,extensionName): - """Return dict {configurable extensionName event : keybinding list}. - - Events come from default config extension_cfgBindings section. - Keybindings list come from the splitting of GetOption, which - tries user config before default config. - """ - keysName = extensionName+'_cfgBindings' - extKeys = {} - if self.defaultCfg['extensions'].has_section(keysName): - eventNames = self.defaultCfg['extensions'].GetOptionList(keysName) - for eventName in eventNames: - binding = self.GetOption( - 'extensions', keysName, eventName, default='').split() - event = '<<' + eventName + '>>' - extKeys[event] = binding - return extKeys - - def GetExtensionBindings(self, extensionName): - """Return dict {extensionName event : active or defined keybinding}. - - Augment self.GetExtensionKeys(extensionName) with mapping of non- - configurable events (from default config) to GetOption splits, - as in self.__GetRawExtensionKeys. - """ - bindsName = extensionName + '_bindings' - extBinds = self.GetExtensionKeys(extensionName) - #add the non-configurable bindings - if self.defaultCfg['extensions'].has_section(bindsName): - eventNames = self.defaultCfg['extensions'].GetOptionList(bindsName) - for eventName in eventNames: - binding = self.GetOption( - 'extensions', bindsName, eventName, default='').split() - event = '<<' + eventName + '>>' - extBinds[event] = binding - - return extBinds - - def GetKeyBinding(self, keySetName, eventStr): - """Return the keybinding list for keySetName eventStr. - - keySetName - name of key binding set (config-keys section). - eventStr - virtual event, including brackets, as in '<>'. - """ - eventName = eventStr[2:-2] #trim off the angle brackets - binding = self.GetOption('keys', keySetName, eventName, default='').split() - return binding - - def GetCurrentKeySet(self): - "Return CurrentKeys with 'darwin' modifications." - result = self.GetKeySet(self.CurrentKeys()) - - if sys.platform == "darwin": - # OS X Tk variants do not support the "Alt" keyboard modifier. - # So replace all keybingings that use "Alt" with ones that - # use the "Option" keyboard modifier. - # TODO (Ned?): the "Option" modifier does not work properly for - # Cocoa Tk and XQuartz Tk so we should not use it - # in default OS X KeySets. - for k, v in result.items(): - v2 = [ x.replace('>' - """ - return ('<<'+virtualEvent+'>>') in self.GetCoreKeys() - -# TODO make keyBindins a file or class attribute used for test above -# and copied in function below - - def GetCoreKeys(self, keySetName=None): - """Return dict of core virtual-key keybindings for keySetName. - - The default keySetName None corresponds to the keyBindings base - dict. If keySetName is not None, bindings from the config - file(s) are loaded _over_ these defaults, so if there is a - problem getting any core binding there will be an 'ultimate last - resort fallback' to the CUA-ish bindings defined here. - """ - keyBindings={ - '<>': ['', ''], - '<>': ['', ''], - '<>': ['', ''], - '<>': ['', ''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': ['', ''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': ['', ''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''], - '<>': [''] - } - if keySetName: - 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 OSError: - pass - return keyBindings - - def GetExtraHelpSourceList(self, configSet): - """Return list of extra help sources from a given configSet. - - Valid configSets are 'user' or 'default'. Return a list of tuples of - the form (menu_item , path_to_help_file , option), or return the empty - list. 'option' is the sequence number of the help resource. 'option' - values determine the position of the menu items on the Help menu, - therefore the returned list must be sorted by 'option'. - - """ - helpSources = [] - if configSet == 'user': - cfgParser = self.userCfg['main'] - elif configSet == 'default': - cfgParser = self.defaultCfg['main'] - else: - raise InvalidConfigSet('Invalid configSet specified') - options=cfgParser.GetOptionList('HelpFiles') - for option in options: - value=cfgParser.Get('HelpFiles', option, default=';') - if value.find(';') == -1: #malformed config entry with no ';' - menuItem = '' #make these empty - helpPath = '' #so value won't be added to list - else: #config entry contains ';' as expected - value=value.split(';') - menuItem=value[0].strip() - helpPath=value[1].strip() - if menuItem and helpPath: #neither are empty strings - helpSources.append( (menuItem,helpPath,option) ) - helpSources.sort(key=lambda x: x[2]) - return helpSources - - def GetAllExtraHelpSourcesList(self): - """Return a list of the details of all additional help sources. - - Tuples in the list are those of GetExtraHelpSourceList. - """ - allHelpSources = (self.GetExtraHelpSourceList('default') + - self.GetExtraHelpSourceList('user') ) - return allHelpSources - - def GetFont(self, root, configType, section): - """Retrieve a font from configuration (font, font-size, font-bold) - Intercept the special value 'TkFixedFont' and substitute - the actual font, factoring in some tweaks if needed for - appearance sakes. - - The 'root' parameter can normally be any valid Tkinter widget. - - Return a tuple (family, size, weight) suitable for passing - to tkinter.Font - """ - family = self.GetOption(configType, section, 'font', default='courier') - size = self.GetOption(configType, section, 'font-size', type='int', - default='10') - bold = self.GetOption(configType, section, 'font-bold', default=0, - type='bool') - if (family == 'TkFixedFont'): - if TkVersion < 8.5: - family = 'Courier' - else: - f = Font(name='TkFixedFont', exists=True, root=root) - actualFont = Font.actual(f) - family = actualFont['family'] - size = actualFont['size'] - if size <= 0: - size = 10 # if font in pixels, ignore actual size - bold = actualFont['weight']=='bold' - return (family, size, 'bold' if bold else 'normal') - - def LoadCfgFiles(self): - "Load all configuration files." - for key in self.defaultCfg: - self.defaultCfg[key].Load() - self.userCfg[key].Load() #same keys - - def SaveUserCfgFiles(self): - "Write all loaded user configuration files to disk." - for key in self.userCfg: - self.userCfg[key].Save() - - -idleConf = IdleConf() - -# TODO Revise test output, write expanded unittest -### module test -if __name__ == '__main__': - def dumpCfg(cfg): - print('\n', cfg, '\n') - for key in cfg: - sections = cfg[key].sections() - print(key) - print(sections) - for section in sections: - options = cfg[key].options(section) - print(section) - print(options) - for option in options: - print(option, '=', cfg[key].Get(section, option)) - dumpCfg(idleConf.defaultCfg) - dumpCfg(idleConf.userCfg) - print(idleConf.userCfg['main'].Get('Theme', 'name')) - #print idleConf.userCfg['highlight'].GetDefHighlight('Foo','normal') diff --git a/Lib/idlelib/configHelpSourceEdit.py b/Lib/idlelib/configHelpSourceEdit.py deleted file mode 100644 index cde8118..0000000 --- a/Lib/idlelib/configHelpSourceEdit.py +++ /dev/null @@ -1,170 +0,0 @@ -"Dialog to specify or edit the parameters for a user configured help source." - -import os -import sys - -from tkinter import * -import tkinter.messagebox as tkMessageBox -import tkinter.filedialog as 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.create_widgets() - 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('', self.ok) - self.wait_window() - - def create_widgets(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.browse_file) - 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 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.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 menu_ok(self): - "Simple validity check for a sensible menu item name" - menu_ok = 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() - menu_ok = 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() - menu_ok = False - return menu_ok - - def path_ok(self): - "Simple validity check for menu file path" - path_ok = 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() - path_ok = 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() - path_ok = False - return path_ok - - def ok(self, event=None): - if self.menu_ok() and self.path_ok(): - self.result = (self.menu.get().strip(), - self.path.get().strip()) - if sys.platform == 'darwin': - path = self.result[1] - if path.startswith(('www', 'file:', 'http:', 'https:')): - pass - else: - # Mac Safari insists on using the URI form for local files - self.result = list(self.result) - self.result[1] = "file://" + path - self.destroy() - - def cancel(self, event=None): - self.result = None - self.destroy() - -if __name__ == '__main__': - import unittest - unittest.main('idlelib.idle_test.test_config_help', - verbosity=2, exit=False) - - from idlelib.idle_test.htest import run - run(GetHelpSourceDialog) diff --git a/Lib/idlelib/configSectionNameDialog.py b/Lib/idlelib/configSectionNameDialog.py deleted file mode 100644 index 5137836..0000000 --- a/Lib/idlelib/configSectionNameDialog.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -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 tkinter.messagebox as 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.destroy() - - def Cancel(self, event=None): - self.result = '' - 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_help.py b/Lib/idlelib/config_help.py new file mode 100644 index 0000000..cde8118 --- /dev/null +++ b/Lib/idlelib/config_help.py @@ -0,0 +1,170 @@ +"Dialog to specify or edit the parameters for a user configured help source." + +import os +import sys + +from tkinter import * +import tkinter.messagebox as tkMessageBox +import tkinter.filedialog as 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.create_widgets() + 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('', self.ok) + self.wait_window() + + def create_widgets(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.browse_file) + 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 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.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 menu_ok(self): + "Simple validity check for a sensible menu item name" + menu_ok = 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() + menu_ok = 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() + menu_ok = False + return menu_ok + + def path_ok(self): + "Simple validity check for menu file path" + path_ok = 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() + path_ok = 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() + path_ok = False + return path_ok + + def ok(self, event=None): + if self.menu_ok() and self.path_ok(): + self.result = (self.menu.get().strip(), + self.path.get().strip()) + if sys.platform == 'darwin': + path = self.result[1] + if path.startswith(('www', 'file:', 'http:', 'https:')): + pass + else: + # Mac Safari insists on using the URI form for local files + self.result = list(self.result) + self.result[1] = "file://" + path + self.destroy() + + def cancel(self, event=None): + self.result = None + self.destroy() + +if __name__ == '__main__': + import unittest + unittest.main('idlelib.idle_test.test_config_help', + verbosity=2, exit=False) + + from idlelib.idle_test.htest import run + run(GetHelpSourceDialog) diff --git a/Lib/idlelib/config_key.py b/Lib/idlelib/config_key.py new file mode 100644 index 0000000..e6438bf --- /dev/null +++ b/Lib/idlelib/config_key.py @@ -0,0 +1,266 @@ +""" +Dialog for building Tkinter accelerator key bindings +""" +from tkinter import * +import tkinter.messagebox as 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('',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: , , ,\n" + ", , .\n" + "Upper case is used when the Shift modifier is present!\n\n" + + "'Emacs style' multi-keystroke bindings are specified as\n" + + "follows: , 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., ." ) + 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('<' + '-'.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','F2','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: + 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.destroy() + + def Cancel(self, event=None): + self.result='' + 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/config_sec.py b/Lib/idlelib/config_sec.py new file mode 100644 index 0000000..5137836 --- /dev/null +++ b/Lib/idlelib/config_sec.py @@ -0,0 +1,98 @@ +""" +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 tkinter.messagebox as 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.destroy() + + def Cancel(self, event=None): + self.result = '' + 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/configdialog.py b/Lib/idlelib/configdialog.py new file mode 100644 index 0000000..b702253 --- /dev/null +++ b/Lib/idlelib/configdialog.py @@ -0,0 +1,1434 @@ +"""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 tkinter.messagebox as tkMessageBox +import tkinter.colorchooser as tkColorChooser +import tkinter.font as 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('', self.Cancel) #dismiss dialog, no save + #self.bind('', self.Apply) #apply changes, save + #self.bind('', 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( + '', 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('', lambda e: 'break') + text.bind('', lambda e: 'break') + textAndTags=( + ('#you can click here', 'comment'), ('\n', 'normal'), + ('#to choose items', 'comment'), ('\n', 'normal'), + ('def', 'keyword'), (' ', 'normal'), + ('func', 'definition'), ('(param):\n ', 'normal'), + ('"""string"""', 'string'), ('\n var0 = ', 'normal'), + ("'string'", 'string'), ('\n var1 = ', 'normal'), + ("'selected'", 'hilite'), ('\n var2 = ', 'normal'), + ("'found'", 'hit'), ('\n var3 = ', 'normal'), + ('list', 'builtin'), ('(', 'normal'), + ('None', 'keyword'), (')\n', 'normal'), + (' breakpoint("line")', 'break'), ('\n\n', 'normal'), + (' error ', 'error'), (' ', 'normal'), + ('cursor |', 'cursor'), ('\n ', 'normal'), + ('shell', 'console'), (' ', 'normal'), + ('stdout', 'stdout'), (' ', 'normal'), + ('stderr', 'stderr'), ('\n', 'normal')) + for txTa in textAndTags: + text.insert(END, txTa[0], txTa[1]) + for element in self.themeElements: + def tem(event, elem=element): + event.widget.winfo_toplevel().highlightTarget.set(elem) + text.tag_bind( + self.themeElements[element][0], '', 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('', 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) + 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) + #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('', 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) + 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) + #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): + "Remove callbacks to prevent memory leaks." + 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 = list(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 = list(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 + #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.Apply() + 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 + #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.Apply() + self.SetThemeType() + + def GetColour(self): + target = self.highlightTarget.get() + prevColour = self.frameColourSet.cget('bg') + rgbTuplet, colourString = tkColorChooser.askcolor( + parent=self, title='Pick new colour for : '+target, + initialcolor=prevColour) + if colourString and (colourString != prevColour): + #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'), 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 = list(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.keys() + 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.destroy() + + def Ok(self): + self.Apply() + 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('<>', 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_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_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/debugger.py b/Lib/idlelib/debugger.py new file mode 100644 index 0000000..d5e217d --- /dev/null +++ b/Lib/idlelib/debugger.py @@ -0,0 +1,539 @@ +import os +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 + bdb.Bdb.__init__(self) + + def user_line(self, frame): + if self.in_rpc_code(frame): + self.set_step() + return + message = self.__frame2message(frame) + try: + self.gui.interaction(message, frame) + except TclError: # When closing debugger window with [x] in 3.x + pass + + def user_exception(self, frame, info): + if self.in_rpc_code(frame): + self.set_step() + return + message = self.__frame2message(frame) + self.gui.interaction(message, frame, info) + + def in_rpc_code(self, frame): + if frame.f_code.co_filename.count('rpc.py'): + return True + else: + prev_frame = frame.f_back + 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) + + def __frame2message(self, frame): + code = frame.f_code + filename = code.co_filename + lineno = frame.f_lineno + basename = os.path.basename(filename) + message = "%s:%s" % (basename, lineno) + if code.co_name != "?": + message = "%s: %s()" % (message, code.co_name) + return message + + +class Debugger: + + vstack = vsource = vlocals = vglobals = None + + def __init__(self, pyshell, idb=None): + if idb is None: + idb = Idb(self) + self.pyshell = pyshell + self.idb = idb + self.frame = None + self.make_gui() + self.interacting = 0 + self.nesting_level = 0 + + def run(self, *args): + # Deal with the scenario where we've already got a program running + # in the debugger and we want to start another. If that is the case, + # our second 'run' was invoked from an event dispatched not from + # the main event loop, but from the nested event loop in 'interaction' + # below. So our stack looks something like this: + # outer main event loop + # run() + # + # callback to debugger's interaction() + # nested event loop + # run() for second command + # + # This kind of nesting of event loops causes all kinds of problems + # (see e.g. issue #24455) especially when dealing with running as a + # subprocess, where there's all kinds of extra stuff happening in + # there - insert a traceback.print_stack() to check it out. + # + # By this point, we've already called restart_subprocess() in + # ScriptBinding. However, we also need to unwind the stack back to + # that outer event loop. To accomplish this, we: + # - return immediately from the nested run() + # - abort_loop ensures the nested event loop will terminate + # - the debugger's interaction routine completes normally + # - the restart_subprocess() will have taken care of stopping + # the running program, which will also let the outer run complete + # + # That leaves us back at the outer main event loop, at which point our + # after event can fire, and we'll come back to this routine with a + # clean stack. + if self.nesting_level > 0: + self.abort_loop() + self.root.after(100, lambda: self.run(*args)) + return + try: + self.interacting = 1 + return self.idb.run(*args) + finally: + self.interacting = 0 + + def close(self, event=None): + try: + self.quit() + except Exception: + pass + if self.interacting: + self.top.bell() + return + if self.stackviewer: + self.stackviewer.close(); self.stackviewer = None + # Clean up pyshell if user clicked debugger control close widget. + # (Causes a harmless extra cycle through close_debugger() if user + # toggled debugger from pyshell Debug menu) + self.pyshell.close_debugger() + # Now close the debugger control window.... + self.top.destroy() + + def make_gui(self): + pyshell = self.pyshell + self.flist = pyshell.flist + self.root = root = pyshell.root + self.top = top = ListedToplevel(root) + self.top.wm_title("Debug Control") + self.top.wm_iconname("Debug") + top.wm_protocol("WM_DELETE_WINDOW", self.close) + self.top.bind("", self.close) + # + self.bframe = bframe = Frame(top) + self.bframe.pack(anchor="w") + self.buttons = bl = [] + # + self.bcont = b = Button(bframe, text="Go", command=self.cont) + bl.append(b) + self.bstep = b = Button(bframe, text="Step", command=self.step) + bl.append(b) + self.bnext = b = Button(bframe, text="Over", command=self.next) + bl.append(b) + self.bret = b = Button(bframe, text="Out", command=self.ret) + bl.append(b) + self.bret = b = Button(bframe, text="Quit", command=self.quit) + bl.append(b) + # + for b in bl: + b.configure(state="disabled") + b.pack(side="left") + # + self.cframe = cframe = Frame(bframe) + self.cframe.pack(side="left") + # + if not self.vstack: + self.__class__.vstack = BooleanVar(top) + self.vstack.set(1) + self.bstack = Checkbutton(cframe, + text="Stack", command=self.show_stack, variable=self.vstack) + self.bstack.grid(row=0, column=0) + if not self.vsource: + self.__class__.vsource = BooleanVar(top) + self.bsource = Checkbutton(cframe, + text="Source", command=self.show_source, variable=self.vsource) + self.bsource.grid(row=0, column=1) + if not self.vlocals: + self.__class__.vlocals = BooleanVar(top) + self.vlocals.set(1) + self.blocals = Checkbutton(cframe, + text="Locals", command=self.show_locals, variable=self.vlocals) + self.blocals.grid(row=1, column=0) + if not self.vglobals: + self.__class__.vglobals = BooleanVar(top) + self.bglobals = Checkbutton(cframe, + text="Globals", command=self.show_globals, variable=self.vglobals) + self.bglobals.grid(row=1, column=1) + # + self.status = Label(top, anchor="w") + self.status.pack(anchor="w") + self.error = Label(top, anchor="w") + self.error.pack(anchor="w", fill="x") + self.errorbg = self.error.cget("background") + # + self.fstack = Frame(top, height=1) + self.fstack.pack(expand=1, fill="both") + self.flocals = Frame(top) + self.flocals.pack(expand=1, fill="both") + self.fglobals = Frame(top, height=1) + self.fglobals.pack(expand=1, fill="both") + # + if self.vstack.get(): + self.show_stack() + if self.vlocals.get(): + self.show_locals() + if self.vglobals.get(): + self.show_globals() + + def interaction(self, message, frame, info=None): + self.frame = frame + self.status.configure(text=message) + # + if info: + type, value, tb = info + try: + m1 = type.__name__ + except AttributeError: + m1 = "%s" % str(type) + if value is not None: + try: + m1 = "%s: %s" % (m1, str(value)) + except: + pass + bg = "yellow" + else: + m1 = "" + tb = None + bg = self.errorbg + self.error.configure(text=m1, background=bg) + # + sv = self.stackviewer + if sv: + stack, i = self.idb.get_stack(self.frame, tb) + sv.load_stack(stack, i) + # + self.show_variables(1) + # + if self.vsource.get(): + self.sync_source_line() + # + for b in self.buttons: + b.configure(state="normal") + # + self.top.wakeup() + # Nested main loop: Tkinter's main loop is not reentrant, so use + # Tcl's vwait facility, which reenters the event loop until an + # event handler sets the variable we're waiting on + self.nesting_level += 1 + self.root.tk.call('vwait', '::idledebugwait') + self.nesting_level -= 1 + # + for b in self.buttons: + b.configure(state="disabled") + self.status.configure(text="") + self.error.configure(text="", background=self.errorbg) + self.frame = None + + def sync_source_line(self): + frame = self.frame + if not frame: + return + filename, lineno = self.__frame2fileline(frame) + if filename[:1] + filename[-1:] != "<>" and os.path.exists(filename): + self.flist.gotofileline(filename, lineno) + + def __frame2fileline(self, frame): + code = frame.f_code + filename = code.co_filename + lineno = frame.f_lineno + return filename, lineno + + def cont(self): + self.idb.set_continue() + self.abort_loop() + + def step(self): + self.idb.set_step() + self.abort_loop() + + def next(self): + self.idb.set_next(self.frame) + self.abort_loop() + + def ret(self): + self.idb.set_return(self.frame) + self.abort_loop() + + def quit(self): + self.idb.set_quit() + self.abort_loop() + + def abort_loop(self): + self.root.tk.call('set', '::idledebugwait', '1') + + stackviewer = None + + def show_stack(self): + if not self.stackviewer and self.vstack.get(): + self.stackviewer = sv = StackViewer(self.fstack, self.flist, self) + if self.frame: + stack, i = self.idb.get_stack(self.frame, None) + sv.load_stack(stack, i) + else: + sv = self.stackviewer + if sv and not self.vstack.get(): + self.stackviewer = None + sv.close() + self.fstack['height'] = 1 + + def show_source(self): + if self.vsource.get(): + self.sync_source_line() + + def show_frame(self, stackitem): + self.frame = stackitem[0] # lineno is stackitem[1] + self.show_variables() + + localsviewer = None + globalsviewer = None + + def show_locals(self): + lv = self.localsviewer + if self.vlocals.get(): + if not lv: + self.localsviewer = NamespaceViewer(self.flocals, "Locals") + else: + if lv: + self.localsviewer = None + lv.close() + self.flocals['height'] = 1 + self.show_variables() + + def show_globals(self): + gv = self.globalsviewer + if self.vglobals.get(): + if not gv: + self.globalsviewer = NamespaceViewer(self.fglobals, "Globals") + else: + if gv: + self.globalsviewer = None + gv.close() + self.fglobals['height'] = 1 + self.show_variables() + + def show_variables(self, force=0): + lv = self.localsviewer + gv = self.globalsviewer + frame = self.frame + if not frame: + ldict = gdict = None + else: + ldict = frame.f_locals + gdict = frame.f_globals + if lv and gv and ldict is gdict: + ldict = None + if lv: + lv.load_dict(ldict, force, self.pyshell.interp.rpcclt) + if gv: + gv.load_dict(gdict, force, self.pyshell.interp.rpcclt) + + def set_breakpoint_here(self, filename, lineno): + self.idb.set_break(filename, lineno) + + def clear_breakpoint_here(self, filename, lineno): + self.idb.clear_break(filename, lineno) + + def clear_file_breaks(self, filename): + self.idb.clear_all_file_breaks(filename) + + def load_breakpoints(self): + "Load PyShellEditorWindow breakpoints into subprocess debugger" + for editwin in self.pyshell.flist.inversedict: + filename = editwin.io.filename + try: + for lineno in editwin.breakpoints: + self.set_breakpoint_here(filename, lineno) + except AttributeError: + continue + +class StackViewer(ScrolledList): + + def __init__(self, master, flist, gui): + 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. + ScrolledList.__init__(self, master) + else: + ScrolledList.__init__(self, master, width=80) + self.flist = flist + self.gui = gui + self.stack = [] + + def load_stack(self, stack, index=None): + self.stack = stack + self.clear() + for i in range(len(stack)): + frame, lineno = stack[i] + try: + modname = frame.f_globals["__name__"] + except: + modname = "?" + code = frame.f_code + filename = code.co_filename + funcname = code.co_name + import linecache + sourceline = linecache.getline(filename, lineno) + sourceline = sourceline.strip() + if funcname in ("?", "", None): + item = "%s, line %d: %s" % (modname, lineno, sourceline) + else: + item = "%s.%s(), line %d: %s" % (modname, funcname, + lineno, sourceline) + if i == index: + item = "> " + item + self.append(item) + if index is not None: + self.select(index) + + def popup_event(self, event): + "override base method" + if self.stack: + return ScrolledList.popup_event(self, event) + + def fill_menu(self): + "override base method" + menu = self.menu + menu.add_command(label="Go to source line", + command=self.goto_source_line) + menu.add_command(label="Show stack frame", + command=self.show_stack_frame) + + def on_select(self, index): + "override base method" + if 0 <= index < len(self.stack): + self.gui.show_frame(self.stack[index]) + + def on_double(self, index): + "override base method" + self.show_source(index) + + def goto_source_line(self): + index = self.listbox.index("active") + self.show_source(index) + + def show_stack_frame(self): + index = self.listbox.index("active") + if 0 <= index < len(self.stack): + self.gui.show_frame(self.stack[index]) + + def show_source(self, index): + if not (0 <= index < len(self.stack)): + return + frame, lineno = self.stack[index] + code = frame.f_code + filename = code.co_filename + if os.path.isfile(filename): + edit = self.flist.open(filename) + if edit: + edit.gotoline(lineno) + + +class NamespaceViewer: + + def __init__(self, master, title, dict=None): + width = 0 + height = 40 + if dict: + height = 20*len(dict) # XXX 20 == observed height of Entry widget + self.master = master + self.title = title + import reprlib + self.repr = reprlib.Repr() + self.repr.maxstring = 60 + self.repr.maxother = 60 + self.frame = frame = Frame(master) + self.frame.pack(expand=1, fill="both") + self.label = Label(frame, text=title, borderwidth=2, relief="groove") + self.label.pack(fill="x") + self.vbar = vbar = Scrollbar(frame, name="vbar") + vbar.pack(side="right", fill="y") + self.canvas = canvas = Canvas(frame, + height=min(300, max(40, height)), + scrollregion=(0, 0, width, height)) + canvas.pack(side="left", fill="both", expand=1) + vbar["command"] = canvas.yview + canvas["yscrollcommand"] = vbar.set + self.subframe = subframe = Frame(canvas) + self.sfid = canvas.create_window(0, 0, window=subframe, anchor="nw") + self.load_dict(dict) + + dict = -1 + + def load_dict(self, dict, force=0, rpc_client=None): + if dict is self.dict and not force: + return + subframe = self.subframe + frame = self.frame + for c in list(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 RemoteDebugger.DictProxy. + ### + keys_list = dict.keys() + names = sorted(keys_list) + ### + row = 0 + for name in names: + value = dict[name] + svalue = self.repr.repr(value) # repr(value) + # Strip extra quotes caused by calling repr on the (already) + # repr'd value sent across the RPC interface: + if rpc_client: + svalue = svalue[1:-1] + l = Label(subframe, text=name) + l.grid(row=row, column=0, sticky="nw") + l = Entry(subframe, width=0, borderwidth=0) + l.insert(0, svalue) + l.grid(row=row, column=1, sticky="nw") + row = row+1 + self.dict = dict + # XXX Could we use a callback for the following? + subframe.update_idletasks() # Alas! + width = subframe.winfo_reqwidth() + height = subframe.winfo_reqheight() + canvas = self.canvas + self.canvas["scrollregion"] = (0, 0, width, height) + if height > 300: + canvas["height"] = 300 + frame.pack(expand=1) + else: + canvas["height"] = height + frame.pack(expand=0) + + def close(self): + self.frame.destroy() diff --git a/Lib/idlelib/debugger_r.py b/Lib/idlelib/debugger_r.py new file mode 100644 index 0000000..be2262f --- /dev/null +++ b/Lib/idlelib/debugger_r.py @@ -0,0 +1,388 @@ +"""Support for remote Python debugging. + +Some ASCII art to describe the structure: + + IN PYTHON SUBPROCESS # IN IDLE PROCESS + # + # oid='gui_adapter' + +----------+ # +------------+ +-----+ + | GUIProxy |--remote#call-->| GUIAdapter |--calls-->| GUI | ++-----+--calls-->+----------+ # +------------+ +-----+ +| Idb | # / ++-----+<-calls--+------------+ # +----------+<--calls-/ + | IdbAdapter |<--remote#call--| IdbProxy | + +------------+ # +----------+ + oid='idb_adapter' # + +The purpose of the Proxy and Adapter classes is to translate certain +arguments and return values that cannot be transported through the RPC +barrier, in particular frame and traceback objects. + +""" + +import types +from idlelib import Debugger + +debugging = 0 + +idb_adap_oid = "idb_adapter" +gui_adap_oid = "gui_adapter" + +#======================================= +# +# In the PYTHON subprocess: + +frametable = {} +dicttable = {} +codetable = {} +tracebacktable = {} + +def wrap_frame(frame): + fid = id(frame) + frametable[fid] = frame + return fid + +def wrap_info(info): + "replace info[2], a traceback instance, by its ID" + if info is None: + return None + else: + traceback = info[2] + assert isinstance(traceback, types.TracebackType) + traceback_id = id(traceback) + tracebacktable[traceback_id] = traceback + modified_info = (info[0], info[1], traceback_id) + return modified_info + +class GUIProxy: + + def __init__(self, conn, gui_adap_oid): + self.conn = conn + self.oid = gui_adap_oid + + def interaction(self, message, frame, info=None): + # calls rpc.SocketIO.remotecall() via run.MyHandler instance + # pass frame and traceback object IDs instead of the objects themselves + self.conn.remotecall(self.oid, "interaction", + (message, wrap_frame(frame), wrap_info(info)), + {}) + +class IdbAdapter: + + def __init__(self, idb): + self.idb = idb + + #----------called by an IdbProxy---------- + + def set_step(self): + self.idb.set_step() + + def set_quit(self): + self.idb.set_quit() + + def set_continue(self): + self.idb.set_continue() + + def set_next(self, fid): + frame = frametable[fid] + self.idb.set_next(frame) + + def set_return(self, fid): + frame = frametable[fid] + self.idb.set_return(frame) + + def get_stack(self, fid, tbid): + frame = frametable[fid] + if tbid is None: + tb = None + else: + tb = tracebacktable[tbid] + stack, i = self.idb.get_stack(frame, tb) + stack = [(wrap_frame(frame2), k) for frame2, k in stack] + return stack, i + + def run(self, cmd): + import __main__ + self.idb.run(cmd, __main__.__dict__) + + def set_break(self, filename, lineno): + msg = self.idb.set_break(filename, lineno) + return msg + + def clear_break(self, filename, lineno): + msg = self.idb.clear_break(filename, lineno) + return msg + + def clear_all_file_breaks(self, filename): + msg = self.idb.clear_all_file_breaks(filename) + return msg + + #----------called by a FrameProxy---------- + + def frame_attr(self, fid, name): + frame = frametable[fid] + return getattr(frame, name) + + def frame_globals(self, fid): + frame = frametable[fid] + dict = frame.f_globals + did = id(dict) + dicttable[did] = dict + return did + + def frame_locals(self, fid): + frame = frametable[fid] + dict = frame.f_locals + did = id(dict) + dicttable[did] = dict + return did + + def frame_code(self, fid): + frame = frametable[fid] + code = frame.f_code + cid = id(code) + codetable[cid] = code + return cid + + #----------called by a CodeProxy---------- + + def code_name(self, cid): + code = codetable[cid] + return code.co_name + + def code_filename(self, cid): + code = codetable[cid] + return code.co_filename + + #----------called by a DictProxy---------- + + def dict_keys(self, did): + raise NotImplemented("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()) + + def dict_item(self, did, key): + dict = dicttable[did] + value = dict[key] + value = repr(value) ### can't pickle module 'builtins' + return value + +#----------end class IdbAdapter---------- + + +def start_debugger(rpchandler, gui_adap_oid): + """Start the debugger and its RPC link in the Python subprocess + + Start the subprocess side of the split debugger and set up that side of the + RPC link by instantiating the GUIProxy, Idb debugger, and IdbAdapter + objects and linking them together. Register the IdbAdapter with the + RPCServer to handle RPC requests from the split debugger GUI via the + IdbProxy. + + """ + gui_proxy = GUIProxy(rpchandler, gui_adap_oid) + idb = Debugger.Idb(gui_proxy) + idb_adap = IdbAdapter(idb) + rpchandler.register(idb_adap_oid, idb_adap) + return idb_adap_oid + + +#======================================= +# +# In the IDLE process: + + +class FrameProxy: + + def __init__(self, conn, fid): + self._conn = conn + self._fid = fid + self._oid = "idb_adapter" + self._dictcache = {} + + def __getattr__(self, name): + if name[:1] == "_": + raise AttributeError(name) + if name == "f_code": + return self._get_f_code() + if name == "f_globals": + return self._get_f_globals() + if name == "f_locals": + return self._get_f_locals() + return self._conn.remotecall(self._oid, "frame_attr", + (self._fid, name), {}) + + def _get_f_code(self): + cid = self._conn.remotecall(self._oid, "frame_code", (self._fid,), {}) + return CodeProxy(self._conn, self._oid, cid) + + def _get_f_globals(self): + did = self._conn.remotecall(self._oid, "frame_globals", + (self._fid,), {}) + return self._get_dict_proxy(did) + + def _get_f_locals(self): + did = self._conn.remotecall(self._oid, "frame_locals", + (self._fid,), {}) + return self._get_dict_proxy(did) + + def _get_dict_proxy(self, did): + if did in self._dictcache: + return self._dictcache[did] + dp = DictProxy(self._conn, self._oid, did) + self._dictcache[did] = dp + return dp + + +class CodeProxy: + + def __init__(self, conn, oid, cid): + self._conn = conn + self._oid = oid + self._cid = cid + + def __getattr__(self, name): + if name == "co_name": + return self._conn.remotecall(self._oid, "code_name", + (self._cid,), {}) + if name == "co_filename": + return self._conn.remotecall(self._oid, "code_filename", + (self._cid,), {}) + + +class DictProxy: + + def __init__(self, conn, oid, did): + self._conn = conn + 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,), {}) + + 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) + + +class GUIAdapter: + + def __init__(self, conn, gui): + self.conn = conn + self.gui = gui + + def interaction(self, 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) + + +class IdbProxy: + + def __init__(self, conn, shell, oid): + self.oid = oid + self.conn = conn + self.shell = shell + + 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)) + return value + + def run(self, cmd, locals): + # Ignores locals on purpose! + seq = self.conn.asyncqueue(self.oid, "run", (cmd,), {}) + self.shell.interp.active_seq = seq + + def get_stack(self, frame, tbid): + # passing frame and traceback IDs, not the objects themselves + stack, i = self.call("get_stack", frame._fid, tbid) + stack = [(FrameProxy(self.conn, fid), k) for fid, k in stack] + return stack, i + + def set_continue(self): + self.call("set_continue") + + def set_step(self): + self.call("set_step") + + def set_next(self, frame): + self.call("set_next", frame._fid) + + def set_return(self, frame): + self.call("set_return", frame._fid) + + def set_quit(self): + self.call("set_quit") + + def set_break(self, filename, lineno): + msg = self.call("set_break", filename, lineno) + return msg + + def clear_break(self, filename, lineno): + msg = self.call("clear_break", filename, lineno) + return msg + + def clear_all_file_breaks(self, filename): + msg = self.call("clear_all_file_breaks", filename) + return msg + +def start_remote_debugger(rpcclt, pyshell): + """Start the subprocess debugger, initialize the debugger GUI and RPC link + + Request the RPCServer start the Python subprocess debugger and link. Set + up the Idle side of the split debugger by instantiating the IdbProxy, + debugger GUI, and debugger GUIAdapter objects and linking them together. + + Register the GUIAdapter with the RPCClient to handle debugger GUI + interaction requests coming from the subprocess debugger via the GUIProxy. + + The IdbAdapter will pass execution and environment requests coming from the + Idle debugger GUI to the subprocess debugger via the IdbProxy. + + """ + global idb_adap_oid + + 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_adap = GUIAdapter(rpcclt, gui) + rpcclt.register(gui_adap_oid, gui_adap) + return gui + +def close_remote_debugger(rpcclt): + """Shut down subprocess debugger and Idle side of debugger RPC link + + 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().) + + """ + close_subprocess_debugger(rpcclt) + rpcclt.unregister(gui_adap_oid) + +def close_subprocess_debugger(rpcclt): + rpcclt.remotecall("exec", "stop_the_debugger", (idb_adap_oid,), {}) + +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' diff --git a/Lib/idlelib/debugobj.py b/Lib/idlelib/debugobj.py new file mode 100644 index 0000000..7b57aa4 --- /dev/null +++ b/Lib/idlelib/debugobj.py @@ -0,0 +1,143 @@ +# XXX TO DO: +# - popup menu +# - support partial or total redisplay +# - more doc strings +# - tooltips + +# object browser + +# XXX TO DO: +# - for classes/modules, add "open source" to object browser + +import re + +from idlelib.TreeWidget import TreeItem, TreeNode, ScrolledCanvas + +from reprlib import Repr + +myrepr = Repr() +myrepr.maxstring = 100 +myrepr.maxother = 100 + +class ObjectTreeItem(TreeItem): + def __init__(self, labeltext, object, setfunction=None): + self.labeltext = labeltext + self.object = object + self.setfunction = setfunction + def GetLabelText(self): + return self.labeltext + def GetText(self): + return myrepr.repr(self.object) + def GetIconName(self): + if not self.IsExpandable(): + return "python" + def IsEditable(self): + return self.setfunction is not None + def SetText(self, text): + try: + value = eval(text) + self.setfunction(value) + except: + pass + else: + self.object = value + def IsExpandable(self): + return not not dir(self.object) + def GetSubList(self): + keys = dir(self.object) + sublist = [] + for key in keys: + try: + value = getattr(self.object, key) + except AttributeError: + continue + item = make_objecttreeitem( + str(key) + " =", + value, + lambda value, key=key, object=self.object: + setattr(object, key, value)) + sublist.append(item) + return sublist + +class ClassTreeItem(ObjectTreeItem): + def IsExpandable(self): + return True + def GetSubList(self): + sublist = ObjectTreeItem.GetSubList(self) + if len(self.object.__bases__) == 1: + item = make_objecttreeitem("__bases__[0] =", + self.object.__bases__[0]) + else: + item = make_objecttreeitem("__bases__ =", self.object.__bases__) + sublist.insert(0, item) + return sublist + +class AtomicObjectTreeItem(ObjectTreeItem): + def IsExpandable(self): + return 0 + +class SequenceTreeItem(ObjectTreeItem): + def IsExpandable(self): + return len(self.object) > 0 + def keys(self): + return range(len(self.object)) + def GetSubList(self): + sublist = [] + for key in self.keys(): + try: + value = self.object[key] + except KeyError: + continue + def setfunction(value, key=key, object=self.object): + object[key] = value + item = make_objecttreeitem("%r:" % (key,), value, setfunction) + sublist.append(item) + return sublist + +class DictTreeItem(SequenceTreeItem): + def keys(self): + keys = list(self.object.keys()) + try: + keys.sort() + except: + pass + return keys + +dispatch = { + int: AtomicObjectTreeItem, + float: AtomicObjectTreeItem, + str: AtomicObjectTreeItem, + tuple: SequenceTreeItem, + list: SequenceTreeItem, + dict: DictTreeItem, + type: ClassTreeItem, +} + +def make_objecttreeitem(labeltext, object, setfunction=None): + t = type(object) + if t in dispatch: + c = dispatch[t] + else: + c = ObjectTreeItem + return c(labeltext, object, setfunction) + + +def _object_browser(parent): + import sys + from tkinter import Tk + root = Tk() + root.title("Test ObjectBrowser") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) + root.configure(bd=0, bg="yellow") + root.focus_set() + sc = ScrolledCanvas(root, bg="white", highlightthickness=0, takefocus=1) + 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 idlelib.idle_test.htest import run + run(_object_browser) diff --git a/Lib/idlelib/debugobj_r.py b/Lib/idlelib/debugobj_r.py new file mode 100644 index 0000000..8031aae --- /dev/null +++ b/Lib/idlelib/debugobj_r.py @@ -0,0 +1,36 @@ +from idlelib import rpc + +def remote_object_tree_item(item): + wrapper = WrappedObjectTreeItem(item) + oid = id(wrapper) + rpc.objecttable[oid] = wrapper + return oid + +class WrappedObjectTreeItem: + # Lives in PYTHON subprocess + + def __init__(self, item): + self.__item = item + + def __getattr__(self, name): + value = getattr(self.__item, name) + return value + + def _GetSubList(self): + sub_list = self.__item._GetSubList() + return list(map(remote_object_tree_item, sub_list)) + +class StubObjectTreeItem: + # Lives in IDLE process + + def __init__(self, sockio, oid): + self.sockio = sockio + self.oid = oid + + def __getattr__(self, name): + value = rpc.MethodProxy(self.sockio, self.oid, name) + return value + + def _GetSubList(self): + sub_list = self.sockio.remotecall(self.oid, "_GetSubList", (), {}) + return [StubObjectTreeItem(self.sockio, oid) for oid in sub_list] diff --git a/Lib/idlelib/delegator.py b/Lib/idlelib/delegator.py new file mode 100644 index 0000000..dc2a1aa --- /dev/null +++ b/Lib/idlelib/delegator.py @@ -0,0 +1,33 @@ +class Delegator: + + 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 + setattr(self, name, attr) + self.__cache.add(name) + return attr + + def resetcache(self): + "Removes added attributes while leaving original attributes." + # Function is really about resetting delagator dict + # to original state. Cache is just a means + for key in self.__cache: + try: + delattr(self, key) + except AttributeError: + pass + 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/dynOptionMenuWidget.py b/Lib/idlelib/dynOptionMenuWidget.py deleted file mode 100644 index 515b4ba..0000000 --- a/Lib/idlelib/dynOptionMenuWidget.py +++ /dev/null @@ -1,57 +0,0 @@ -""" -OptionMenu widget modified to allow dynamic menu reconfiguration -and setting of highlightthickness -""" -import copy -from tkinter import OptionMenu, _setit, StringVar, Button - -class DynOptionMenu(OptionMenu): - """ - unlike OptionMenu, our kwargs can include highlightthickness - """ - def __init__(self, master, variable, value, *values, **kwargs): - # TODO copy value instead of whole dict - kwargsCopy=copy.copy(kwargs) - if 'highlightthickness' in list(kwargs.keys()): - del(kwargs['highlightthickness']) - OptionMenu.__init__(self, master, variable, value, *values, **kwargs) - self.config(highlightthickness=kwargsCopy.get('highlightthickness')) - #self.menu=self['menu'] - self.variable=variable - self.command=kwargs.get('command') - - def SetMenu(self,valueList,value=None): - """ - clear and reload the menu with a new set of options. - valueList - list of new options - value - initial value to set the optionmenu's menubutton to - """ - self['menu'].delete(0,'end') - for item in valueList: - self['menu'].add_command(label=item, - command=_setit(self.variable,item,self.command)) - if value: - self.variable.set(value) - -def _dyn_option_menu(parent): # htest # - from tkinter import Toplevel - - top = Toplevel() - top.title("Tets dynamic option menu") - top.geometry("200x100+%d+%d" % (parent.winfo_rootx() + 200, - parent.winfo_rooty() + 150)) - top.focus_set() - - var = StringVar(top) - var.set("Old option set") #Set the default value - dyn = DynOptionMenu(top,var, "old1","old2","old3","old4") - dyn.pack() - - def update(): - dyn.SetMenu(["new1","new2","new3","new4"], value="new option set") - button = Button(top, text="Change option set", command=update) - button.pack() - -if __name__ == '__main__': - from idlelib.idle_test.htest import run - run(_dyn_option_menu) diff --git a/Lib/idlelib/dynoption.py b/Lib/idlelib/dynoption.py new file mode 100644 index 0000000..515b4ba --- /dev/null +++ b/Lib/idlelib/dynoption.py @@ -0,0 +1,57 @@ +""" +OptionMenu widget modified to allow dynamic menu reconfiguration +and setting of highlightthickness +""" +import copy +from tkinter import OptionMenu, _setit, StringVar, Button + +class DynOptionMenu(OptionMenu): + """ + unlike OptionMenu, our kwargs can include highlightthickness + """ + def __init__(self, master, variable, value, *values, **kwargs): + # TODO copy value instead of whole dict + kwargsCopy=copy.copy(kwargs) + if 'highlightthickness' in list(kwargs.keys()): + del(kwargs['highlightthickness']) + OptionMenu.__init__(self, master, variable, value, *values, **kwargs) + self.config(highlightthickness=kwargsCopy.get('highlightthickness')) + #self.menu=self['menu'] + self.variable=variable + self.command=kwargs.get('command') + + def SetMenu(self,valueList,value=None): + """ + clear and reload the menu with a new set of options. + valueList - list of new options + value - initial value to set the optionmenu's menubutton to + """ + self['menu'].delete(0,'end') + for item in valueList: + self['menu'].add_command(label=item, + command=_setit(self.variable,item,self.command)) + if value: + self.variable.set(value) + +def _dyn_option_menu(parent): # htest # + from tkinter import Toplevel + + top = Toplevel() + top.title("Tets dynamic option menu") + top.geometry("200x100+%d+%d" % (parent.winfo_rootx() + 200, + parent.winfo_rooty() + 150)) + top.focus_set() + + var = StringVar(top) + var.set("Old option set") #Set the default value + dyn = DynOptionMenu(top,var, "old1","old2","old3","old4") + dyn.pack() + + def update(): + dyn.SetMenu(["new1","new2","new3","new4"], value="new option set") + button = Button(top, text="Change option set", command=update) + button.pack() + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(_dyn_option_menu) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py new file mode 100644 index 0000000..b5868be --- /dev/null +++ b/Lib/idlelib/editor.py @@ -0,0 +1,1703 @@ +import importlib +import importlib.abc +import importlib.util +import os +import platform +import re +import string +import sys +from tkinter import * +import tkinter.simpledialog as tkSimpleDialog +import tkinter.messagebox as tkMessageBox +import traceback +import webbrowser + +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 + +# The default tab setting for a Text widget, in average-width characters. +TK_TABWIDTH_DEFAULT = 8 + +_py_version = ' (%s)' % platform.python_version() + +def _sphinx_version(): + "Format sys.version_info to produce the Sphinx version string used to install the chm docs" + major, minor, micro, level, serial = sys.version_info + release = '%s%s' % (major, minor) + release += '%s' % (micro,) + if level == 'candidate': + release += 'rc%s' % (serial,) + elif level != 'final': + release += '%s%s' % (level[0], serial) + return release + + +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('', 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.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 + + help_url = None + + def __init__(self, flist=None, filename=None, key=None, root=None): + if EditorWindow.help_url is None: + dochome = os.path.join(sys.base_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] + if os.path.isdir('/var/www/html/python/'): # "python2" rpm + dochome = '/var/www/html/python/index.html' + else: + basepath = '/usr/share/doc/' # standard location + dochome = os.path.join(basepath, pyver, + 'Doc', 'index.html') + elif sys.platform[:3] == 'win': + chmfile = os.path.join(sys.base_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, + 'Resources/English.lproj/Documentation/index.html') + dochome = os.path.normpath(dochome) + if os.path.isfile(dochome): + EditorWindow.help_url = dochome + if sys.platform == 'darwin': + # 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] + 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 = 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 + 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 = os.path.join(idleConf.GetUserCfgDir(), + 'recent-files.lst') + self.text_frame = text_frame = Frame(top) + self.vbar = vbar = Scrollbar(text_frame, name='vbar') + self.width = idleConf.GetOption('main', 'EditorWindow', + 'width', type='int') + text_options = { + 'name': 'text', + 'padx': 5, + 'wrap': 'none', + 'highlightthickness': 0, + '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 + + self.createmenubar() + self.apply_bindings() + + self.top.protocol("WM_DELETE_WINDOW", self.close) + self.top.bind("<>", self.close_event) + if macosxSupport.isAquaTk(): + # Command-W on editorwindows doesn't work without this. + text.bind('<>', self.close_event) + # Some OS X systems have only one mouse button, so use + # control-click for popup context menus there. For two + # buttons, AquaTk defines <2> as the right button, not <3>. + text.bind("",self.right_menu_event) + text.bind("<2>", self.right_menu_event) + else: + # Elsewhere, use right-click for popup menus. + text.bind("<3>",self.right_menu_event) + text.bind("<>", self.cut) + text.bind("<>", self.copy) + text.bind("<>", self.paste) + text.bind("<>", self.center_insert_event) + text.bind("<>", self.help_dialog) + text.bind("<>", self.python_docs) + text.bind("<>", self.about_dialog) + text.bind("<>", self.config_dialog) + text.bind("<>", self.open_module) + text.bind("<>", lambda event: "break") + text.bind("<>", self.select_all) + text.bind("<>", self.remove_selection) + text.bind("<>", self.find_event) + text.bind("<>", self.find_again_event) + text.bind("<>", self.find_in_files_event) + text.bind("<>", self.find_selection_event) + text.bind("<>", self.replace_event) + text.bind("<>", self.goto_line_event) + text.bind("<>",self.smart_backspace_event) + text.bind("<>",self.newline_and_indent_event) + text.bind("<>",self.smart_indent_event) + text.bind("<>",self.indent_region_event) + text.bind("<>",self.dedent_region_event) + text.bind("<>",self.comment_region_event) + text.bind("<>",self.uncomment_region_event) + text.bind("<>",self.tabify_region_event) + text.bind("<>",self.untabify_region_event) + text.bind("<>",self.toggle_tabs_event) + text.bind("<>",self.change_indentwidth_event) + text.bind("", self.move_at_edge_if_selection(0)) + text.bind("", self.move_at_edge_if_selection(1)) + text.bind("<>", self.del_word_left) + text.bind("<>", self.del_word_right) + text.bind("<>", self.home_callback) + + if flist: + flist.inversedict[self] = key + if key: + flist.dict[key] = self + text.bind("<>", self.new_callback) + text.bind("<>", self.flist.close_all_callback) + text.bind("<>", self.open_class_browser) + text.bind("<>", self.open_path_browser) + text.bind("<>", self.open_turtle_demo) + + self.set_status_bar() + vbar['command'] = text.yview + vbar.pack(side=RIGHT, fill=Y) + text['yscrollcommand'] = vbar.set + text['font'] = idleConf.GetFont(self.root, 'main', 'EditorWindow') + text_frame.pack(side=LEFT, fill=BOTH, expand=1) + text.pack(side=TOP, fill=BOTH, expand=1) + text.focus_set() + + # usetabs true -> literal tab characters are used by indent and + # dedent cmds, possibly mixed with spaces if + # indentwidth is not a multiple of tabwidth, + # which will cause Tabnanny to nag! + # false -> tab characters are converted to spaces by indent + # and dedent cmds, and ditto TAB keystrokes + # 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') + self.usetabs = not usespaces + + # tabwidth is the display width of a literal tab character. + # CAUTION: telling Tk to use anything other than its default + # tab setting causes it to use an entirely different tabbing algorithm, + # treating tab stops as fixed distances from the left margin. + # Nobody expects this, so for now tabwidth should never be changed. + self.tabwidth = 8 # must remain 8 until Tk is fixed. + + # indentwidth is the number of screen characters per indent level. + # The recommended Python indentation is four spaces. + self.indentwidth = self.tabwidth + self.set_notabs_indentwidth() + + # 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 + # num_context_lines[1] lines back if that didn't work, and so on. + # The last value should be huge (larger than the # of lines in a + # 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) + self.color = None # initialized below in self.ResetColorizer + 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) + else: + io.set_filename(filename) + self.good_load = True + + self.ResetColorizer() + self.saved_change_hook() + self.update_recent_files_list() + self.load_extensions() + menu = self.menudict.get('windows') + if menu: + end = menu.index("end") + if end is None: + end = -1 + if end >= 0: + menu.add_separator() + end = end + 1 + self.wmenu_end = end + 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 + + def _filename_to_unicode(self, filename): + """Return filename as BMP unicode so diplayable in Tk.""" + # Decode bytes to unicode. + if isinstance(filename, bytes): + try: + filename = filename.decode(self.filesystemencoding) + except UnicodeDecodeError: + try: + filename = filename.decode(self.encoding) + except UnicodeDecodeError: + # byte-to-byte conversion + filename = filename.decode('iso8859-1') + # Replace non-BMP char with diamond questionmark. + return re.sub('[\U00010000-\U0010FFFF]', '\ufffd', filename) + + def new_callback(self, event): + dirname, basename = self.io.defaultfilename() + self.flist.new(dirname) + return "break" + + def home_callback(self, event): + if (event.state & 4) != 0 and event.keysym == "Home": + # state&4==Control. If , use the Tk binding. + return + if self.text.index("iomark") and \ + self.text.compare("iomark", "<=", "insert lineend") and \ + self.text.compare("insert linestart", "<=", "iomark"): + # In Shell on input line, go to just after prompt + insertpt = int(self.text.index("iomark").split(".")[1]) + else: + line = self.text.get("insert linestart", "insert lineend") + for insertpt in range(len(line)): + if line[insertpt] not in (' ','\t'): + break + else: + insertpt=len(line) + lineat = int(self.text.index("insert").split('.')[1]) + if insertpt == lineat: + insertpt = 0 + dest = "insert linestart+"+str(insertpt)+"c" + if (event.state&1) == 0: + # shift was not pressed + 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") + else: + 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 + first = self.text.index(dest) + last = self.text.index("my_anchor") + if self.text.compare(first,">",last): + first,last = last,first + self.text.tag_remove("sel", "1.0", "end") + self.text.tag_add("sel", first, last) + self.text.mark_set("insert", dest) + self.text.see("insert") + return "break" + + def set_status_bar(self): + self.status_bar = self.MultiStatusBar(self.top) + sep = Frame(self.top, height=1, borderwidth=1, background='grey75') + if sys.platform == "darwin": + # Insert some padding to avoid obscuring some of the statusbar + # by the resize widget. + self.status_bar.set_label('_padding1', ' ', side=RIGHT) + self.status_bar.set_label('column', 'Col: ?', side=RIGHT) + self.status_bar.set_label('line', 'Ln: ?', side=RIGHT) + self.status_bar.pack(side=BOTTOM, fill=X) + sep.pack(side=BOTTOM, fill=X) + self.text.bind("<>", self.set_line_and_column) + self.text.event_add("<>", + "", "") + self.text.after_idle(self.set_line_and_column) + + def set_line_and_column(self, event=None): + line, column = self.text.index(INSERT).split('.') + self.status_bar.set_label('column', 'Col: %s' % column) + self.status_bar.set_label('line', 'Ln: %s' % line) + + menu_specs = [ + ("file", "_File"), + ("edit", "_Edit"), + ("format", "F_ormat"), + ("run", "_Run"), + ("options", "_Options"), + ("windows", "_Window"), + ("help", "_Help"), + ] + + + def createmenubar(self): + mbar = self.menubar + self.menudict = menudict = {} + for name, label in self.menu_specs: + underline, label = prepstr(label) + menudict[name] = menu = Menu(mbar, name=name, tearoff=0) + mbar.add_cascade(label=label, menu=menu, underline=underline) + if macosxSupport.isCarbonTk(): + # Insert the application menu + menudict['application'] = menu = Menu(mbar, name='apple', + tearoff=0) + mbar.add_cascade(label='IDLE', menu=menu) + self.fill_menus() + self.recent_files_menu = Menu(self.menubar, 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 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) + WindowList.add_windows_to_menu(menu) + + rmenu = None + + def right_menu_event(self, event): + self.text.mark_set("insert", "@%d,%d" % (event.x, event.y)) + if not self.rmenu: + self.make_rmenu() + rmenu = self.rmenu + self.event = event + iswin = sys.platform[:3] == 'win' + if iswin: + self.text.config(cursor="arrow") + + for item in self.rmenu_specs: + try: + label, eventname, verify_state = item + except ValueError: # see issue1207589 + continue + + if verify_state is None: + continue + 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") + + rmenu_specs = [ + # ("Label", "<>", "statefuncname"), ... + ("Close", "<>", None), # Example + ] + + def make_rmenu(self): + rmenu = Menu(self.text, tearoff=0) + for item in self.rmenu_specs: + label, eventname = item[0], item[1] + if label is not None: + def command(text=self.text, eventname=eventname): + text.event_generate(eventname) + rmenu.add_command(label=label, command=command) + else: + rmenu.add_separator() + self.rmenu = rmenu + + def rmenu_check_cut(self): + return self.rmenu_check_copy() + + def rmenu_check_copy(self): + try: + indx = self.text.index('sel.first') + except TclError: + return 'disabled' + else: + return 'normal' if indx else 'disabled' + + def rmenu_check_paste(self): + try: + self.text.tk.call('tk::GetSelection', self.text, 'CLIPBOARD') + except TclError: + return 'disabled' + else: + return 'normal' + + def about_dialog(self, event=None): + "Handle Help 'About IDLE' event." + # Synchronize with macosxSupport.overrideRootMenu.about_dialog. + aboutDialog.AboutDialog(self.top,'About IDLE') + + def config_dialog(self, event=None): + "Handle Options 'Configure IDLE' event." + # Synchronize with macosxSupport.overrideRootMenu.config_dialog. + configDialog.ConfigDialog(self.top,'Settings') + + def help_dialog(self, event=None): + "Handle Help 'IDLE Help' event." + # Synchronize with macosxSupport.overrideRootMenu.help_dialog. + if self.root: + parent = self.root + else: + parent = self.top + help.show_idlehelp(parent) + + def python_docs(self, event=None): + if sys.platform[:3] == 'win': + try: + os.startfile(self.help_url) + except OSError as why: + tkMessageBox.showerror(title='Document Start Failure', + message=str(why), parent=self.text) + else: + webbrowser.open(self.help_url) + return "break" + + def cut(self,event): + self.text.event_generate("<>") + return "break" + + def copy(self,event): + if not self.text.tag_ranges("sel"): + # There is no selection, so do nothing and maybe interrupt. + return + self.text.event_generate("<>") + return "break" + + def paste(self,event): + self.text.event_generate("<>") + self.text.see("insert") + return "break" + + def select_all(self, event=None): + self.text.tag_add("sel", "1.0", "end-1c") + self.text.mark_set("insert", "1.0") + self.text.see("insert") + return "break" + + def remove_selection(self, event=None): + self.text.tag_remove("sel", "1.0", "end") + self.text.see("insert") + + def move_at_edge_if_selection(self, edge_index): + """Cursor move begins at start or end of selection + + When a left/right cursor key is pressed create and return to Tkinter a + function which causes a cursor move from the associated edge of the + selection. + + """ + self_text_index = self.text.index + self_text_mark_set = self.text.mark_set + edges_table = ("sel.first+1c", "sel.last-1c") + def move_at_edge(event): + if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed + try: + self_text_index("sel.first") + self_text_mark_set("insert", edges_table[edge_index]) + except TclError: + pass + return move_at_edge + + def del_word_left(self, event): + self.text.event_generate('') + return "break" + + def del_word_right(self, event): + self.text.event_generate('') + return "break" + + def find_event(self, event): + SearchDialog.find(self.text) + return "break" + + def find_again_event(self, event): + SearchDialog.find_again(self.text) + return "break" + + def find_selection_event(self, event): + SearchDialog.find_selection(self.text) + return "break" + + def find_in_files_event(self, event): + GrepDialog.grep(self.text, self.io, self.flist) + return "break" + + def replace_event(self, event): + ReplaceDialog.replace(self.text) + return "break" + + def goto_line_event(self, event): + text = self.text + lineno = tkSimpleDialog.askinteger("Goto", + "Go to line number:",parent=text) + if lineno is None: + return "break" + if lineno <= 0: + text.bell() + return "break" + text.mark_set("insert", "%d.0" % lineno) + text.see("insert") + + def open_module(self, event=None): + # XXX Shouldn't this be in IOBinding? + try: + name = self.text.get("sel.first", "sel.last") + except TclError: + 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: + spec = importlib.util.find_spec(name) + except (ValueError, ImportError) as msg: + tkMessageBox.showerror("Import error", str(msg), parent=self.text) + return + if spec is None: + tkMessageBox.showerror("Import error", "module not found", + parent=self.text) + return + if not isinstance(spec.loader, importlib.abc.SourceLoader): + tkMessageBox.showerror("Import error", "not a source-based module", + parent=self.text) + return + try: + file_path = spec.loader.get_filename(name) + except AttributeError: + tkMessageBox.showerror("Import error", + "loader does not support get_filename", + parent=self.text) + return + if self.flist: + self.flist.open(file_path) + else: + self.io.loadfile(file_path) + return file_path + + def open_class_browser(self, event=None): + filename = self.io.filename + if not (self.__class__.__name__ == 'PyShellEditorWindow' + and filename): + filename = self.open_module() + if filename is None: + return + head, tail = os.path.split(filename) + base, ext = os.path.splitext(tail) + from idlelib import ClassBrowser + ClassBrowser.ClassBrowser(self.flist, base, [head]) + + def open_path_browser(self, event=None): + from idlelib import PathBrowser + PathBrowser.PathBrowser(self.flist) + + def open_turtle_demo(self, event = None): + import subprocess + + cmd = [sys.executable, + '-c', + 'from turtledemo.__main__ import main; main()'] + subprocess.Popen(cmd, shell=False) + + def gotoline(self, lineno): + if lineno is not None and lineno > 0: + self.text.mark_set("insert", "%d.0" % lineno) + self.text.tag_remove("sel", "1.0", "end") + self.text.tag_add("sel", "insert", "insert +1l") + self.center() + + def ispythonsource(self, filename): + if not filename or os.path.isdir(filename): + return True + 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 + + def close_hook(self): + if self.flist: + self.flist.unregister_maybe_terminate(self) + self.flist = None + + def set_close_hook(self, close_hook): + self.close_hook = close_hook + + def filename_change_hook(self): + if self.flist: + self.flist.filename_changed_edit(self) + self.saved_change_hook() + self.top.update_windowlist_registry(self) + self.ResetColorizer() + + def _addcolorizer(self): + if self.color: + return + if self.ispythonsource(self.io.filename): + self.color = self.ColorDelegator() + # can add more colorizers here... + if self.color: + self.per.removefilter(self.undo) + self.per.insertfilter(self.color) + self.per.insertfilter(self.undo) + + def _rmcolorizer(self): + if not self.color: + return + self.color.removecolors() + self.per.removefilter(self.color) + self.color = None + + def ResetColorizer(self): + "Update the color theme" + # Called from self.filename_change_hook and from configDialog.py + self._rmcolorizer() + self._addcolorizer() + 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']) + + 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 ResetFont(self): + "Update the text widgets' font if it is changed" + # 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.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(): + xkeydefs = idleConf.GetExtensionBindings(extensionName) + if xkeydefs: + for event, keylist in xkeydefs.items(): + self.text.event_delete(event, *keylist) + + def ApplyKeybindings(self): + "Update the keybindings after they are changed" + # 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) + if xkeydefs: + self.apply_bindings(xkeydefs) + #update menu accelerators + menuEventDict = {} + 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: + menu = self.menudict[menubarItem] + end = menu.index(END) + if end is None: + # Skip empty menus + continue + end += 1 + for index in range(0, end): + if menu.type(index) == 'command': + accel = menu.entrycget(index, 'accelerator') + if accel: + itemName = menu.entrycget(index, 'label') + event = '' + if menubarItem in menuEventDict: + if itemName in menuEventDict[menubarItem]: + event = menuEventDict[menubarItem][itemName] + if event: + accel = get_accelerator(keydefs, event) + menu.entryconfig(index, accelerator=accel) + + def set_notabs_indentwidth(self): + "Update the indentwidth if changed and not using tabs in this window" + # Called from configDialog.py + if not self.usetabs: + self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces', + type='int') + + def reset_help_menu_entries(self): + "Update the additional help entries on the Help menu" + help_list = idleConf.GetAllExtraHelpSourcesList() + helpmenu = self.menudict['help'] + # first delete the extra help entries, if any + helpmenu_length = helpmenu.index(END) + if helpmenu_length > self.base_helpmenu_length: + helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length) + # then rebuild them + if help_list: + helpmenu.add_separator() + for entry in help_list: + cmd = self.__extra_help_callback(entry[1]) + helpmenu.add_command(label=entry[0], command=cmd) + # and update the menu dictionary + self.menudict['help'] = helpmenu + + def __extra_help_callback(self, helpfile): + "Create a callback with the helpfile value frozen at definition time" + def display_extra_help(helpfile=helpfile): + if not helpfile.startswith(('www', 'http')): + helpfile = os.path.normpath(helpfile) + if sys.platform[:3] == 'win': + try: + os.startfile(helpfile) + except OSError as why: + tkMessageBox.showerror(title='Document Start Failure', + message=str(why), parent=self.text) + else: + webbrowser.open(helpfile) + return display_extra_help + + def update_recent_files_list(self, new_file=None): + "Load and update the recent files list and menus" + rf_list = [] + if os.path.exists(self.recent_files_path): + with open(self.recent_files_path, 'r', + encoding='utf_8', errors='replace') as rf_list_file: + rf_list = rf_list_file.readlines() + if new_file: + new_file = os.path.abspath(new_file) + '\n' + if new_file in rf_list: + rf_list.remove(new_file) # move to top + rf_list.insert(0, new_file) + # clean and save the recent files list + bad_paths = [] + for path in rf_list: + if '\0' in path or not os.path.exists(path[0:-1]): + bad_paths.append(path) + rf_list = [path for path in rf_list if path not in bad_paths] + ulchars = "1234567890ABCDEFGHIJK" + rf_list = rf_list[0:len(ulchars)] + try: + with open(self.recent_files_path, 'w', + encoding='utf_8', errors='replace') as rf_file: + rf_file.writelines(rf_list) + except OSError 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." + % self._filename_to_unicode(str(err)), + parent=self.text) + # for each edit window instance, construct the recent files menu + for instance in self.top.instance_dict: + 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] + " " + ufile_name, + command=callback, + underline=0) + + def __recent_file_callback(self, file_name): + def open_recent_file(fn_closure=file_name): + self.io.open(editFile=fn_closure) + return open_recent_file + + def saved_change_hook(self): + short = self.short_title() + long = self.long_title() + if short and long: + title = short + " - " + long + _py_version + elif short: + title = short + elif long: + title = long + else: + title = "Untitled" + icon = short or long or title + if not self.get_saved(): + title = "*%s*" % title + icon = "*%s" % icon + self.top.wm_title(title) + self.top.wm_iconname(icon) + + def get_saved(self): + return self.undo.get_saved() + + def set_saved(self, flag): + self.undo.set_saved(flag) + + def reset_undo(self): + self.undo.reset_undo() + + def short_title(self): + filename = self.io.filename + 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 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() + + def center(self, mark="insert"): + text = self.text + top, bot = self.getwindowlines() + lineno = self.getlineno(mark) + height = bot - top + newtop = max(1, lineno - height//2) + text.yview(float(newtop)) + + def getwindowlines(self): + text = self.text + top = self.getlineno("@0,0") + bot = self.getlineno("@0,65535") + if top == bot and text.winfo_height() == 1: + # Geometry manager hasn't run yet + height = int(text['height']) + bot = top + height - 1 + return top, bot + + def getlineno(self, mark="insert"): + text = self.text + return int(float(text.index(mark))) + + def get_geometry(self): + "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())) + + def close_event(self, event): + self.close() + + def maybesave(self): + if self.io: + if not self.get_saved(): + if self.top.state()!='normal': + self.top.deiconify() + self.top.lower() + self.top.lift() + return self.io.maybesave() + + def close(self): + 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) + WindowList.unregister_callback(self.postwindowsmenu) + self.unload_extensions() + self.io.close() + self.io = None + self.undo = None + if self.color: + self.color.close(False) + self.color = None + self.text = None + self.tkinter_vars = None + self.per.close() + self.per = None + self.top.destroy() + if self.close_hook: + # unless override: unregister from flist, terminate if last window + self.close_hook() + + def load_extensions(self): + self.extensions = {} + self.load_standard_extensions() + + def unload_extensions(self): + for ins in list(self.extensions.values()): + if hasattr(ins, "close"): + ins.close() + self.extensions = {} + + def load_standard_extensions(self): + for name in self.get_standard_extension_names(): + try: + self.load_extension(name) + except: + print("Failed to load extension", repr(name)) + traceback.print_exc() + + def get_standard_extension_names(self): + return idleConf.GetExtensions(editor_only=True) + + def load_extension(self, name): + try: + try: + mod = importlib.import_module('.' + name, package=__package__) + except (ImportError, TypeError): + mod = importlib.import_module(name) + except ImportError: + print("\nFailed to import extension: ", name) + raise + cls = getattr(mod, name) + keydefs = idleConf.GetExtensionBindings(name) + if hasattr(cls, "menudefs"): + self.fill_menus(cls.menudefs, keydefs) + ins = cls(self) + self.extensions[name] = ins + if keydefs: + self.apply_bindings(keydefs) + for vevent in keydefs: + methodname = vevent.replace("-", "_") + while methodname[:1] == '<': + methodname = methodname[1:] + while methodname[-1:] == '>': + methodname = methodname[:-1] + methodname = methodname + "_event" + if hasattr(ins, methodname): + self.text.bind(vevent, getattr(ins, methodname)) + + def apply_bindings(self, keydefs=None): + if keydefs is None: + keydefs = self.Bindings.default_keydefs + text = self.text + text.keydefs = keydefs + for event, keylist in keydefs.items(): + if keylist: + text.event_add(event, *keylist) + + def fill_menus(self, menudefs=None, keydefs=None): + """Add appropriate entries to the menus and submenus + + Menus that are absent or None in self.menudict are ignored. + """ + if menudefs is None: + menudefs = self.Bindings.menudefs + if keydefs is None: + keydefs = self.Bindings.default_keydefs + menudict = self.menudict + text = self.text + for mname, entrylist in menudefs: + menu = menudict.get(mname) + if not menu: + continue + for entry in entrylist: + if not entry: + menu.add_separator() + else: + label, eventname = entry + checkbutton = (label[:1] == '!') + if checkbutton: + label = label[1:] + underline, label = prepstr(label) + accelerator = get_accelerator(keydefs, eventname) + def command(text=text, eventname=eventname): + text.event_generate(eventname) + if checkbutton: + var = self.get_var_obj(eventname, BooleanVar) + menu.add_checkbutton(label=label, underline=underline, + command=command, accelerator=accelerator, + variable=var) + else: + menu.add_command(label=label, underline=underline, + command=command, + accelerator=accelerator) + + def getvar(self, name): + var = self.get_var_obj(name) + if var: + value = var.get() + return value + else: + 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) + + def get_var_obj(self, name, vartype=None): + var = self.tkinter_vars.get(name) + if not var and vartype: + # create a Tkinter variable object with self.text as master: + self.tkinter_vars[name] = var = vartype(self.text) + return var + + # Tk implementations of "virtual text methods" -- each platform + # reusing IDLE's support code needs to define these for its GUI's + # flavor of widget. + + # Is character at text_index in a Python string? Return 0 for + # "guaranteed no", true for anything else. This info is expensive + # to compute ab initio, but is probably already known by the + # platform's colorizer. + + def is_char_in_string(self, text_index): + if self.color: + # Return true iff colorizer hasn't (re)gotten this far + # yet, or the character is tagged as being in a string + return self.text.tag_prevrange("TODO", text_index) or \ + "STRING" in self.text.tag_names(text_index) + else: + # The colorizer is missing: assume the worst + return 1 + + # If a selection is defined in the text widget, return (start, + # end) as Tkinter text indices, otherwise return (None, None) + def get_selection_indices(self): + try: + first = self.text.index("sel.first") + last = self.text.index("sel.last") + return first, last + except TclError: + return None, None + + # Return the text widget's current view of what a tab stop means + # (equivalent width in spaces). + + def get_tk_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): + text = self.text + if self.get_tk_tabwidth() != newtabwidth: + # Set text widget tab width + 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) + + def set_indentation_params(self, is_py_src, guess=True): + if is_py_src and guess: + 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) + + def smart_backspace_event(self, event): + text = self.text + first, last = self.get_selection_indices() + if first and last: + text.delete(first, last) + text.mark_set("insert", first) + return "break" + # Delete whitespace left, until hitting a real char or closest + # preceding virtual tab stop. + chars = text.get("insert linestart", "insert") + if chars == '': + if text.compare("insert", ">", "1.0"): + # easy: delete preceding newline + text.delete("insert-1c") + else: + text.bell() # at start of buffer + return "break" + if chars[-1] not in " \t": + # easy: delete preceding real char + text.delete("insert-1c") + return "break" + # Ick. It may require *inserting* spaces if we back up over a + # tab character! This is written to be clear, not fast. + tabwidth = self.tabwidth + have = len(chars.expandtabs(tabwidth)) + 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 == last_line_of_prompt: + break + chars = chars[:-1] + ncharsdeleted = ncharsdeleted + 1 + have = len(chars.expandtabs(tabwidth)) + if have <= want or chars[-1] not in " \t": + break + text.undo_block_start() + text.delete("insert-%dc" % ncharsdeleted, "insert") + if have < want: + text.insert("insert", ' ' * (want - have)) + text.undo_block_stop() + return "break" + + def smart_indent_event(self, event): + # if intraline selection: + # delete it + # elif multiline selection: + # do indent-region + # else: + # indent one level + text = self.text + first, last = self.get_selection_indices() + text.undo_block_start() + try: + if first and last: + if index2line(first) != index2line(last): + return self.indent_region_event(event) + text.delete(first, last) + text.mark_set("insert", first) + prefix = text.get("insert linestart", "insert") + raw, effective = classifyws(prefix, self.tabwidth) + if raw == len(prefix): + # only whitespace to the left + self.reindent_to(effective + self.indentwidth) + else: + # tab to the next 'stop' within or to right of line's text: + if self.usetabs: + pad = '\t' + else: + effective = len(prefix.expandtabs(self.tabwidth)) + n = self.indentwidth + pad = ' ' * (n - effective % n) + text.insert("insert", pad) + text.see("insert") + return "break" + finally: + text.undo_block_stop() + + def newline_and_indent_event(self, event): + text = self.text + first, last = self.get_selection_indices() + text.undo_block_start() + try: + if first and last: + text.delete(first, last) + text.mark_set("insert", first) + line = text.get("insert linestart", "insert") + i, n = 0, len(line) + while i < n and line[i] in " \t": + i = i+1 + if i == n: + # the cursor is in or at leading indentation in a continuation + # line; just inject an empty line at the start + text.insert("insert linestart", '\n') + return "break" + indent = line[:i] + # strip whitespace before insert point unless it's in the prompt + i = 0 + 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: + text.delete("insert - %d chars" % i, "insert") + # strip whitespace after insert point + while text.get("insert") in " \t": + text.delete("insert") + # start new line + text.insert("insert", '\n') + + # 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.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_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 + y.set_lo(bod or 0) + else: + r = text.tag_prevrange("console", "insert") + if r: + startatindex = r[1] + else: + startatindex = "1.0" + rawtext = text.get(startatindex, "insert") + y.set_str(rawtext) + y.set_lo(0) + + c = y.get_continuation_type() + if c != PyParse.C_NONE: + # The current stmt hasn't ended yet. + 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: + # inside a string which started before this line; + # just mimic the current indent + text.insert("insert", indent) + 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: + # 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 + # beyond leftmost =; else to beyond first chunk of + # non-whitespace on initial line + if y.get_num_lines_in_stmt() > 1: + text.insert("insert", indent) + else: + self.reindent_to(y.compute_backslash_indent()) + else: + assert 0, "bogus continuation type %r" % (c,) + return "break" + + # This line starts a brand new stmt; indent relative to + # indentation of initial line of closest preceding + # interesting stmt. + indent = y.get_base_indent_string() + text.insert("insert", indent) + if y.is_block_opener(): + self.smart_indent_event(event) + elif indent and y.is_block_closer(): + self.smart_backspace_event(event) + return "break" + finally: + text.see("insert") + text.undo_block_stop() + + # Our editwin provides an is_char_in_string function that works + # with a Tk text index, but PyParse only knows about offsets into + # a string. This builds a function for PyParse that accepts an + # offset. + + def _build_char_in_string_func(self, startindex): + def inner(offset, _startindex=startindex, + _icis=self.is_char_in_string): + 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() +## if new != self.tabwidth: +## self.tabwidth = new +## 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): + if self.usetabs: + ntabs, nspaces = divmod(n, self.tabwidth) + return '\t' * ntabs + ' ' * nspaces + else: + return ' ' * n + + # Delete from beginning of line to insert point, then reinsert + # column logical (meaning use tabs if appropriate) spaces. + + def reindent_to(self, column): + text = self.text + text.undo_block_start() + if text.compare("insert linestart", "!=", "insert"): + text.delete("insert linestart", "insert") + if column: + 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 + # blocks are found). + + def guess_indent(self): + opener, indented = IndentSearcher(self.text, self.tabwidth).run() + if opener and indented: + raw, indentsmall = classifyws(opener, self.tabwidth) + raw, indentlarge = classifyws(indented, self.tabwidth) + else: + indentsmall = indentlarge = 0 + return indentlarge - indentsmall + +# "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 + +import tokenize +_tokenize = tokenize +del tokenize + +class IndentSearcher(object): + + # .run() chews over the Text widget, looking for a block opener + # and the stmt following it. Returns a pair, + # (line containing block opener, line containing stmt) + # Either or both may be None. + + def __init__(self, text, tabwidth): + self.text = text + self.tabwidth = tabwidth + self.i = self.finished = 0 + self.blkopenline = self.indentedline = None + + def readline(self): + if self.finished: + return "" + i = self.i = self.i + 1 + mark = repr(i) + ".0" + if self.text.compare(mark, ">=", "end"): + return "" + return self.text.get(mark, mark + " lineend+1c") + + def tokeneater(self, type, token, start, end, line, + INDENT=_tokenize.INDENT, + NAME=_tokenize.NAME, + OPENERS=('class', 'def', 'for', 'if', 'try', 'while')): + if self.finished: + pass + elif type == NAME and token in OPENERS: + self.blkopenline = line + elif type == INDENT and self.blkopenline: + self.indentedline = line + self.finished = 1 + + def run(self): + 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): + # since we cut off the tokenizer early, we can trigger + # spurious errors + pass + finally: + _tokenize.tabsize = save_tabsize + return self.blkopenline, self.indentedline + +### end autoindent code ### + +def prepstr(s): + # Helper to extract the underscore from a string, e.g. + # prepstr("Co_py") returns (2, "Copy"). + i = s.find('_') + if i >= 0: + s = s[:i] + s[i+1:] + return i, s + + +keynames = { + 'bracketleft': '[', + 'bracketright': ']', + 'slash': '/', +} + +def get_accelerator(keydefs, eventname): + keylist = keydefs.get(eventname) + # issue10940: temporary workaround to prevent hang with OS X Cocoa Tk 8.5 + # if not keylist: + if (not keylist) or (macosxSupport.isCocoaTk() and eventname in { + "<>", + "<>", + "<>"}): + return "" + s = keylist[0] + s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s) + s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s) + s = re.sub("Key-", "", s) + s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu + s = re.sub("Control-", "Ctrl-", s) + s = re.sub("-", "+", s) + s = re.sub("><", " ", s) + s = re.sub("<", "", s) + s = re.sub(">", "", s) + return s + + +def fixwordbreaks(root): + # 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', '[a-zA-Z0-9_]') + tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]') + + +def _editor_window(parent): # htest # + # error if close master window first - timer event, after script + root = parent + fixwordbreaks(root) + if sys.argv[1:]: + filename = sys.argv[1] + else: + filename = None + macosxSupport.setupApp(root, None) + edit = EditorWindow(root=root, filename=filename) + edit.text.bind("<>", edit.close_event) + # Does not stop error, neither does following + # edit.text.bind("<>", edit.close_event) + +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 new file mode 100644 index 0000000..a9989a8 --- /dev/null +++ b/Lib/idlelib/filelist.py @@ -0,0 +1,129 @@ +import os +from tkinter import * +import tkinter.messagebox as tkMessageBox + + +class FileList: + + # N.B. this import overridden in PyShellFileList. + from idlelib.EditorWindow import EditorWindow + + def __init__(self, root): + self.root = root + self.dict = {} + self.inversedict = {} + self.vars = {} # For EditorWindow.getrawvar (shared Tcl variables) + + def open(self, filename, action=None): + assert filename + filename = self.canonize(filename) + if os.path.isdir(filename): + # This can happen when bad filename is passed on command line: + tkMessageBox.showerror( + "File Error", + "%r is a directory." % (filename,), + master=self.root) + return None + key = os.path.normcase(filename) + if key in self.dict: + edit = self.dict[key] + edit.top.wakeup() + return edit + if action: + # 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 + + def gotofileline(self, filename, lineno=None): + edit = self.open(filename) + if edit is not None and lineno is not None: + edit.gotoline(lineno) + + def new(self, filename=None): + return self.EditorWindow(self, filename) + + def close_all_callback(self, *args, **kwds): + for edit in list(self.inversedict): + reply = edit.close() + if reply == "cancel": + break + return "break" + + def unregister_maybe_terminate(self, edit): + try: + key = self.inversedict[edit] + except KeyError: + print("Don't know this EditorWindow object. (close)") + return + if key: + del self.dict[key] + del self.inversedict[edit] + if not self.inversedict: + self.root.quit() + + def filename_changed_edit(self, edit): + edit.saved_change_hook() + try: + key = self.inversedict[edit] + except KeyError: + print("Don't know this EditorWindow object. (rename)") + return + filename = edit.io.filename + if not filename: + if key: + del self.dict[key] + self.inversedict[edit] = None + return + filename = self.canonize(filename) + newkey = os.path.normcase(filename) + if newkey == key: + return + if newkey in self.dict: + conflict = self.dict[newkey] + self.inversedict[conflict] = None + tkMessageBox.showerror( + "Name Conflict", + "You now have multiple edit windows open for %r" % (filename,), + master=self.root) + self.dict[newkey] = edit + self.inversedict[edit] = newkey + if key: + try: + del self.dict[key] + except KeyError: + pass + + def canonize(self, filename): + if not os.path.isabs(filename): + try: + pwd = os.getcwd() + except OSError: + pass + else: + filename = os.path.join(pwd, filename) + return os.path.normpath(filename) + + +def _test(): + from idlelib.EditorWindow import fixwordbreaks + import sys + root = Tk() + fixwordbreaks(root) + root.withdraw() + flist = FileList(root) + if sys.argv[1:]: + for filename in sys.argv[1:]: + flist.open(filename) + else: + flist.new() + if flist.inversedict: + root.mainloop() + +if __name__ == '__main__': + _test() diff --git a/Lib/idlelib/grep.py b/Lib/idlelib/grep.py new file mode 100644 index 0000000..721b231 --- /dev/null +++ b/Lib/idlelib/grep.py @@ -0,0 +1,158 @@ +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, errors='replace') as f: + for lineno, line in enumerate(f, 1): + if line[-1:] == '\n': + line = line[:-1] + if prog.search(line): + sys.stdout.write("%s: %s: %s\n" % + (fn, lineno, line)) + hits += 1 + except OSError as msg: + print(msg) + print(("Hits found: %s\n" + "(Hint: right-click to open locations.)" + % hits) if hits else "No hits.") + except AttributeError: + # Tk window has been closed, OutputWindow.text = None, + # so in OW.write, OW.text.insert fails. + pass + + def findfiles(self, dir, base, rec): + try: + names = os.listdir(dir or os.curdir) + except OSError 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/help_about.py b/Lib/idlelib/help_about.py new file mode 100644 index 0000000..3112e6a --- /dev/null +++ b/Lib/idlelib/help_about.py @@ -0,0 +1,149 @@ +"""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('',self.Ok) #dismiss dialog + self.bind('',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', 'ascii') + + 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.destroy() + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(AboutDialog) diff --git a/Lib/idlelib/history.py b/Lib/idlelib/history.py new file mode 100644 index 0000000..078af29 --- /dev/null +++ b/Lib/idlelib/history.py @@ -0,0 +1,104 @@ +"Implement Idle Shell history mechanism with History class" + +from idlelib.configHandler import idleConf + +class History: + ''' Implement Idle Shell history mechanism. + + store - Store source statement (called from PyShell.resetoutput). + fetch - Fetch stored statement matching prefix already entered. + history_next - Bound to <> event (default Alt-N). + history_prev - Bound to <> event (default Alt-P). + ''' + def __init__(self, text): + '''Initialize data attributes and bind event methods. + + .text - Idle wrapper of tk Text widget, with .bell(). + .history - source statements, possibly with multiple lines. + .prefix - source already entered at prompt; filters history list. + .pointer - index into history. + .cyclic - wrap around history list (or not). + ''' + self.text = text + self.history = [] + self.prefix = None + self.pointer = None + self.cyclic = idleConf.GetOption("main", "History", "cyclic", 1, "bool") + text.bind("<>", self.history_prev) + text.bind("<>", self.history_next) + + def history_next(self, event): + "Fetch later statement; start with ealiest if cyclic." + self.fetch(reverse=False) + return "break" + + def history_prev(self, event): + "Fetch earlier statement; start with most recent." + self.fetch(reverse=True) + return "break" + + def fetch(self, reverse): + '''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. + Sound bell when return to start line or cannot leave a line + because cyclic is False. + ''' + nhist = len(self.history) + pointer = self.pointer + prefix = self.prefix + if pointer is not None and prefix is not None: + if self.text.compare("insert", "!=", "end-1c") or \ + self.text.get("iomark", "end-1c") != self.history[pointer]: + pointer = prefix = None + self.text.mark_set("insert", "end-1c") # != after cursor move + if pointer is None or prefix is None: + prefix = self.text.get("iomark", "end-1c") + if reverse: + pointer = nhist # will be decremented + else: + if self.cyclic: + pointer = -1 # will be incremented + else: # abort history_next + self.text.bell() + return + nprefix = len(prefix) + while 1: + pointer += -1 if reverse else 1 + if pointer < 0 or pointer >= nhist: + self.text.bell() + if not self.cyclic and pointer < 0: # abort history_prev + return + else: + if self.text.get("iomark", "end-1c") != prefix: + self.text.delete("iomark", "end-1c") + self.text.insert("iomark", prefix) + pointer = prefix = None + break + item = self.history[pointer] + if item[:nprefix] == prefix and len(item) > nprefix: + self.text.delete("iomark", "end-1c") + self.text.insert("iomark", item) + break + self.text.see("insert") + self.text.tag_remove("sel", "1.0", "end") + self.pointer = pointer + self.prefix = prefix + + def store(self, source): + "Store Shell input statement into history list." + source = source.strip() + if len(source) > 2: + # avoid duplicates + try: + self.history.remove(source) + except ValueError: + pass + self.history.append(source) + self.pointer = None + self.prefix = None + +if __name__ == "__main__": + from unittest import main + main('idlelib.idle_test.test_idlehistory', verbosity=2, exit=False) diff --git a/Lib/idlelib/hyperparser.py b/Lib/idlelib/hyperparser.py new file mode 100644 index 0000000..77cb057 --- /dev/null +++ b/Lib/idlelib/hyperparser.py @@ -0,0 +1,313 @@ +"""Provide advanced parsing abilities for ParenMatch and other extensions. + +HyperParser uses PyParser. PyParser mostly gives information on the +proper indentation of code. HyperParser gives additional information on +the structure of code. +""" + +import string +from keyword import iskeyword +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)] + + +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) + + def index2line(index): + return int(float(index)) + lno = index2line(text.index(index)) + + if not editwin.context_use_ps1: + for context in editwin.num_context_lines: + startat = max(lno - context, 1) + startatindex = repr(startat) + ".0" + stopatindex = "%d.end" % lno + # We add the newline because PyParse requires a newline + # at end. We add a space so that index won't be at end + # of line, so that its status will be the same as the + # char before it, if should. + 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: + break + parser.set_lo(bod or 0) + else: + r = text.tag_prevrange("console", index) + if r: + startatindex = r[1] + else: + startatindex = "1.0" + stopatindex = "%d.end" % lno + # We add the newline because PyParse requires it. We add a + # space so that index won't be at end of line, so that its + # status will be the same as the char before it, if should. + parser.set_str(text.get(startatindex, stopatindex)+' \n') + parser.set_lo(0) + + # We want what the parser has, minus the last newline and space. + 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 + self.bracketing = parser.get_last_stmt_bracketing() + # find which pairs of bracketing are openers. These always + # correspond to a character of rawtext. + self.isopener = [i>0 and self.bracketing[i][1] > + self.bracketing[i-1][1] + for i in range(len(self.bracketing))] + + self.set_index(index) + + def set_index(self, index): + """Set the index to which the functions relate. + + The index must be in the same statement. + """ + indexinrawtext = (len(self.rawtext) - + len(self.text.get(index, self.stopatindex))) + if indexinrawtext < 0: + raise ValueError("Index %s precedes the analyzed statement" + % index) + self.indexinrawtext = indexinrawtext + # find the rightmost bracket to which index belongs + self.indexbracket = 0 + while (self.indexbracket < len(self.bracketing)-1 and + self.bracketing[self.indexbracket+1][0] < self.indexinrawtext): + self.indexbracket += 1 + if (self.indexbracket < len(self.bracketing)-1 and + self.bracketing[self.indexbracket+1][0] == self.indexinrawtext and + not self.isopener[self.indexbracket+1]): + self.indexbracket += 1 + + def is_in_string(self): + """Is the index given to the HyperParser in a string?""" + # The bracket to which we belong should be an opener. + # If it's an opener, it has to have a character. + return (self.isopener[self.indexbracket] and + self.rawtext[self.bracketing[self.indexbracket][0]] + in ('"', "'")) + + def is_in_code(self): + """Is the index given to the HyperParser in normal code?""" + return (not self.isopener[self.indexbracket] or + self.rawtext[self.bracketing[self.indexbracket][0]] + not in ('#', '"', "'")) + + def get_surrounding_brackets(self, openers='([{', mustclose=False): + """Return bracket indexes or None. + + If the index given to the HyperParser is surrounded by a + bracket defined in openers (or at least has one before it), + return the indices of the opening bracket and the closing + bracket (or the end of line, whichever comes first). + + If it is not surrounded by brackets, or the end of line comes + before the closing bracket and mustclose is True, returns None. + """ + + bracketinglevel = self.bracketing[self.indexbracket][1] + before = self.indexbracket + while (not self.isopener[before] or + self.rawtext[self.bracketing[before][0]] not in openers or + self.bracketing[before][1] > bracketinglevel): + before -= 1 + if before < 0: + return None + bracketinglevel = min(bracketinglevel, self.bracketing[before][1]) + after = self.indexbracket + 1 + while (after < len(self.bracketing) and + self.bracketing[after][1] >= bracketinglevel): + after += 1 + + beforeindex = self.text.index("%s-%dc" % + (self.stopatindex, len(self.rawtext)-self.bracketing[before][0])) + if (after >= len(self.bracketing) or + self.bracketing[after][0] > len(self.rawtext)): + if mustclose: + return None + afterindex = self.stopatindex + else: + # We are after a real char, so it is a ')' and we give the + # index before it. + afterindex = self.text.index( + "%s-%dc" % (self.stopatindex, + len(self.rawtext)-(self.bracketing[after][0]-1))) + + 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. + 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])] + ): + 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 + + 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. + """ + if not self.is_in_code(): + raise ValueError("get_expression should only be called" + "if index is inside a code.") + + rawtext = self.rawtext + bracketing = self.bracketing + + brck_index = self.indexbracket + brck_limit = bracketing[brck_index][0] + pos = self.indexinrawtext + + last_identifier_pos = pos + postdot_phase = True + + while 1: + # Eat whitespaces, comments, and if postdot_phase is False - a dot + while 1: + if pos>brck_limit and rawtext[pos-1] in self._whitespace_chars: + # Eat a whitespace + pos -= 1 + elif (not postdot_phase and + pos > brck_limit and rawtext[pos-1] == '.'): + # Eat a dot + pos -= 1 + postdot_phase = True + # The next line will fail if we are *inside* a comment, + # but we shouldn't be. + elif (pos == brck_limit and brck_index > 0 and + rawtext[bracketing[brck_index-1][0]] == '#'): + # Eat a comment + brck_index -= 2 + brck_limit = bracketing[brck_index][0] + pos = bracketing[brck_index+1][0] + else: + # If we didn't eat anything, quit. + break + + if not postdot_phase: + # We didn't find a dot, so the expression end at the + # last identifier pos. + break + + ret = self._eat_identifier(rawtext, brck_limit, pos) + if ret: + # There is an identifier to eat + pos = pos - ret + last_identifier_pos = pos + # Now, to continue the search, we must find a dot. + postdot_phase = False + # (the loop continues now) + + elif pos == brck_limit: + # We are at a bracketing limit. If it is a closing + # bracket, eat the bracket, otherwise, stop the search. + level = bracketing[brck_index][1] + while brck_index > 0 and bracketing[brck_index-1][1] > level: + brck_index -= 1 + if bracketing[brck_index][0] == brck_limit: + # We were not at the end of a closing bracket + break + pos = bracketing[brck_index][0] + brck_index -= 1 + brck_limit = bracketing[brck_index][0] + last_identifier_pos = pos + if rawtext[pos] in "([": + # [] and () may be used after an identifier, so we + # continue. postdot_phase is True, so we don't allow a dot. + pass + else: + # We can't continue after other types of brackets + if rawtext[pos] in "'\"": + # Scan a string prefix + while pos > 0 and rawtext[pos - 1] in "rRbBuU": + pos -= 1 + last_identifier_pos = pos + break + + else: + # We've found an operator or something. + break + + return rawtext[last_identifier_pos:self.indexinrawtext] + + +if __name__ == '__main__': + import unittest + unittest.main('idlelib.idle_test.test_hyperparser', verbosity=2) diff --git a/Lib/idlelib/idle_test/test_formatparagraph.py b/Lib/idlelib/idle_test/test_formatparagraph.py deleted file mode 100644 index f6039e6..0000000 --- a/Lib/idlelib/idle_test/test_formatparagraph.py +++ /dev/null @@ -1,377 +0,0 @@ -# Test the functions and main class method of FormatParagraph.py -import unittest -from idlelib import FormatParagraph as fp -from idlelib.EditorWindow import EditorWindow -from tkinter import Tk, Text -from test.support import requires - - -class Is_Get_Test(unittest.TestCase): - """Test the is_ and get_ functions""" - test_comment = '# This is a comment' - test_nocomment = 'This is not a comment' - trailingws_comment = '# This is a comment ' - leadingws_comment = ' # This is a comment' - leadingws_nocomment = ' This is not a comment' - - def test_is_all_white(self): - 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(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(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(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 FormatParagraph. - - Using the runcase() function, find_paragraph() is called with 'mark' set at - multiple indexes before and inside the test paragraph. - - It appears that code with the same indentation as a quoted string is grouped - as part of the same paragraph, which is probably incorrect behavior. - """ - - @classmethod - def setUpClass(cls): - from idlelib.idle_test.mock_tk import Text - cls.text = Text() - - def runcase(self, inserttext, stopline, expected): - # Check that find_paragraph returns the expected paragraph when - # the mark index is set to beginning, middle, end of each line - # up to but not including the stop line - text = self.text - text.insert('1.0', inserttext) - for line in range(1, stopline): - linelength = int(text.index("%d.end" % line).split('.')[1]) - for col in (0, linelength//2, linelength): - tempindex = "%d.%d" % (line, col) - self.assertEqual(fp.find_paragraph(text, tempindex), expected) - text.delete('1.0', 'end') - - def test_find_comment(self): - comment = ( - "# Comment block with no blank lines before\n" - "# Comment line\n" - "\n") - self.runcase(comment, 3, ('1.0', '3.0', '#', comment[0:58])) - - comment = ( - "\n" - "# Comment block with whitespace line before and after\n" - "# Comment line\n" - "\n") - self.runcase(comment, 4, ('2.0', '4.0', '#', comment[1:70])) - - comment = ( - "\n" - " # Indented comment block with whitespace before and after\n" - " # Comment line\n" - "\n") - self.runcase(comment, 4, ('2.0', '4.0', ' #', comment[1:82])) - - comment = ( - "\n" - "# Single line comment\n" - "\n") - self.runcase(comment, 3, ('2.0', '3.0', '#', comment[1:23])) - - comment = ( - "\n" - " # Single line comment with leading whitespace\n" - "\n") - self.runcase(comment, 3, ('2.0', '3.0', ' #', comment[1:51])) - - comment = ( - "\n" - "# Comment immediately followed by code\n" - "x = 42\n" - "\n") - self.runcase(comment, 3, ('2.0', '3.0', '#', comment[1:40])) - - comment = ( - "\n" - " # Indented comment immediately followed by code\n" - "x = 42\n" - "\n") - self.runcase(comment, 3, ('2.0', '3.0', ' #', comment[1:53])) - - comment = ( - "\n" - "# Comment immediately followed by indented code\n" - " x = 42\n" - "\n") - self.runcase(comment, 3, ('2.0', '3.0', '#', comment[1:49])) - - def test_find_paragraph(self): - teststring = ( - '"""String with no blank lines before\n' - 'String line\n' - '"""\n' - '\n') - self.runcase(teststring, 4, ('1.0', '4.0', '', teststring[0:53])) - - teststring = ( - "\n" - '"""String with whitespace line before and after\n' - 'String line.\n' - '"""\n' - '\n') - self.runcase(teststring, 5, ('2.0', '5.0', '', teststring[1:66])) - - teststring = ( - '\n' - ' """Indented string with whitespace before and after\n' - ' Comment string.\n' - ' """\n' - '\n') - self.runcase(teststring, 5, ('2.0', '5.0', ' ', teststring[1:85])) - - teststring = ( - '\n' - '"""Single line string."""\n' - '\n') - self.runcase(teststring, 3, ('2.0', '3.0', '', teststring[1:27])) - - teststring = ( - '\n' - ' """Single line string with leading whitespace."""\n' - '\n') - self.runcase(teststring, 3, ('2.0', '3.0', ' ', teststring[1:55])) - - -class ReformatFunctionTest(unittest.TestCase): - """Test the reformat_paragraph function without the editor window.""" - - def test_reformat_paragrah(self): - Equal = self.assertEqual - reform = fp.reformat_paragraph - hw = "O hello world" - Equal(reform(' ', 1), ' ') - Equal(reform("Hello world", 20), "Hello world") - - # Test without leading newline - Equal(reform(hw, 1), "O\nhello\nworld") - Equal(reform(hw, 6), "O\nhello\nworld") - Equal(reform(hw, 7), "O hello\nworld") - Equal(reform(hw, 12), "O hello\nworld") - Equal(reform(hw, 13), "O hello world") - - # Test with leading newline - hw = "\nO hello world" - Equal(reform(hw, 1), "\nO\nhello\nworld") - Equal(reform(hw, 6), "\nO\nhello\nworld") - Equal(reform(hw, 7), "\nO hello\nworld") - Equal(reform(hw, 12), "\nO hello\nworld") - Equal(reform(hw, 13), "\nO hello world") - - -class ReformatCommentTest(unittest.TestCase): - """Test the reformat_comment function without the editor window.""" - - def test_reformat_comment(self): - Equal = self.assertEqual - - # reformat_comment formats to a minimum of 20 characters - 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 = 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?\"\"\"") - Equal(result, expected) - - 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 = 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?") - Equal(result, expected) - - -class FormatClassTest(unittest.TestCase): - def test_init_close(self): - instance = fp.FormatParagraph('editor') - self.assertEqual(instance.editwin, 'editor') - instance.close() - self.assertEqual(instance.editwin, None) - - -# For testing format_paragraph_event, Initialize FormatParagraph with -# a mock Editor with .text and .get_selection_indices. The text must -# be a Text wrapper that adds two methods - -# A real EditorWindow creates unneeded, time-consuming baggage and -# sometimes emits shutdown warnings like this: -# "warning: callback failed in WindowList -# : invalid command name ".55131368.windows". -# Calling EditorWindow._close in tearDownClass prevents this but causes -# other problems (windows left open). - -class TextWrapper: - def __init__(self, master): - self.text = Text(master=master) - def __getattr__(self, name): - return getattr(self.text, name) - def undo_block_start(self): pass - def undo_block_stop(self): pass - -class Editor: - def __init__(self, root): - self.text = TextWrapper(root) - get_selection_indices = EditorWindow. get_selection_indices - -class FormatEventTest(unittest.TestCase): - """Test the formatting of text inside a Text widget. - - This is done with FormatParagraph.format.paragraph_event, - which calls functions in the module as appropriate. - """ - test_string = ( - " '''this is a test of a reformat for a triple " - "quoted string will it reformat to less than 70 " - "characters for me?'''\n") - multiline_test_string = ( - " '''The first line is under the max width.\n" - " The second line's length is way over the max width. It goes " - "on and on until it is over 100 characters long.\n" - " Same thing with the third line. It is also way over the max " - "width, but FormatParagraph will fix it.\n" - " '''\n") - multiline_test_comment = ( - "# The first line is under the max width.\n" - "# The second line's length is way over the max width. It goes on " - "and on until it is over 100 characters long.\n" - "# Same thing with the third line. It is also way over the max " - "width, but FormatParagraph will fix it.\n" - "# The fourth line is short like the first line.") - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - editor = Editor(root=cls.root) - cls.text = editor.text.text # Test code does not need the wrapper. - cls.formatter = fp.FormatParagraph(editor).format_paragraph_event - # Sets the insert mark just after the re-wrapped and inserted text. - - @classmethod - def tearDownClass(cls): - cls.root.destroy() - del cls.root - del cls.text - del cls.formatter - - def test_short_line(self): - self.text.insert('1.0', "Short line\n") - self.formatter("Dummy") - self.assertEqual(self.text.get('1.0', 'insert'), "Short line\n" ) - self.text.delete('1.0', 'end') - - def test_long_line(self): - text = self.text - - # Set cursor ('insert' mark) to '1.0', within text. - text.insert('1.0', self.test_string) - text.mark_set('insert', '1.0') - self.formatter('ParameterDoesNothing', limit=70) - result = text.get('1.0', 'insert') - # find function includes \n - expected = ( -" '''this is a test of a reformat for a triple quoted string will it\n" -" reformat to less than 70 characters for me?'''\n") # yes - self.assertEqual(result, expected) - text.delete('1.0', 'end') - - # Select from 1.11 to line end. - text.insert('1.0', self.test_string) - text.tag_add('sel', '1.11', '1.end') - self.formatter('ParameterDoesNothing', limit=70) - result = text.get('1.0', 'insert') - # selection excludes \n - expected = ( -" '''this is a test of a reformat for a triple quoted string will it reformat\n" -" to less than 70 characters for me?'''") # no - self.assertEqual(result, expected) - text.delete('1.0', 'end') - - def test_multiple_lines(self): - text = self.text - # Select 2 long lines. - text.insert('1.0', self.multiline_test_string) - text.tag_add('sel', '2.0', '4.0') - self.formatter('ParameterDoesNothing', limit=70) - result = text.get('2.0', 'insert') - expected = ( -" The second line's length is way over the max width. It goes on and\n" -" on until it is over 100 characters long. Same thing with the third\n" -" line. It is also way over the max width, but FormatParagraph will\n" -" fix it.\n") - self.assertEqual(result, expected) - text.delete('1.0', 'end') - - def test_comment_block(self): - text = self.text - - # Set cursor ('insert') to '1.0', within block. - text.insert('1.0', self.multiline_test_comment) - self.formatter('ParameterDoesNothing', limit=70) - result = text.get('1.0', 'insert') - expected = ( -"# The first line is under the max width. The second line's length is\n" -"# way over the max width. It goes on and on until it is over 100\n" -"# characters long. Same thing with the third line. It is also way over\n" -"# the max width, but FormatParagraph will fix it. The fourth line is\n" -"# short like the first line.\n") - self.assertEqual(result, expected) - text.delete('1.0', 'end') - - # Select line 2, verify line 1 unaffected. - text.insert('1.0', self.multiline_test_comment) - text.tag_add('sel', '2.0', '3.0') - self.formatter('ParameterDoesNothing', limit=70) - result = text.get('1.0', 'insert') - expected = ( -"# The first line is under the max width.\n" -"# The second line's length is way over the max width. It goes on and\n" -"# on until it is over 100 characters long.\n") - self.assertEqual(result, expected) - text.delete('1.0', 'end') - -# The following block worked with EditorWindow but fails with the mock. -# Lines 2 and 3 get pasted together even though the previous block left -# the previous line alone. More investigation is needed. -## # Select lines 3 and 4 -## text.insert('1.0', self.multiline_test_comment) -## text.tag_add('sel', '3.0', '5.0') -## self.formatter('ParameterDoesNothing') -## result = text.get('3.0', 'insert') -## expected = ( -##"# Same thing with the third line. It is also way over the max width,\n" -##"# but FormatParagraph will fix it. The fourth line is short like the\n" -##"# first line.\n") -## self.assertEqual(result, expected) -## text.delete('1.0', 'end') - - -if __name__ == '__main__': - unittest.main(verbosity=2, exit=2) diff --git a/Lib/idlelib/idle_test/test_history.py b/Lib/idlelib/idle_test/test_history.py new file mode 100644 index 0000000..d7c3d70 --- /dev/null +++ b/Lib/idlelib/idle_test/test_history.py @@ -0,0 +1,167 @@ +import unittest +from test.support import requires + +import tkinter as tk +from tkinter import Text as tkText +from idlelib.idle_test.mock_tk import Text as mkText +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''' + + @classmethod + def setUpClass(cls): + cls.text = mkText() + cls.history = History(cls.text) + + def tearDown(self): + self.text.delete('1.0', 'end') + self.history.history = [] + + def test_init(self): + self.assertIs(self.history.text, self.text) + self.assertEqual(self.history.history, []) + self.assertIsNone(self.history.prefix) + self.assertIsNone(self.history.pointer) + self.assertEqual(self.history.cyclic, + idleConf.GetOption("main", "History", "cyclic", 1, "bool")) + + def test_store_short(self): + self.history.store('a') + self.assertEqual(self.history.history, []) + self.history.store(' a ') + self.assertEqual(self.history.history, []) + + def test_store_dup(self): + self.history.store(line1) + self.assertEqual(self.history.history, [line1]) + self.history.store(line2) + self.assertEqual(self.history.history, [line1, line2]) + self.history.store(line1) + self.assertEqual(self.history.history, [line2, line1]) + + def test_store_reset(self): + self.history.prefix = line1 + self.history.pointer = 0 + self.history.store(line2) + self.assertIsNone(self.history.prefix) + self.assertIsNone(self.history.pointer) + + +class TextWrapper: + def __init__(self, master): + self.text = tkText(master=master) + self._bell = False + def __getattr__(self, name): + return getattr(self.text, name) + def bell(self): + self._bell = True + +class FetchTest(unittest.TestCase): + '''Test History.fetch with wrapped tk.Text. + ''' + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = tk.Tk() + + def setUp(self): + self.text = text = TextWrapper(self.root) + text.insert('1.0', ">>> ") + text.mark_set('iomark', '1.4') + text.mark_gravity('iomark', 'left') + self.history = History(text) + self.history.history = [line1, line2] + + @classmethod + def tearDownClass(cls): + cls.root.destroy() + del cls.root + + 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. + History = self.history + History.fetch(reverse) + + Equal = self.assertEqual + Equal(self.text.get('iomark', 'end-1c'), line) + Equal(self.text._bell, bell) + if bell: + self.text._bell = False + Equal(History.prefix, prefix) + Equal(History.pointer, index) + Equal(self.text.compare("insert", '==', "end-1c"), 1) + + def test_fetch_prev_cyclic(self): + prefix = '' + test = self.fetch_test + test(True, line2, prefix, 1) + test(True, line1, prefix, 0) + test(True, prefix, None, None, bell=True) + + def test_fetch_next_cyclic(self): + prefix = '' + test = self.fetch_test + test(False, line1, prefix, 0) + test(False, line2, prefix, 1) + test(False, prefix, None, None, bell=True) + + # Prefix 'a' tests skip line2, which starts with 'b' + def test_fetch_prev_prefix(self): + prefix = 'a' + self.text.insert('iomark', prefix) + self.fetch_test(True, line1, prefix, 0) + self.fetch_test(True, prefix, None, None, bell=True) + + def test_fetch_next_prefix(self): + prefix = 'a' + self.text.insert('iomark', prefix) + self.fetch_test(False, line1, prefix, 0) + self.fetch_test(False, prefix, None, None, bell=True) + + def test_fetch_prev_noncyclic(self): + prefix = '' + self.history.cyclic = False + test = self.fetch_test + test(True, line2, prefix, 1) + test(True, line1, prefix, 0) + test(True, line1, prefix, 0, bell=True) + + def test_fetch_next_noncyclic(self): + prefix = '' + self.history.cyclic = False + test = self.fetch_test + test(False, prefix, None, None, bell=True) + test(True, line2, prefix, 1) + test(False, prefix, None, None, bell=True) + test(False, prefix, None, None, bell=True) + + def test_fetch_cursor_move(self): + # Move cursor after fetch + self.history.fetch(reverse=True) # initialization + self.text.mark_set('insert', 'iomark') + self.fetch_test(True, line2, None, None, bell=True) + + def test_fetch_edit(self): + # Edit after fetch + self.history.fetch(reverse=True) # initialization + self.text.delete('iomark', 'insert', ) + self.text.insert('iomark', 'a =') + self.fetch_test(True, line1, 'a =', 0) # prefix is reset + + def test_history_prev_next(self): + # Minimally test functions bound to events + self.history.history_prev('dummy event') + self.assertEqual(self.history.pointer, 1) + self.history.history_next('dummy event') + self.assertEqual(self.history.pointer, None) + + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=2) diff --git a/Lib/idlelib/idle_test/test_idlehistory.py b/Lib/idlelib/idle_test/test_idlehistory.py deleted file mode 100644 index d7c3d70..0000000 --- a/Lib/idlelib/idle_test/test_idlehistory.py +++ /dev/null @@ -1,167 +0,0 @@ -import unittest -from test.support import requires - -import tkinter as tk -from tkinter import Text as tkText -from idlelib.idle_test.mock_tk import Text as mkText -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''' - - @classmethod - def setUpClass(cls): - cls.text = mkText() - cls.history = History(cls.text) - - def tearDown(self): - self.text.delete('1.0', 'end') - self.history.history = [] - - def test_init(self): - self.assertIs(self.history.text, self.text) - self.assertEqual(self.history.history, []) - self.assertIsNone(self.history.prefix) - self.assertIsNone(self.history.pointer) - self.assertEqual(self.history.cyclic, - idleConf.GetOption("main", "History", "cyclic", 1, "bool")) - - def test_store_short(self): - self.history.store('a') - self.assertEqual(self.history.history, []) - self.history.store(' a ') - self.assertEqual(self.history.history, []) - - def test_store_dup(self): - self.history.store(line1) - self.assertEqual(self.history.history, [line1]) - self.history.store(line2) - self.assertEqual(self.history.history, [line1, line2]) - self.history.store(line1) - self.assertEqual(self.history.history, [line2, line1]) - - def test_store_reset(self): - self.history.prefix = line1 - self.history.pointer = 0 - self.history.store(line2) - self.assertIsNone(self.history.prefix) - self.assertIsNone(self.history.pointer) - - -class TextWrapper: - def __init__(self, master): - self.text = tkText(master=master) - self._bell = False - def __getattr__(self, name): - return getattr(self.text, name) - def bell(self): - self._bell = True - -class FetchTest(unittest.TestCase): - '''Test History.fetch with wrapped tk.Text. - ''' - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = tk.Tk() - - def setUp(self): - self.text = text = TextWrapper(self.root) - text.insert('1.0', ">>> ") - text.mark_set('iomark', '1.4') - text.mark_gravity('iomark', 'left') - self.history = History(text) - self.history.history = [line1, line2] - - @classmethod - def tearDownClass(cls): - cls.root.destroy() - del cls.root - - 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. - History = self.history - History.fetch(reverse) - - Equal = self.assertEqual - Equal(self.text.get('iomark', 'end-1c'), line) - Equal(self.text._bell, bell) - if bell: - self.text._bell = False - Equal(History.prefix, prefix) - Equal(History.pointer, index) - Equal(self.text.compare("insert", '==', "end-1c"), 1) - - def test_fetch_prev_cyclic(self): - prefix = '' - test = self.fetch_test - test(True, line2, prefix, 1) - test(True, line1, prefix, 0) - test(True, prefix, None, None, bell=True) - - def test_fetch_next_cyclic(self): - prefix = '' - test = self.fetch_test - test(False, line1, prefix, 0) - test(False, line2, prefix, 1) - test(False, prefix, None, None, bell=True) - - # Prefix 'a' tests skip line2, which starts with 'b' - def test_fetch_prev_prefix(self): - prefix = 'a' - self.text.insert('iomark', prefix) - self.fetch_test(True, line1, prefix, 0) - self.fetch_test(True, prefix, None, None, bell=True) - - def test_fetch_next_prefix(self): - prefix = 'a' - self.text.insert('iomark', prefix) - self.fetch_test(False, line1, prefix, 0) - self.fetch_test(False, prefix, None, None, bell=True) - - def test_fetch_prev_noncyclic(self): - prefix = '' - self.history.cyclic = False - test = self.fetch_test - test(True, line2, prefix, 1) - test(True, line1, prefix, 0) - test(True, line1, prefix, 0, bell=True) - - def test_fetch_next_noncyclic(self): - prefix = '' - self.history.cyclic = False - test = self.fetch_test - test(False, prefix, None, None, bell=True) - test(True, line2, prefix, 1) - test(False, prefix, None, None, bell=True) - test(False, prefix, None, None, bell=True) - - def test_fetch_cursor_move(self): - # Move cursor after fetch - self.history.fetch(reverse=True) # initialization - self.text.mark_set('insert', 'iomark') - self.fetch_test(True, line2, None, None, bell=True) - - def test_fetch_edit(self): - # Edit after fetch - self.history.fetch(reverse=True) # initialization - self.text.delete('iomark', 'insert', ) - self.text.insert('iomark', 'a =') - self.fetch_test(True, line1, 'a =', 0) # prefix is reset - - def test_history_prev_next(self): - # Minimally test functions bound to events - self.history.history_prev('dummy event') - self.assertEqual(self.history.pointer, 1) - self.history.history_next('dummy event') - self.assertEqual(self.history.pointer, None) - - -if __name__ == '__main__': - unittest.main(verbosity=2, exit=2) diff --git a/Lib/idlelib/idle_test/test_io.py b/Lib/idlelib/idle_test/test_io.py deleted file mode 100644 index e0e3b98..0000000 --- a/Lib/idlelib/idle_test/test_io.py +++ /dev/null @@ -1,233 +0,0 @@ -import unittest -import io -from idlelib.PyShell import PseudoInputFile, PseudoOutputFile - - -class S(str): - def __str__(self): - return '%s:str' % type(self).__name__ - def __unicode__(self): - return '%s:unicode' % type(self).__name__ - def __len__(self): - return 3 - def __iter__(self): - return iter('abc') - def __getitem__(self, *args): - return '%s:item' % type(self).__name__ - def __getslice__(self, *args): - return '%s:slice' % type(self).__name__ - -class MockShell: - def __init__(self): - self.reset() - - def write(self, *args): - self.written.append(args) - - def readline(self): - return self.lines.pop() - - def close(self): - pass - - def reset(self): - self.written = [] - - def push(self, lines): - self.lines = list(lines)[::-1] - - -class PseudeOutputFilesTest(unittest.TestCase): - def test_misc(self): - shell = MockShell() - f = PseudoOutputFile(shell, 'stdout', 'utf-8') - self.assertIsInstance(f, io.TextIOBase) - self.assertEqual(f.encoding, 'utf-8') - self.assertIsNone(f.errors) - self.assertIsNone(f.newlines) - self.assertEqual(f.name, '') - self.assertFalse(f.closed) - self.assertTrue(f.isatty()) - self.assertFalse(f.readable()) - self.assertTrue(f.writable()) - self.assertFalse(f.seekable()) - - def test_unsupported(self): - shell = MockShell() - f = PseudoOutputFile(shell, 'stdout', 'utf-8') - self.assertRaises(OSError, f.fileno) - self.assertRaises(OSError, f.tell) - self.assertRaises(OSError, f.seek, 0) - self.assertRaises(OSError, f.read, 0) - self.assertRaises(OSError, f.readline, 0) - - def test_write(self): - shell = MockShell() - f = PseudoOutputFile(shell, 'stdout', 'utf-8') - f.write('test') - self.assertEqual(shell.written, [('test', 'stdout')]) - shell.reset() - f.write('t\xe8st') - self.assertEqual(shell.written, [('t\xe8st', 'stdout')]) - shell.reset() - - f.write(S('t\xe8st')) - self.assertEqual(shell.written, [('t\xe8st', 'stdout')]) - self.assertEqual(type(shell.written[0][0]), str) - shell.reset() - - self.assertRaises(TypeError, f.write) - self.assertEqual(shell.written, []) - self.assertRaises(TypeError, f.write, b'test') - self.assertRaises(TypeError, f.write, 123) - self.assertEqual(shell.written, []) - self.assertRaises(TypeError, f.write, 'test', 'spam') - self.assertEqual(shell.written, []) - - def test_writelines(self): - shell = MockShell() - f = PseudoOutputFile(shell, 'stdout', 'utf-8') - f.writelines([]) - self.assertEqual(shell.written, []) - shell.reset() - f.writelines(['one\n', 'two']) - self.assertEqual(shell.written, - [('one\n', 'stdout'), ('two', 'stdout')]) - shell.reset() - f.writelines(['on\xe8\n', 'tw\xf2']) - self.assertEqual(shell.written, - [('on\xe8\n', 'stdout'), ('tw\xf2', 'stdout')]) - shell.reset() - - f.writelines([S('t\xe8st')]) - self.assertEqual(shell.written, [('t\xe8st', 'stdout')]) - self.assertEqual(type(shell.written[0][0]), str) - shell.reset() - - self.assertRaises(TypeError, f.writelines) - self.assertEqual(shell.written, []) - self.assertRaises(TypeError, f.writelines, 123) - self.assertEqual(shell.written, []) - self.assertRaises(TypeError, f.writelines, [b'test']) - self.assertRaises(TypeError, f.writelines, [123]) - self.assertEqual(shell.written, []) - self.assertRaises(TypeError, f.writelines, [], []) - self.assertEqual(shell.written, []) - - def test_close(self): - shell = MockShell() - f = PseudoOutputFile(shell, 'stdout', 'utf-8') - self.assertFalse(f.closed) - f.write('test') - f.close() - self.assertTrue(f.closed) - self.assertRaises(ValueError, f.write, 'x') - self.assertEqual(shell.written, [('test', 'stdout')]) - f.close() - self.assertRaises(TypeError, f.close, 1) - - -class PseudeInputFilesTest(unittest.TestCase): - def test_misc(self): - shell = MockShell() - f = PseudoInputFile(shell, 'stdin', 'utf-8') - self.assertIsInstance(f, io.TextIOBase) - self.assertEqual(f.encoding, 'utf-8') - self.assertIsNone(f.errors) - self.assertIsNone(f.newlines) - self.assertEqual(f.name, '') - self.assertFalse(f.closed) - self.assertTrue(f.isatty()) - self.assertTrue(f.readable()) - self.assertFalse(f.writable()) - self.assertFalse(f.seekable()) - - def test_unsupported(self): - shell = MockShell() - f = PseudoInputFile(shell, 'stdin', 'utf-8') - self.assertRaises(OSError, f.fileno) - self.assertRaises(OSError, f.tell) - self.assertRaises(OSError, f.seek, 0) - self.assertRaises(OSError, f.write, 'x') - self.assertRaises(OSError, f.writelines, ['x']) - - def test_read(self): - shell = MockShell() - f = PseudoInputFile(shell, 'stdin', 'utf-8') - shell.push(['one\n', 'two\n', '']) - self.assertEqual(f.read(), 'one\ntwo\n') - shell.push(['one\n', 'two\n', '']) - self.assertEqual(f.read(-1), 'one\ntwo\n') - shell.push(['one\n', 'two\n', '']) - self.assertEqual(f.read(None), 'one\ntwo\n') - shell.push(['one\n', 'two\n', 'three\n', '']) - self.assertEqual(f.read(2), 'on') - self.assertEqual(f.read(3), 'e\nt') - self.assertEqual(f.read(10), 'wo\nthree\n') - - shell.push(['one\n', 'two\n']) - self.assertEqual(f.read(0), '') - self.assertRaises(TypeError, f.read, 1.5) - self.assertRaises(TypeError, f.read, '1') - self.assertRaises(TypeError, f.read, 1, 1) - - def test_readline(self): - shell = MockShell() - f = PseudoInputFile(shell, 'stdin', 'utf-8') - shell.push(['one\n', 'two\n', 'three\n', 'four\n']) - self.assertEqual(f.readline(), 'one\n') - self.assertEqual(f.readline(-1), 'two\n') - self.assertEqual(f.readline(None), 'three\n') - shell.push(['one\ntwo\n']) - self.assertEqual(f.readline(), 'one\n') - self.assertEqual(f.readline(), 'two\n') - shell.push(['one', 'two', 'three']) - self.assertEqual(f.readline(), 'one') - self.assertEqual(f.readline(), 'two') - shell.push(['one\n', 'two\n', 'three\n']) - self.assertEqual(f.readline(2), 'on') - self.assertEqual(f.readline(1), 'e') - self.assertEqual(f.readline(1), '\n') - self.assertEqual(f.readline(10), 'two\n') - - shell.push(['one\n', 'two\n']) - self.assertEqual(f.readline(0), '') - self.assertRaises(TypeError, f.readlines, 1.5) - self.assertRaises(TypeError, f.readlines, '1') - self.assertRaises(TypeError, f.readlines, 1, 1) - - def test_readlines(self): - shell = MockShell() - f = PseudoInputFile(shell, 'stdin', 'utf-8') - shell.push(['one\n', 'two\n', '']) - self.assertEqual(f.readlines(), ['one\n', 'two\n']) - shell.push(['one\n', 'two\n', '']) - self.assertEqual(f.readlines(-1), ['one\n', 'two\n']) - shell.push(['one\n', 'two\n', '']) - self.assertEqual(f.readlines(None), ['one\n', 'two\n']) - shell.push(['one\n', 'two\n', '']) - self.assertEqual(f.readlines(0), ['one\n', 'two\n']) - shell.push(['one\n', 'two\n', '']) - self.assertEqual(f.readlines(3), ['one\n']) - shell.push(['one\n', 'two\n', '']) - self.assertEqual(f.readlines(4), ['one\n', 'two\n']) - - shell.push(['one\n', 'two\n', '']) - self.assertRaises(TypeError, f.readlines, 1.5) - self.assertRaises(TypeError, f.readlines, '1') - self.assertRaises(TypeError, f.readlines, 1, 1) - - def test_close(self): - shell = MockShell() - f = PseudoInputFile(shell, 'stdin', 'utf-8') - shell.push(['one\n', 'two\n', '']) - self.assertFalse(f.closed) - self.assertEqual(f.readline(), 'one\n') - f.close() - self.assertFalse(f.closed) - self.assertEqual(f.readline(), 'two\n') - self.assertRaises(TypeError, f.close, 1) - - -if __name__ == '__main__': - unittest.main() diff --git a/Lib/idlelib/idle_test/test_iomenu.py b/Lib/idlelib/idle_test/test_iomenu.py new file mode 100644 index 0000000..e0e3b98 --- /dev/null +++ b/Lib/idlelib/idle_test/test_iomenu.py @@ -0,0 +1,233 @@ +import unittest +import io +from idlelib.PyShell import PseudoInputFile, PseudoOutputFile + + +class S(str): + def __str__(self): + return '%s:str' % type(self).__name__ + def __unicode__(self): + return '%s:unicode' % type(self).__name__ + def __len__(self): + return 3 + def __iter__(self): + return iter('abc') + def __getitem__(self, *args): + return '%s:item' % type(self).__name__ + def __getslice__(self, *args): + return '%s:slice' % type(self).__name__ + +class MockShell: + def __init__(self): + self.reset() + + def write(self, *args): + self.written.append(args) + + def readline(self): + return self.lines.pop() + + def close(self): + pass + + def reset(self): + self.written = [] + + def push(self, lines): + self.lines = list(lines)[::-1] + + +class PseudeOutputFilesTest(unittest.TestCase): + def test_misc(self): + shell = MockShell() + f = PseudoOutputFile(shell, 'stdout', 'utf-8') + self.assertIsInstance(f, io.TextIOBase) + self.assertEqual(f.encoding, 'utf-8') + self.assertIsNone(f.errors) + self.assertIsNone(f.newlines) + self.assertEqual(f.name, '') + self.assertFalse(f.closed) + self.assertTrue(f.isatty()) + self.assertFalse(f.readable()) + self.assertTrue(f.writable()) + self.assertFalse(f.seekable()) + + def test_unsupported(self): + shell = MockShell() + f = PseudoOutputFile(shell, 'stdout', 'utf-8') + self.assertRaises(OSError, f.fileno) + self.assertRaises(OSError, f.tell) + self.assertRaises(OSError, f.seek, 0) + self.assertRaises(OSError, f.read, 0) + self.assertRaises(OSError, f.readline, 0) + + def test_write(self): + shell = MockShell() + f = PseudoOutputFile(shell, 'stdout', 'utf-8') + f.write('test') + self.assertEqual(shell.written, [('test', 'stdout')]) + shell.reset() + f.write('t\xe8st') + self.assertEqual(shell.written, [('t\xe8st', 'stdout')]) + shell.reset() + + f.write(S('t\xe8st')) + self.assertEqual(shell.written, [('t\xe8st', 'stdout')]) + self.assertEqual(type(shell.written[0][0]), str) + shell.reset() + + self.assertRaises(TypeError, f.write) + self.assertEqual(shell.written, []) + self.assertRaises(TypeError, f.write, b'test') + self.assertRaises(TypeError, f.write, 123) + self.assertEqual(shell.written, []) + self.assertRaises(TypeError, f.write, 'test', 'spam') + self.assertEqual(shell.written, []) + + def test_writelines(self): + shell = MockShell() + f = PseudoOutputFile(shell, 'stdout', 'utf-8') + f.writelines([]) + self.assertEqual(shell.written, []) + shell.reset() + f.writelines(['one\n', 'two']) + self.assertEqual(shell.written, + [('one\n', 'stdout'), ('two', 'stdout')]) + shell.reset() + f.writelines(['on\xe8\n', 'tw\xf2']) + self.assertEqual(shell.written, + [('on\xe8\n', 'stdout'), ('tw\xf2', 'stdout')]) + shell.reset() + + f.writelines([S('t\xe8st')]) + self.assertEqual(shell.written, [('t\xe8st', 'stdout')]) + self.assertEqual(type(shell.written[0][0]), str) + shell.reset() + + self.assertRaises(TypeError, f.writelines) + self.assertEqual(shell.written, []) + self.assertRaises(TypeError, f.writelines, 123) + self.assertEqual(shell.written, []) + self.assertRaises(TypeError, f.writelines, [b'test']) + self.assertRaises(TypeError, f.writelines, [123]) + self.assertEqual(shell.written, []) + self.assertRaises(TypeError, f.writelines, [], []) + self.assertEqual(shell.written, []) + + def test_close(self): + shell = MockShell() + f = PseudoOutputFile(shell, 'stdout', 'utf-8') + self.assertFalse(f.closed) + f.write('test') + f.close() + self.assertTrue(f.closed) + self.assertRaises(ValueError, f.write, 'x') + self.assertEqual(shell.written, [('test', 'stdout')]) + f.close() + self.assertRaises(TypeError, f.close, 1) + + +class PseudeInputFilesTest(unittest.TestCase): + def test_misc(self): + shell = MockShell() + f = PseudoInputFile(shell, 'stdin', 'utf-8') + self.assertIsInstance(f, io.TextIOBase) + self.assertEqual(f.encoding, 'utf-8') + self.assertIsNone(f.errors) + self.assertIsNone(f.newlines) + self.assertEqual(f.name, '') + self.assertFalse(f.closed) + self.assertTrue(f.isatty()) + self.assertTrue(f.readable()) + self.assertFalse(f.writable()) + self.assertFalse(f.seekable()) + + def test_unsupported(self): + shell = MockShell() + f = PseudoInputFile(shell, 'stdin', 'utf-8') + self.assertRaises(OSError, f.fileno) + self.assertRaises(OSError, f.tell) + self.assertRaises(OSError, f.seek, 0) + self.assertRaises(OSError, f.write, 'x') + self.assertRaises(OSError, f.writelines, ['x']) + + def test_read(self): + shell = MockShell() + f = PseudoInputFile(shell, 'stdin', 'utf-8') + shell.push(['one\n', 'two\n', '']) + self.assertEqual(f.read(), 'one\ntwo\n') + shell.push(['one\n', 'two\n', '']) + self.assertEqual(f.read(-1), 'one\ntwo\n') + shell.push(['one\n', 'two\n', '']) + self.assertEqual(f.read(None), 'one\ntwo\n') + shell.push(['one\n', 'two\n', 'three\n', '']) + self.assertEqual(f.read(2), 'on') + self.assertEqual(f.read(3), 'e\nt') + self.assertEqual(f.read(10), 'wo\nthree\n') + + shell.push(['one\n', 'two\n']) + self.assertEqual(f.read(0), '') + self.assertRaises(TypeError, f.read, 1.5) + self.assertRaises(TypeError, f.read, '1') + self.assertRaises(TypeError, f.read, 1, 1) + + def test_readline(self): + shell = MockShell() + f = PseudoInputFile(shell, 'stdin', 'utf-8') + shell.push(['one\n', 'two\n', 'three\n', 'four\n']) + self.assertEqual(f.readline(), 'one\n') + self.assertEqual(f.readline(-1), 'two\n') + self.assertEqual(f.readline(None), 'three\n') + shell.push(['one\ntwo\n']) + self.assertEqual(f.readline(), 'one\n') + self.assertEqual(f.readline(), 'two\n') + shell.push(['one', 'two', 'three']) + self.assertEqual(f.readline(), 'one') + self.assertEqual(f.readline(), 'two') + shell.push(['one\n', 'two\n', 'three\n']) + self.assertEqual(f.readline(2), 'on') + self.assertEqual(f.readline(1), 'e') + self.assertEqual(f.readline(1), '\n') + self.assertEqual(f.readline(10), 'two\n') + + shell.push(['one\n', 'two\n']) + self.assertEqual(f.readline(0), '') + self.assertRaises(TypeError, f.readlines, 1.5) + self.assertRaises(TypeError, f.readlines, '1') + self.assertRaises(TypeError, f.readlines, 1, 1) + + def test_readlines(self): + shell = MockShell() + f = PseudoInputFile(shell, 'stdin', 'utf-8') + shell.push(['one\n', 'two\n', '']) + self.assertEqual(f.readlines(), ['one\n', 'two\n']) + shell.push(['one\n', 'two\n', '']) + self.assertEqual(f.readlines(-1), ['one\n', 'two\n']) + shell.push(['one\n', 'two\n', '']) + self.assertEqual(f.readlines(None), ['one\n', 'two\n']) + shell.push(['one\n', 'two\n', '']) + self.assertEqual(f.readlines(0), ['one\n', 'two\n']) + shell.push(['one\n', 'two\n', '']) + self.assertEqual(f.readlines(3), ['one\n']) + shell.push(['one\n', 'two\n', '']) + self.assertEqual(f.readlines(4), ['one\n', 'two\n']) + + shell.push(['one\n', 'two\n', '']) + self.assertRaises(TypeError, f.readlines, 1.5) + self.assertRaises(TypeError, f.readlines, '1') + self.assertRaises(TypeError, f.readlines, 1, 1) + + def test_close(self): + shell = MockShell() + f = PseudoInputFile(shell, 'stdin', 'utf-8') + shell.push(['one\n', 'two\n', '']) + self.assertFalse(f.closed) + self.assertEqual(f.readline(), 'one\n') + f.close() + self.assertFalse(f.closed) + self.assertEqual(f.readline(), 'two\n') + self.assertRaises(TypeError, f.close, 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/idlelib/idle_test/test_paragraph.py b/Lib/idlelib/idle_test/test_paragraph.py new file mode 100644 index 0000000..f6039e6 --- /dev/null +++ b/Lib/idlelib/idle_test/test_paragraph.py @@ -0,0 +1,377 @@ +# Test the functions and main class method of FormatParagraph.py +import unittest +from idlelib import FormatParagraph as fp +from idlelib.EditorWindow import EditorWindow +from tkinter import Tk, Text +from test.support import requires + + +class Is_Get_Test(unittest.TestCase): + """Test the is_ and get_ functions""" + test_comment = '# This is a comment' + test_nocomment = 'This is not a comment' + trailingws_comment = '# This is a comment ' + leadingws_comment = ' # This is a comment' + leadingws_nocomment = ' This is not a comment' + + def test_is_all_white(self): + 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(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(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(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 FormatParagraph. + + Using the runcase() function, find_paragraph() is called with 'mark' set at + multiple indexes before and inside the test paragraph. + + It appears that code with the same indentation as a quoted string is grouped + as part of the same paragraph, which is probably incorrect behavior. + """ + + @classmethod + def setUpClass(cls): + from idlelib.idle_test.mock_tk import Text + cls.text = Text() + + def runcase(self, inserttext, stopline, expected): + # Check that find_paragraph returns the expected paragraph when + # the mark index is set to beginning, middle, end of each line + # up to but not including the stop line + text = self.text + text.insert('1.0', inserttext) + for line in range(1, stopline): + linelength = int(text.index("%d.end" % line).split('.')[1]) + for col in (0, linelength//2, linelength): + tempindex = "%d.%d" % (line, col) + self.assertEqual(fp.find_paragraph(text, tempindex), expected) + text.delete('1.0', 'end') + + def test_find_comment(self): + comment = ( + "# Comment block with no blank lines before\n" + "# Comment line\n" + "\n") + self.runcase(comment, 3, ('1.0', '3.0', '#', comment[0:58])) + + comment = ( + "\n" + "# Comment block with whitespace line before and after\n" + "# Comment line\n" + "\n") + self.runcase(comment, 4, ('2.0', '4.0', '#', comment[1:70])) + + comment = ( + "\n" + " # Indented comment block with whitespace before and after\n" + " # Comment line\n" + "\n") + self.runcase(comment, 4, ('2.0', '4.0', ' #', comment[1:82])) + + comment = ( + "\n" + "# Single line comment\n" + "\n") + self.runcase(comment, 3, ('2.0', '3.0', '#', comment[1:23])) + + comment = ( + "\n" + " # Single line comment with leading whitespace\n" + "\n") + self.runcase(comment, 3, ('2.0', '3.0', ' #', comment[1:51])) + + comment = ( + "\n" + "# Comment immediately followed by code\n" + "x = 42\n" + "\n") + self.runcase(comment, 3, ('2.0', '3.0', '#', comment[1:40])) + + comment = ( + "\n" + " # Indented comment immediately followed by code\n" + "x = 42\n" + "\n") + self.runcase(comment, 3, ('2.0', '3.0', ' #', comment[1:53])) + + comment = ( + "\n" + "# Comment immediately followed by indented code\n" + " x = 42\n" + "\n") + self.runcase(comment, 3, ('2.0', '3.0', '#', comment[1:49])) + + def test_find_paragraph(self): + teststring = ( + '"""String with no blank lines before\n' + 'String line\n' + '"""\n' + '\n') + self.runcase(teststring, 4, ('1.0', '4.0', '', teststring[0:53])) + + teststring = ( + "\n" + '"""String with whitespace line before and after\n' + 'String line.\n' + '"""\n' + '\n') + self.runcase(teststring, 5, ('2.0', '5.0', '', teststring[1:66])) + + teststring = ( + '\n' + ' """Indented string with whitespace before and after\n' + ' Comment string.\n' + ' """\n' + '\n') + self.runcase(teststring, 5, ('2.0', '5.0', ' ', teststring[1:85])) + + teststring = ( + '\n' + '"""Single line string."""\n' + '\n') + self.runcase(teststring, 3, ('2.0', '3.0', '', teststring[1:27])) + + teststring = ( + '\n' + ' """Single line string with leading whitespace."""\n' + '\n') + self.runcase(teststring, 3, ('2.0', '3.0', ' ', teststring[1:55])) + + +class ReformatFunctionTest(unittest.TestCase): + """Test the reformat_paragraph function without the editor window.""" + + def test_reformat_paragrah(self): + Equal = self.assertEqual + reform = fp.reformat_paragraph + hw = "O hello world" + Equal(reform(' ', 1), ' ') + Equal(reform("Hello world", 20), "Hello world") + + # Test without leading newline + Equal(reform(hw, 1), "O\nhello\nworld") + Equal(reform(hw, 6), "O\nhello\nworld") + Equal(reform(hw, 7), "O hello\nworld") + Equal(reform(hw, 12), "O hello\nworld") + Equal(reform(hw, 13), "O hello world") + + # Test with leading newline + hw = "\nO hello world" + Equal(reform(hw, 1), "\nO\nhello\nworld") + Equal(reform(hw, 6), "\nO\nhello\nworld") + Equal(reform(hw, 7), "\nO hello\nworld") + Equal(reform(hw, 12), "\nO hello\nworld") + Equal(reform(hw, 13), "\nO hello world") + + +class ReformatCommentTest(unittest.TestCase): + """Test the reformat_comment function without the editor window.""" + + def test_reformat_comment(self): + Equal = self.assertEqual + + # reformat_comment formats to a minimum of 20 characters + 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 = 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?\"\"\"") + Equal(result, expected) + + 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 = 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?") + Equal(result, expected) + + +class FormatClassTest(unittest.TestCase): + def test_init_close(self): + instance = fp.FormatParagraph('editor') + self.assertEqual(instance.editwin, 'editor') + instance.close() + self.assertEqual(instance.editwin, None) + + +# For testing format_paragraph_event, Initialize FormatParagraph with +# a mock Editor with .text and .get_selection_indices. The text must +# be a Text wrapper that adds two methods + +# A real EditorWindow creates unneeded, time-consuming baggage and +# sometimes emits shutdown warnings like this: +# "warning: callback failed in WindowList +# : invalid command name ".55131368.windows". +# Calling EditorWindow._close in tearDownClass prevents this but causes +# other problems (windows left open). + +class TextWrapper: + def __init__(self, master): + self.text = Text(master=master) + def __getattr__(self, name): + return getattr(self.text, name) + def undo_block_start(self): pass + def undo_block_stop(self): pass + +class Editor: + def __init__(self, root): + self.text = TextWrapper(root) + get_selection_indices = EditorWindow. get_selection_indices + +class FormatEventTest(unittest.TestCase): + """Test the formatting of text inside a Text widget. + + This is done with FormatParagraph.format.paragraph_event, + which calls functions in the module as appropriate. + """ + test_string = ( + " '''this is a test of a reformat for a triple " + "quoted string will it reformat to less than 70 " + "characters for me?'''\n") + multiline_test_string = ( + " '''The first line is under the max width.\n" + " The second line's length is way over the max width. It goes " + "on and on until it is over 100 characters long.\n" + " Same thing with the third line. It is also way over the max " + "width, but FormatParagraph will fix it.\n" + " '''\n") + multiline_test_comment = ( + "# The first line is under the max width.\n" + "# The second line's length is way over the max width. It goes on " + "and on until it is over 100 characters long.\n" + "# Same thing with the third line. It is also way over the max " + "width, but FormatParagraph will fix it.\n" + "# The fourth line is short like the first line.") + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + editor = Editor(root=cls.root) + cls.text = editor.text.text # Test code does not need the wrapper. + cls.formatter = fp.FormatParagraph(editor).format_paragraph_event + # Sets the insert mark just after the re-wrapped and inserted text. + + @classmethod + def tearDownClass(cls): + cls.root.destroy() + del cls.root + del cls.text + del cls.formatter + + def test_short_line(self): + self.text.insert('1.0', "Short line\n") + self.formatter("Dummy") + self.assertEqual(self.text.get('1.0', 'insert'), "Short line\n" ) + self.text.delete('1.0', 'end') + + def test_long_line(self): + text = self.text + + # Set cursor ('insert' mark) to '1.0', within text. + text.insert('1.0', self.test_string) + text.mark_set('insert', '1.0') + self.formatter('ParameterDoesNothing', limit=70) + result = text.get('1.0', 'insert') + # find function includes \n + expected = ( +" '''this is a test of a reformat for a triple quoted string will it\n" +" reformat to less than 70 characters for me?'''\n") # yes + self.assertEqual(result, expected) + text.delete('1.0', 'end') + + # Select from 1.11 to line end. + text.insert('1.0', self.test_string) + text.tag_add('sel', '1.11', '1.end') + self.formatter('ParameterDoesNothing', limit=70) + result = text.get('1.0', 'insert') + # selection excludes \n + expected = ( +" '''this is a test of a reformat for a triple quoted string will it reformat\n" +" to less than 70 characters for me?'''") # no + self.assertEqual(result, expected) + text.delete('1.0', 'end') + + def test_multiple_lines(self): + text = self.text + # Select 2 long lines. + text.insert('1.0', self.multiline_test_string) + text.tag_add('sel', '2.0', '4.0') + self.formatter('ParameterDoesNothing', limit=70) + result = text.get('2.0', 'insert') + expected = ( +" The second line's length is way over the max width. It goes on and\n" +" on until it is over 100 characters long. Same thing with the third\n" +" line. It is also way over the max width, but FormatParagraph will\n" +" fix it.\n") + self.assertEqual(result, expected) + text.delete('1.0', 'end') + + def test_comment_block(self): + text = self.text + + # Set cursor ('insert') to '1.0', within block. + text.insert('1.0', self.multiline_test_comment) + self.formatter('ParameterDoesNothing', limit=70) + result = text.get('1.0', 'insert') + expected = ( +"# The first line is under the max width. The second line's length is\n" +"# way over the max width. It goes on and on until it is over 100\n" +"# characters long. Same thing with the third line. It is also way over\n" +"# the max width, but FormatParagraph will fix it. The fourth line is\n" +"# short like the first line.\n") + self.assertEqual(result, expected) + text.delete('1.0', 'end') + + # Select line 2, verify line 1 unaffected. + text.insert('1.0', self.multiline_test_comment) + text.tag_add('sel', '2.0', '3.0') + self.formatter('ParameterDoesNothing', limit=70) + result = text.get('1.0', 'insert') + expected = ( +"# The first line is under the max width.\n" +"# The second line's length is way over the max width. It goes on and\n" +"# on until it is over 100 characters long.\n") + self.assertEqual(result, expected) + text.delete('1.0', 'end') + +# The following block worked with EditorWindow but fails with the mock. +# Lines 2 and 3 get pasted together even though the previous block left +# the previous line alone. More investigation is needed. +## # Select lines 3 and 4 +## text.insert('1.0', self.multiline_test_comment) +## text.tag_add('sel', '3.0', '5.0') +## self.formatter('ParameterDoesNothing') +## result = text.get('3.0', 'insert') +## expected = ( +##"# Same thing with the third line. It is also way over the max width,\n" +##"# but FormatParagraph will fix it. The fourth line is short like the\n" +##"# first line.\n") +## self.assertEqual(result, expected) +## text.delete('1.0', 'end') + + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=2) diff --git a/Lib/idlelib/idle_test/test_redirector.py b/Lib/idlelib/idle_test/test_redirector.py new file mode 100644 index 0000000..6440561 --- /dev/null +++ b/Lib/idlelib/idle_test/test_redirector.py @@ -0,0 +1,122 @@ +"""Unittest for idlelib.WidgetRedirector + +100% coverage +""" +from test.support import requires +import unittest +from idlelib.idle_test.mock_idle import Func +from tkinter import Tk, Text, TclError +from idlelib.WidgetRedirector import WidgetRedirector + + +class InitCloseTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.tk = Tk() + cls.text = Text(cls.tk) + + @classmethod + def tearDownClass(cls): + cls.text.destroy() + cls.tk.destroy() + del cls.text, cls.tk + + def test_init(self): + redir = WidgetRedirector(self.text) + self.assertEqual(redir.widget, self.text) + self.assertEqual(redir.tk, self.text.tk) + self.assertRaises(TclError, WidgetRedirector, self.text) + redir.close() # restore self.tk, self.text + + def test_close(self): + redir = WidgetRedirector(self.text) + redir.register('insert', Func) + redir.close() + self.assertEqual(redir._operations, {}) + self.assertFalse(hasattr(self.text, 'widget')) + + +class WidgetRedirectorTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.tk = Tk() + cls.text = Text(cls.tk) + + @classmethod + def tearDownClass(cls): + cls.text.destroy() + cls.tk.destroy() + del cls.text, cls.tk + + def setUp(self): + self.redir = WidgetRedirector(self.text) + self.func = Func() + self.orig_insert = self.redir.register('insert', self.func) + self.text.insert('insert', 'asdf') # leaves self.text empty + + def tearDown(self): + self.text.delete('1.0', 'end') + self.redir.close() + + def test_repr(self): # partly for 100% coverage + self.assertIn('Redirector', repr(self.redir)) + self.assertIn('Original', repr(self.orig_insert)) + + def test_register(self): + self.assertEqual(self.text.get('1.0', 'end'), '\n') + self.assertEqual(self.func.args, ('insert', 'asdf')) + self.assertIn('insert', self.redir._operations) + self.assertIn('insert', self.text.__dict__) + self.assertEqual(self.text.insert, self.func) + + def test_original_command(self): + self.assertEqual(self.orig_insert.operation, 'insert') + self.assertEqual(self.orig_insert.tk_call, self.text.tk.call) + self.orig_insert('insert', 'asdf') + self.assertEqual(self.text.get('1.0', 'end'), 'asdf\n') + + def test_unregister(self): + self.assertIsNone(self.redir.unregister('invalid operation name')) + self.assertEqual(self.redir.unregister('insert'), self.func) + self.assertNotIn('insert', self.redir._operations) + self.assertNotIn('insert', self.text.__dict__) + + def test_unregister_no_attribute(self): + del self.text.insert + self.assertEqual(self.redir.unregister('insert'), self.func) + + def test_dispatch_intercept(self): + self.func.__init__(True) + self.assertTrue(self.redir.dispatch('insert', False)) + self.assertFalse(self.func.args[0]) + + def test_dispatch_bypass(self): + self.orig_insert('insert', 'asdf') + # tk.call returns '' where Python would return None + self.assertEqual(self.redir.dispatch('delete', '1.0', 'end'), '') + self.assertEqual(self.text.get('1.0', 'end'), '\n') + + def test_dispatch_error(self): + self.func.__init__(TclError()) + self.assertEqual(self.redir.dispatch('insert', False), '') + self.assertEqual(self.redir.dispatch('invalid'), '') + + def test_command_dispatch(self): + # Test that .__init__ causes redirection of tk calls + # through redir.dispatch + self.tk.call(self.text._w, 'insert', 'hello') + self.assertEqual(self.func.args, ('hello',)) + self.assertEqual(self.text.get('1.0', 'end'), '\n') + # Ensure that called through redir .dispatch and not through + # self.text.insert by having mock raise TclError. + self.func.__init__(TclError()) + self.assertEqual(self.tk.call(self.text._w, 'insert', 'boo'), '') + + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_replace.py b/Lib/idlelib/idle_test/test_replace.py new file mode 100644 index 0000000..09669f8 --- /dev/null +++ b/Lib/idlelib/idle_test/test_replace.py @@ -0,0 +1,292 @@ +"""Unittest for idlelib.ReplaceDialog""" +from test.support import requires +requires('gui') + +import unittest +from unittest.mock import Mock +from tkinter import Tk, Text +from idlelib.idle_test.mock_tk import Mbox +import idlelib.SearchEngine as se +import idlelib.ReplaceDialog as rd + +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 = rd.ReplaceDialog(cls.root, cls.engine) + 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 + cls.root.destroy() + del cls.text, cls.dialog, cls.engine, 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') + self.dialog.open(self.text) + replace() + equal(text.get('1.8', '1.12'), 'asdf') + + # dont "match word" case + text.mark_set('insert', '1.0') + pv.set('is') + rv.set('hello') + replace() + equal(text.get('1.2', '1.7'), 'hello') + + # dont "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('[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('[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('[\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_replacedialog.py b/Lib/idlelib/idle_test/test_replacedialog.py deleted file mode 100644 index 09669f8..0000000 --- a/Lib/idlelib/idle_test/test_replacedialog.py +++ /dev/null @@ -1,292 +0,0 @@ -"""Unittest for idlelib.ReplaceDialog""" -from test.support import requires -requires('gui') - -import unittest -from unittest.mock import Mock -from tkinter import Tk, Text -from idlelib.idle_test.mock_tk import Mbox -import idlelib.SearchEngine as se -import idlelib.ReplaceDialog as rd - -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 = rd.ReplaceDialog(cls.root, cls.engine) - 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 - cls.root.destroy() - del cls.text, cls.dialog, cls.engine, 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') - self.dialog.open(self.text) - replace() - equal(text.get('1.8', '1.12'), 'asdf') - - # dont "match word" case - text.mark_set('insert', '1.0') - pv.set('is') - rv.set('hello') - replace() - equal(text.get('1.2', '1.7'), 'hello') - - # dont "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('[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('[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('[\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_search.py b/Lib/idlelib/idle_test/test_search.py new file mode 100644 index 0000000..190c866 --- /dev/null +++ b/Lib/idlelib/idle_test/test_search.py @@ -0,0 +1,80 @@ +"""Test SearchDialog class in SearchDialogue.py""" + +# Does not currently test the event handler wrappers. +# A usage test should simulate clicks and check hilighting. +# Tests need to be coordinated with SearchDialogBase tests +# to avoid duplication. + +from test.support import requires +requires('gui') + +import unittest +import tkinter as tk +from tkinter import BooleanVar +import idlelib.SearchEngine as se +import idlelib.SearchDialog as sd + + +class SearchDialogTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.root = tk.Tk() + + @classmethod + def tearDownClass(cls): + cls.root.destroy() + del cls.root + + def setUp(self): + self.engine = se.SearchEngine(self.root) + self.dialog = sd.SearchDialog(self.root, self.engine) + self.text = tk.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.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_searchbase.py new file mode 100644 index 0000000..8036b91 --- /dev/null +++ b/Lib/idlelib/idle_test/test_searchbase.py @@ -0,0 +1,165 @@ +'''Unittests for idlelib/SearchDialogBase.py + +Coverage: 99%. The only thing not covered is inconsequential -- +testing skipping of suite when self.needwrapbutton is false. + +''' +import unittest +from test.support import requires +from tkinter import Tk, Toplevel, Frame ##, BooleanVar, StringVar +from idlelib import SearchEngine as se +from idlelib import SearchDialogBase as sdb +from idlelib.idle_test.mock_idle import Func +## from idlelib.idle_test.mock_tk import Var + +# The ## imports above & following could help make some tests gui-free. +# However, they currently make radiobutton tests fail. +##def setUpModule(): +## # Replace tk objects used to initialize se.SearchEngine. +## se.BooleanVar = Var +## se.StringVar = Var +## +##def tearDownModule(): +## se.BooleanVar = BooleanVar +## se.StringVar = StringVar + +class SearchDialogBaseTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + + @classmethod + def tearDownClass(cls): + cls.root.destroy() + del cls.root + + def setUp(self): + self.engine = se.SearchEngine(self.root) # None also seems to work + self.dialog = sdb.SearchDialogBase(root=self.root, engine=self.engine) + + def tearDown(self): + self.dialog.close() + + def test_open_and_close(self): + # open calls create_widgets, which needs default_command + self.dialog.default_command = None + + # Since text parameter of .open is not used in base class, + # pass dummy 'text' instead of tk.Text(). + self.dialog.open('text') + self.assertEqual(self.dialog.top.state(), 'normal') + self.dialog.close() + self.assertEqual(self.dialog.top.state(), 'withdrawn') + + self.dialog.open('text', searchphrase="hello") + self.assertEqual(self.dialog.ent.get(), 'hello') + self.dialog.close() + + def test_create_widgets(self): + self.dialog.create_entries = Func() + self.dialog.create_option_buttons = Func() + self.dialog.create_other_buttons = Func() + self.dialog.create_command_buttons = Func() + + self.dialog.default_command = None + self.dialog.create_widgets() + + self.assertTrue(self.dialog.create_entries.called) + self.assertTrue(self.dialog.create_option_buttons.called) + self.assertTrue(self.dialog.create_other_buttons.called) + self.assertTrue(self.dialog.create_command_buttons.called) + + def test_make_entry(self): + equal = self.assertEqual + self.dialog.row = 0 + self.dialog.top = Toplevel(self.root) + entry, label = self.dialog.make_entry("Test:", 'hello') + equal(label['text'], 'Test:') + + self.assertIn(entry.get(), 'hello') + egi = entry.grid_info() + equal(int(egi['row']), 0) + equal(int(egi['column']), 1) + equal(int(egi['rowspan']), 1) + equal(int(egi['columnspan']), 1) + equal(self.dialog.row, 1) + + def test_create_entries(self): + self.dialog.row = 0 + self.engine.setpat('hello') + self.dialog.create_entries() + self.assertIn(self.dialog.ent.get(), 'hello') + + def test_make_frame(self): + self.dialog.row = 0 + self.dialog.top = Toplevel(self.root) + frame, label = self.dialog.make_frame() + self.assertEqual(label, '') + self.assertIsInstance(frame, Frame) + + frame, label = self.dialog.make_frame('testlabel') + self.assertEqual(label['text'], 'testlabel') + self.assertIsInstance(frame, Frame) + + def btn_test_setup(self, meth): + self.dialog.top = Toplevel(self.root) + self.dialog.row = 0 + return meth() + + def test_create_option_buttons(self): + e = self.engine + for state in (0, 1): + for var in (e.revar, e.casevar, e.wordvar, e.wrapvar): + var.set(state) + frame, options = self.btn_test_setup( + self.dialog.create_option_buttons) + for spec, button in zip (options, frame.pack_slaves()): + var, label = spec + self.assertEqual(button['text'], label) + self.assertEqual(var.get(), state) + if state == 1: + button.deselect() + else: + button.select() + self.assertEqual(var.get(), 1 - state) + + def test_create_other_buttons(self): + for state in (False, True): + var = self.engine.backvar + var.set(state) + frame, others = self.btn_test_setup( + self.dialog.create_other_buttons) + buttons = frame.pack_slaves() + for spec, button in zip(others, buttons): + val, label = spec + self.assertEqual(button['text'], label) + if val == state: + # hit other button, then this one + # indexes depend on button order + self.assertEqual(var.get(), state) + buttons[val].select() + self.assertEqual(var.get(), 1 - state) + buttons[1-val].select() + self.assertEqual(var.get(), state) + + def test_make_button(self): + self.dialog.top = Toplevel(self.root) + self.dialog.buttonframe = Frame(self.dialog.top) + btn = self.dialog.make_button('Test', self.dialog.close) + self.assertEqual(btn['text'], 'Test') + + def test_create_command_buttons(self): + self.dialog.create_command_buttons() + # Look for close button command in buttonframe + closebuttoncommand = '' + for child in self.dialog.buttonframe.winfo_children(): + if child['text'] == 'close': + closebuttoncommand = child['command'] + self.assertIn('close', closebuttoncommand) + + + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=2) diff --git a/Lib/idlelib/idle_test/test_searchdialog.py b/Lib/idlelib/idle_test/test_searchdialog.py deleted file mode 100644 index 190c866..0000000 --- a/Lib/idlelib/idle_test/test_searchdialog.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Test SearchDialog class in SearchDialogue.py""" - -# Does not currently test the event handler wrappers. -# A usage test should simulate clicks and check hilighting. -# Tests need to be coordinated with SearchDialogBase tests -# to avoid duplication. - -from test.support import requires -requires('gui') - -import unittest -import tkinter as tk -from tkinter import BooleanVar -import idlelib.SearchEngine as se -import idlelib.SearchDialog as sd - - -class SearchDialogTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.root = tk.Tk() - - @classmethod - def tearDownClass(cls): - cls.root.destroy() - del cls.root - - def setUp(self): - self.engine = se.SearchEngine(self.root) - self.dialog = sd.SearchDialog(self.root, self.engine) - self.text = tk.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.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_searchdialogbase.py b/Lib/idlelib/idle_test/test_searchdialogbase.py deleted file mode 100644 index 8036b91..0000000 --- a/Lib/idlelib/idle_test/test_searchdialogbase.py +++ /dev/null @@ -1,165 +0,0 @@ -'''Unittests for idlelib/SearchDialogBase.py - -Coverage: 99%. The only thing not covered is inconsequential -- -testing skipping of suite when self.needwrapbutton is false. - -''' -import unittest -from test.support import requires -from tkinter import Tk, Toplevel, Frame ##, BooleanVar, StringVar -from idlelib import SearchEngine as se -from idlelib import SearchDialogBase as sdb -from idlelib.idle_test.mock_idle import Func -## from idlelib.idle_test.mock_tk import Var - -# The ## imports above & following could help make some tests gui-free. -# However, they currently make radiobutton tests fail. -##def setUpModule(): -## # Replace tk objects used to initialize se.SearchEngine. -## se.BooleanVar = Var -## se.StringVar = Var -## -##def tearDownModule(): -## se.BooleanVar = BooleanVar -## se.StringVar = StringVar - -class SearchDialogBaseTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.root = Tk() - - @classmethod - def tearDownClass(cls): - cls.root.destroy() - del cls.root - - def setUp(self): - self.engine = se.SearchEngine(self.root) # None also seems to work - self.dialog = sdb.SearchDialogBase(root=self.root, engine=self.engine) - - def tearDown(self): - self.dialog.close() - - def test_open_and_close(self): - # open calls create_widgets, which needs default_command - self.dialog.default_command = None - - # Since text parameter of .open is not used in base class, - # pass dummy 'text' instead of tk.Text(). - self.dialog.open('text') - self.assertEqual(self.dialog.top.state(), 'normal') - self.dialog.close() - self.assertEqual(self.dialog.top.state(), 'withdrawn') - - self.dialog.open('text', searchphrase="hello") - self.assertEqual(self.dialog.ent.get(), 'hello') - self.dialog.close() - - def test_create_widgets(self): - self.dialog.create_entries = Func() - self.dialog.create_option_buttons = Func() - self.dialog.create_other_buttons = Func() - self.dialog.create_command_buttons = Func() - - self.dialog.default_command = None - self.dialog.create_widgets() - - self.assertTrue(self.dialog.create_entries.called) - self.assertTrue(self.dialog.create_option_buttons.called) - self.assertTrue(self.dialog.create_other_buttons.called) - self.assertTrue(self.dialog.create_command_buttons.called) - - def test_make_entry(self): - equal = self.assertEqual - self.dialog.row = 0 - self.dialog.top = Toplevel(self.root) - entry, label = self.dialog.make_entry("Test:", 'hello') - equal(label['text'], 'Test:') - - self.assertIn(entry.get(), 'hello') - egi = entry.grid_info() - equal(int(egi['row']), 0) - equal(int(egi['column']), 1) - equal(int(egi['rowspan']), 1) - equal(int(egi['columnspan']), 1) - equal(self.dialog.row, 1) - - def test_create_entries(self): - self.dialog.row = 0 - self.engine.setpat('hello') - self.dialog.create_entries() - self.assertIn(self.dialog.ent.get(), 'hello') - - def test_make_frame(self): - self.dialog.row = 0 - self.dialog.top = Toplevel(self.root) - frame, label = self.dialog.make_frame() - self.assertEqual(label, '') - self.assertIsInstance(frame, Frame) - - frame, label = self.dialog.make_frame('testlabel') - self.assertEqual(label['text'], 'testlabel') - self.assertIsInstance(frame, Frame) - - def btn_test_setup(self, meth): - self.dialog.top = Toplevel(self.root) - self.dialog.row = 0 - return meth() - - def test_create_option_buttons(self): - e = self.engine - for state in (0, 1): - for var in (e.revar, e.casevar, e.wordvar, e.wrapvar): - var.set(state) - frame, options = self.btn_test_setup( - self.dialog.create_option_buttons) - for spec, button in zip (options, frame.pack_slaves()): - var, label = spec - self.assertEqual(button['text'], label) - self.assertEqual(var.get(), state) - if state == 1: - button.deselect() - else: - button.select() - self.assertEqual(var.get(), 1 - state) - - def test_create_other_buttons(self): - for state in (False, True): - var = self.engine.backvar - var.set(state) - frame, others = self.btn_test_setup( - self.dialog.create_other_buttons) - buttons = frame.pack_slaves() - for spec, button in zip(others, buttons): - val, label = spec - self.assertEqual(button['text'], label) - if val == state: - # hit other button, then this one - # indexes depend on button order - self.assertEqual(var.get(), state) - buttons[val].select() - self.assertEqual(var.get(), 1 - state) - buttons[1-val].select() - self.assertEqual(var.get(), state) - - def test_make_button(self): - self.dialog.top = Toplevel(self.root) - self.dialog.buttonframe = Frame(self.dialog.top) - btn = self.dialog.make_button('Test', self.dialog.close) - self.assertEqual(btn['text'], 'Test') - - def test_create_command_buttons(self): - self.dialog.create_command_buttons() - # Look for close button command in buttonframe - closebuttoncommand = '' - for child in self.dialog.buttonframe.winfo_children(): - if child['text'] == 'close': - closebuttoncommand = child['command'] - self.assertIn('close', closebuttoncommand) - - - -if __name__ == '__main__': - unittest.main(verbosity=2, exit=2) diff --git a/Lib/idlelib/idle_test/test_undo.py b/Lib/idlelib/idle_test/test_undo.py new file mode 100644 index 0000000..2657984 --- /dev/null +++ b/Lib/idlelib/idle_test/test_undo.py @@ -0,0 +1,134 @@ +"""Unittest for UndoDelegator in idlelib.UndoDelegator. + +Coverage about 80% (retest). +""" +from test.support import requires +requires('gui') + +import unittest +from unittest.mock import Mock +from tkinter import Text, Tk +from idlelib.UndoDelegator import UndoDelegator +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() + cls.root.destroy() + del cls.percolator, cls.text, cls.root + + def setUp(self): + self.delegator = UndoDelegator() + self.percolator.insertfilter(self.delegator) + self.delegator.bell = Mock(wraps=self.delegator.bell) + + 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('<>') + 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('<>') + self.assertEqual(text.get('1.0', '1.4'), 'foar') + text.event_generate('<>') + self.assertEqual(text.get('1.0', '1.6'), 'foobar') + text.event_generate('<>') + self.assertEqual(text.get('1.0', '1.3'), 'foo') + text.event_generate('<>') + 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('<>') + text.event_generate('<>') + self.assertEqual(text.get('1.0', '1.3'), 'bar') + text.event_generate('<>') + 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('<>') + 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_undodelegator.py b/Lib/idlelib/idle_test/test_undodelegator.py deleted file mode 100644 index 2657984..0000000 --- a/Lib/idlelib/idle_test/test_undodelegator.py +++ /dev/null @@ -1,134 +0,0 @@ -"""Unittest for UndoDelegator in idlelib.UndoDelegator. - -Coverage about 80% (retest). -""" -from test.support import requires -requires('gui') - -import unittest -from unittest.mock import Mock -from tkinter import Text, Tk -from idlelib.UndoDelegator import UndoDelegator -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() - cls.root.destroy() - del cls.percolator, cls.text, cls.root - - def setUp(self): - self.delegator = UndoDelegator() - self.percolator.insertfilter(self.delegator) - self.delegator.bell = Mock(wraps=self.delegator.bell) - - 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('<>') - 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('<>') - self.assertEqual(text.get('1.0', '1.4'), 'foar') - text.event_generate('<>') - self.assertEqual(text.get('1.0', '1.6'), 'foobar') - text.event_generate('<>') - self.assertEqual(text.get('1.0', '1.3'), 'foo') - text.event_generate('<>') - 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('<>') - text.event_generate('<>') - self.assertEqual(text.get('1.0', '1.3'), 'bar') - text.event_generate('<>') - 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('<>') - 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_widgetredir.py b/Lib/idlelib/idle_test/test_widgetredir.py deleted file mode 100644 index 6440561..0000000 --- a/Lib/idlelib/idle_test/test_widgetredir.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Unittest for idlelib.WidgetRedirector - -100% coverage -""" -from test.support import requires -import unittest -from idlelib.idle_test.mock_idle import Func -from tkinter import Tk, Text, TclError -from idlelib.WidgetRedirector import WidgetRedirector - - -class InitCloseTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.tk = Tk() - cls.text = Text(cls.tk) - - @classmethod - def tearDownClass(cls): - cls.text.destroy() - cls.tk.destroy() - del cls.text, cls.tk - - def test_init(self): - redir = WidgetRedirector(self.text) - self.assertEqual(redir.widget, self.text) - self.assertEqual(redir.tk, self.text.tk) - self.assertRaises(TclError, WidgetRedirector, self.text) - redir.close() # restore self.tk, self.text - - def test_close(self): - redir = WidgetRedirector(self.text) - redir.register('insert', Func) - redir.close() - self.assertEqual(redir._operations, {}) - self.assertFalse(hasattr(self.text, 'widget')) - - -class WidgetRedirectorTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - requires('gui') - cls.tk = Tk() - cls.text = Text(cls.tk) - - @classmethod - def tearDownClass(cls): - cls.text.destroy() - cls.tk.destroy() - del cls.text, cls.tk - - def setUp(self): - self.redir = WidgetRedirector(self.text) - self.func = Func() - self.orig_insert = self.redir.register('insert', self.func) - self.text.insert('insert', 'asdf') # leaves self.text empty - - def tearDown(self): - self.text.delete('1.0', 'end') - self.redir.close() - - def test_repr(self): # partly for 100% coverage - self.assertIn('Redirector', repr(self.redir)) - self.assertIn('Original', repr(self.orig_insert)) - - def test_register(self): - self.assertEqual(self.text.get('1.0', 'end'), '\n') - self.assertEqual(self.func.args, ('insert', 'asdf')) - self.assertIn('insert', self.redir._operations) - self.assertIn('insert', self.text.__dict__) - self.assertEqual(self.text.insert, self.func) - - def test_original_command(self): - self.assertEqual(self.orig_insert.operation, 'insert') - self.assertEqual(self.orig_insert.tk_call, self.text.tk.call) - self.orig_insert('insert', 'asdf') - self.assertEqual(self.text.get('1.0', 'end'), 'asdf\n') - - def test_unregister(self): - self.assertIsNone(self.redir.unregister('invalid operation name')) - self.assertEqual(self.redir.unregister('insert'), self.func) - self.assertNotIn('insert', self.redir._operations) - self.assertNotIn('insert', self.text.__dict__) - - def test_unregister_no_attribute(self): - del self.text.insert - self.assertEqual(self.redir.unregister('insert'), self.func) - - def test_dispatch_intercept(self): - self.func.__init__(True) - self.assertTrue(self.redir.dispatch('insert', False)) - self.assertFalse(self.func.args[0]) - - def test_dispatch_bypass(self): - self.orig_insert('insert', 'asdf') - # tk.call returns '' where Python would return None - self.assertEqual(self.redir.dispatch('delete', '1.0', 'end'), '') - self.assertEqual(self.text.get('1.0', 'end'), '\n') - - def test_dispatch_error(self): - self.func.__init__(TclError()) - self.assertEqual(self.redir.dispatch('insert', False), '') - self.assertEqual(self.redir.dispatch('invalid'), '') - - def test_command_dispatch(self): - # Test that .__init__ causes redirection of tk calls - # through redir.dispatch - self.tk.call(self.text._w, 'insert', 'hello') - self.assertEqual(self.func.args, ('hello',)) - self.assertEqual(self.text.get('1.0', 'end'), '\n') - # Ensure that called through redir .dispatch and not through - # self.text.insert by having mock raise TclError. - self.func.__init__(TclError()) - self.assertEqual(self.tk.call(self.text._w, 'insert', 'boo'), '') - - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/iomenu.py b/Lib/idlelib/iomenu.py new file mode 100644 index 0000000..84f39a2 --- /dev/null +++ b/Lib/idlelib/iomenu.py @@ -0,0 +1,565 @@ +import codecs +from codecs import BOM_UTF8 +import os +import re +import shlex +import sys +import tempfile + +import tkinter.filedialog as tkFileDialog +import tkinter.messagebox as tkMessageBox +from tkinter.simpledialog import askstring + +from idlelib.configHandler import idleConf + + +# 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 + +# Encoding for file names +filesystemencoding = sys.getfilesystemencoding() ### currently unused + +locale_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: + locale_encoding = locale.getdefaultlocale()[1] + codecs.lookup(locale_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 + locale_encoding = locale.nl_langinfo(locale.CODESET) + if locale_encoding is None or locale_encoding is '': + # situation occurs on Mac OS X + locale_encoding = 'ascii' + 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 is None or locale_encoding is '': + # situation occurs on Mac OS X + locale_encoding = 'ascii' + codecs.lookup(locale_encoding) + except (ValueError, LookupError): + pass + +locale_encoding = locale_encoding.lower() + +encoding = locale_encoding ### KBK 07Sep07 This is used all over IDLE, check! + ### 'encoding' is used below in encode(), check! + +coding_re = re.compile(r'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)', re.ASCII) +blank_re = re.compile(r'^[ \t\f]*(?:[#\r\n]|$)', re.ASCII) + +def coding_spec(data): + """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. + """ + 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] + for line in lst: + match = coding_re.match(line) + if match is not None: + break + if not blank_re.match(line): + return None + else: + return None + name = match.group(1) + try: + codecs.lookup(name) + except LookupError: + # The standard encoding error does not indicate the encoding + raise LookupError("Unknown encoding: "+name) + return name + + +class IOBinding: + + def __init__(self, editwin): + self.editwin = editwin + self.text = editwin.text + self.__id_open = self.text.bind("<>", self.open) + self.__id_save = self.text.bind("<>", self.save) + self.__id_saveas = self.text.bind("<>", + self.save_as) + self.__id_savecopy = self.text.bind("<>", + self.save_a_copy) + self.fileencoding = None + self.__id_print = self.text.bind("<>", self.print_window) + + def close(self): + # Undo command bindings + self.text.unbind("<>", self.__id_open) + self.text.unbind("<>", self.__id_save) + self.text.unbind("<>",self.__id_saveas) + self.text.unbind("<>", self.__id_savecopy) + self.text.unbind("<>", self.__id_print) + # Break cycles + self.editwin = None + self.text = None + self.filename_change_hook = None + + def get_saved(self): + return self.editwin.get_saved() + + def set_saved(self, flag): + self.editwin.set_saved(flag) + + def reset_undo(self): + self.editwin.reset_undo() + + filename_change_hook = None + + def set_filename_change_hook(self, hook): + self.filename_change_hook = hook + + filename = None + dirname = None + + def set_filename(self, filename): + if filename and os.path.isdir(filename): + self.filename = None + self.dirname = filename + else: + self.filename = filename + self.dirname = None + self.set_saved(1) + if self.filename_change_hook: + self.filename_change_hook() + + def open(self, event=None, editFile=None): + flist = self.editwin.flist + # Save in case parent window is closed (ie, during askopenfile()). + if flist: + if not editFile: + filename = self.askopenfile() + else: + filename=editFile + if filename: + # If editFile is valid and already open, flist.open will + # shift focus to its existing window. + # If the current window exists and is a fresh unnamed, + # unmodified editor window (not an interpreter shell), + # pass self.loadfile to flist.open so it will load the file + # in the current window (if the file is not already open) + # instead of a new window. + if (self.editwin and + not getattr(self.editwin, 'interp', None) and + not self.filename and + self.get_saved()): + flist.open(filename, self.loadfile) + else: + flist.open(filename) + else: + if self.text: + self.text.focus_set() + return "break" + + # Code for use outside IDLE: + if self.get_saved(): + reply = self.maybesave() + if reply == "cancel": + self.text.focus_set() + return "break" + if not editFile: + filename = self.askopenfile() + else: + filename=editFile + if filename: + self.loadfile(filename) + else: + self.text.focus_set() + return "break" + + eol = r"(\r\n)|\n|\r" # \r\n (Windows), \n (UNIX), or \r (Mac) + eol_re = re.compile(eol) + 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. + with open(filename, 'rb') as f: + two_lines = f.readline() + f.readline() + f.seek(0) + bytes = f.read() + except OSError 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 + # We now convert all end-of-lines to '\n's + firsteol = self.eol_re.search(chars) + if firsteol: + self.eol_convention = firsteol.group(0) + 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 + # Check presence of a UTF-8 signature first + if bytes.startswith(BOM_UTF8): + try: + chars = bytes[3:].decode("utf-8") + except UnicodeDecodeError: + # has UTF-8 signature, but fails to decode... + return None, False + else: + # Indicates that this file originally had a BOM + self.fileencoding = 'BOM' + return chars, False + # Next look for coding specification + try: + enc = coding_spec(two_lines) + except LookupError as name: + tkMessageBox.showerror( + title="Error loading the file", + message="The encoding '%s' is not known to this Python "\ + "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: + pass + # Try ascii: + try: + chars = str(bytes, 'ascii') + self.fileencoding = None + return chars, False + except UnicodeDecodeError: + pass + # Try utf-8: + try: + chars = str(bytes, 'utf-8') + self.fileencoding = 'utf-8' + return chars, False + except UnicodeDecodeError: + 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 = locale_encoding, + parent = self.editwin.text) + + if enc: + chars = str(bytes, enc) + self.fileencoding = None + return chars, True + except (UnicodeDecodeError, LookupError): + pass + return None, False # None on failure + + def maybesave(self): + if self.get_saved(): + return "yes" + message = "Do you want to save %s before closing?" % ( + self.filename or "this untitled document") + confirm = tkMessageBox.askyesnocancel( + title="Save On Close", + message=message, + default=tkMessageBox.YES, + parent=self.text) + if confirm: + reply = "yes" + self.save(None) + if not self.get_saved(): + reply = "cancel" + elif confirm is None: + reply = "cancel" + else: + reply = "no" + self.text.focus_set() + return reply + + def save(self, event): + if not self.filename: + self.save_as(event) + else: + if self.writefile(self.filename): + self.set_saved(True) + try: + self.editwin.store_file_breaks() + except AttributeError: # may be a PyShell + pass + self.text.focus_set() + return "break" + + def save_as(self, event): + filename = self.asksavefile() + if filename: + if self.writefile(filename): + self.set_filename(filename) + self.set_saved(1) + try: + self.editwin.store_file_breaks() + except AttributeError: + pass + self.text.focus_set() + self.updaterecentfileslist(filename) + return "break" + + def save_a_copy(self, event): + filename = self.asksavefile() + if filename: + self.writefile(filename) + self.text.focus_set() + self.updaterecentfileslist(filename) + return "break" + + def writefile(self, filename): + self.fixlastline() + text = self.text.get("1.0", "end-1c") + if self.eol_convention != "\n": + text = text.replace("\n", self.eol_convention) + chars = self.encode(text) + try: + with open(filename, "wb") as f: + f.write(chars) + return True + except OSError as msg: + tkMessageBox.showerror("I/O Error", str(msg), + parent=self.text) + return False + + def encode(self, chars): + if isinstance(chars, bytes): + # 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 + 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") + + 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( + title="Print", + message="Print to Default Printer", + default=tkMessageBox.OK, + parent=self.text) + if not confirm: + self.text.focus_set() + return "break" + tempfilename = None + saved = self.get_saved() + if saved: + filename = self.filename + # shell undo is reset after every prompt, looks saved, probably isn't + if not saved or filename is None: + (tfd, tempfilename) = tempfile.mkstemp(prefix='IDLE_tmp_') + filename = tempfilename + os.close(tfd) + if not self.writefile(tempfilename): + os.unlink(tempfilename) + return "break" + platform = os.name + printPlatform = True + if platform == 'posix': #posix platform + command = idleConf.GetOption('main','General', + 'print-command-posix') + command = command + " 2>&1" + elif platform == 'nt': #win32 platform + command = idleConf.GetOption('main','General','print-command-win') + else: #no printing for this platform + printPlatform = False + if printPlatform: #we can try to print for this platform + command = command % shlex.quote(filename) + pipe = os.popen(command, "r") + # things can get ugly on NT if there is no printer available. + output = pipe.read().strip() + status = pipe.close() + if status: + output = "Printing failed (exit status 0x%x)\n" % \ + status + output + if output: + output = "Printing command: %s\n" % repr(command) + output + tkMessageBox.showerror("Print status", output, parent=self.text) + else: #no printing for this platform + message = "Printing is not enabled for this platform: %s" % platform + tkMessageBox.showinfo("Print status", message, parent=self.text) + if tempfilename: + os.unlink(tempfilename) + return "break" + + opendialog = None + savedialog = None + + filetypes = [ + ("Python files", "*.py *.pyw", "TEXT"), + ("Text files", "*.txt", "TEXT"), + ("All files", "*"), + ] + + defaultextension = '.py' if sys.platform == 'darwin' else '' + + def askopenfile(self): + dir, base = self.defaultfilename("open") + if not self.opendialog: + self.opendialog = tkFileDialog.Open(parent=self.text, + filetypes=self.filetypes) + filename = self.opendialog.show(initialdir=dir, initialfile=base) + return filename + + def defaultfilename(self, mode="open"): + if self.filename: + return os.path.split(self.filename) + elif self.dirname: + return self.dirname, "" + else: + try: + pwd = os.getcwd() + except OSError: + pwd = "" + return pwd, "" + + def asksavefile(self): + dir, base = self.defaultfilename("save") + if not self.savedialog: + self.savedialog = tkFileDialog.SaveAs( + parent=self.text, + filetypes=self.filetypes, + defaultextension=self.defaultextension) + filename = self.savedialog.show(initialdir=dir, initialfile=base) + 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) + +def _io_binding(parent): # htest # + from tkinter import Toplevel, Text + + root = Toplevel(parent) + root.title("Test IOBinding") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) + class MyEditWin: + def __init__(self, text): + self.text = text + self.flist = None + self.text.bind("", self.open) + self.text.bind('', self.print) + self.text.bind("", self.save) + self.text.bind("", self.saveas) + self.text.bind('', self.savecopy) + def get_saved(self): return 0 + def set_saved(self, flag): pass + def reset_undo(self): pass + def open(self, event): + self.text.event_generate("<>") + def print(self, event): + self.text.event_generate("<>") + def save(self, event): + self.text.event_generate("<>") + def saveas(self, event): + self.text.event_generate("<>") + def savecopy(self, event): + self.text.event_generate("<>") + + text = Text(root) + text.pack() + text.focus_set() + editwin = MyEditWin(text) + IOBinding(editwin) + +if __name__ == "__main__": + from idlelib.idle_test.htest import run + run(_io_binding) diff --git a/Lib/idlelib/keybindingDialog.py b/Lib/idlelib/keybindingDialog.py deleted file mode 100644 index e6438bf..0000000 --- a/Lib/idlelib/keybindingDialog.py +++ /dev/null @@ -1,266 +0,0 @@ -""" -Dialog for building Tkinter accelerator key bindings -""" -from tkinter import * -import tkinter.messagebox as 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('',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: , , ,\n" - ", , .\n" - "Upper case is used when the Shift modifier is present!\n\n" + - "'Emacs style' multi-keystroke bindings are specified as\n" + - "follows: , 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., ." ) - 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('<' + '-'.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','F2','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: - 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.destroy() - - def Cancel(self, event=None): - self.result='' - 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/macosx.py new file mode 100644 index 0000000..268426f --- /dev/null +++ b/Lib/idlelib/macosx.py @@ -0,0 +1,239 @@ +""" +A number of functions that enhance IDLE on Mac OSX. +""" +import sys +import tkinter +import warnings + +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 _initializeTkVariantTests(root): + """ + Initializes OS X Tk variant values for + isAquaTk(), isCarbonTk(), isCocoaTk(), and isXQuartz(). + """ + global _tk_type + if sys.platform == 'darwin': + ws = root.tk.call('tk', 'windowingsystem') + if 'x11' in ws: + _tk_type = "xquartz" + elif 'aqua' not in ws: + _tk_type = "other" + elif 'AppKit' in root.tk.call('winfo', 'server', '.'): + _tk_type = "cocoa" + else: + _tk_type = "carbon" + else: + _tk_type = "other" + +def isAquaTk(): + """ + Returns True if IDLE is using a native OS X Tk (Cocoa or Carbon). + """ + assert _tk_type is not None + return _tk_type == "cocoa" or _tk_type == "carbon" + +def isCarbonTk(): + """ + Returns True if IDLE is using a Carbon Aqua Tk (instead of the + newer Cocoa Aqua Tk). + """ + assert _tk_type is not None + return _tk_type == "carbon" + +def isCocoaTk(): + """ + Returns True if IDLE is using a Cocoa Aqua Tk. + """ + assert _tk_type is not None + return _tk_type == "cocoa" + +def isXQuartz(): + """ + Returns True if IDLE is using an OS X X11 Tk. + """ + assert _tk_type is not None + return _tk_type == "xquartz" + +def tkVersionWarning(root): + """ + Returns a string warning message if the Tk version in use appears to + be one known to cause problems with IDLE. + 1. Apple Cocoa-based Tk 8.5.7 shipped with Mac OS X 10.6 is unusable. + 2. Apple Cocoa-based Tk 8.5.9 in OS X 10.7 and 10.8 is better but + can still crash unexpectedly. + """ + + if isCocoaTk(): + patchlevel = root.tk.call('info', 'patchlevel') + if patchlevel not in ('8.5.7', '8.5.9'): + return False + 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 addOpenEventSupport(root, flist): + """ + This ensures that the application will respond to open AppleEvents, which + makes is feasible to use IDLE as the default application for python files. + """ + def doOpenFile(*args): + for fn in args: + flist.open(fn) + + # The command below is a hook in aquatk that is called whenever the app + # receives a file open event. The callback can have multiple arguments, + # one for every file that should be opened. + root.createcommand("::tk::mac::OpenDocument", doOpenFile) + +def hideTkConsole(root): + try: + root.tk.call('console', 'hide') + except tkinter.TclError: + # Some versions of the Tk framework don't have a console object + pass + +def overrideRootMenu(root, flist): + """ + Replace the Tk root menu by something that is more appropriate for + IDLE with an Aqua Tk. + """ + # The menu that is attached to the Tk root (".") is also used by AquaTk for + # all windows that don't specify a menu of their own. The default menubar + # contains a number of menus, none of which are appropriate for IDLE. The + # Most annoying of those is an 'About Tck/Tk...' menu in the application + # menu. + # + # This function replaces the default menubar by a mostly empty one, it + # should only contain the correct application menu and the window menu. + # + # Due to a (mis-)feature of TkAqua the user will also see an empty Help + # menu. + from tkinter import Menu + from idlelib import Bindings + from idlelib import WindowList + + closeItem = Bindings.menudefs[0][1][-2] + + # Remove the last 3 items of the file menu: a separator, close window and + # quit. Close window will be reinserted just above the save item, where + # it should be according to the HIG. Quit is in the application menu. + del Bindings.menudefs[0][1][-3:] + Bindings.menudefs[0][1].insert(6, closeItem) + + # Remove the 'About' entry from the help menu, it is in the application + # menu + del Bindings.menudefs[-1][1][0:2] + # Remove the 'Configure Idle' entry from the options menu, it is in the + # application menu as 'Preferences' + del Bindings.menudefs[-2][1][0] + menubar = Menu(root) + root.configure(menu=menubar) + menudict = {} + + menudict['windows'] = menu = Menu(menubar, name='windows', tearoff=0) + menubar.add_cascade(label='Window', menu=menu, underline=0) + + def postwindowsmenu(menu=menu): + end = menu.index('end') + if end is None: + end = -1 + + if end > 0: + menu.delete(0, end) + WindowList.add_windows_to_menu(menu) + WindowList.register_callback(postwindowsmenu) + + def about_dialog(event=None): + "Handle Help 'About IDLE' event." + # Synchronize with EditorWindow.EditorWindow.about_dialog. + from idlelib import aboutDialog + aboutDialog.AboutDialog(root, 'About IDLE') + + def config_dialog(event=None): + "Handle Options 'Configure IDLE' event." + # Synchronize with EditorWindow.EditorWindow.config_dialog. + from idlelib import configDialog + + # Ensure that the root object has an instance_dict attribute, + # mirrors code in EditorWindow (although that sets the attribute + # on an EditorWindow instance that is then passed as the first + # argument to ConfigDialog) + root.instance_dict = flist.inversedict + configDialog.ConfigDialog(root, 'Settings') + + def help_dialog(event=None): + "Handle Help 'IDLE Help' event." + # Synchronize with EditorWindow.EditorWindow.help_dialog. + from idlelib import help + help.show_idlehelp(root) + + root.bind('<>', about_dialog) + root.bind('<>', config_dialog) + root.createcommand('::tk::mac::ShowPreferences', config_dialog) + if flist: + root.bind('<>', flist.close_all_callback) + + # The binding above doesn't reliably work on all versions of Tk + # on MacOSX. Adding command definition below does seem to do the + # right thing for now. + root.createcommand('exit', flist.close_all_callback) + + if isCarbonTk(): + # for Carbon AquaTk, replace the default Tk apple menu + menudict['application'] = menu = Menu(menubar, name='apple', + tearoff=0) + menubar.add_cascade(label='IDLE', menu=menu) + Bindings.menudefs.insert(0, + ('application', [ + ('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....', '<>'), + ) + 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 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() + + There are currently three major versions of Tk in use on OS X: + 1. Aqua Cocoa Tk (native default since OS X 10.6) + 2. Aqua Carbon Tk (original native, 32-bit only, deprecated) + 3. X11 (supported by some third-party distributors, deprecated) + There are various differences among the three that affect IDLE + behavior, primarily with menus, mouse key events, and accelerators. + Some one-time customizations are performed here. + Others are dynamically tested throughout idlelib by calls to the + isAquaTk(), isCarbonTk(), isCocoaTk(), isXQuartz() functions which + are initialized here as well. + """ + _initializeTkVariantTests(root) + if isAquaTk(): + hideTkConsole(root) + overrideRootMenu(root, flist) + addOpenEventSupport(root, flist) diff --git a/Lib/idlelib/macosxSupport.py b/Lib/idlelib/macosxSupport.py deleted file mode 100644 index 268426f..0000000 --- a/Lib/idlelib/macosxSupport.py +++ /dev/null @@ -1,239 +0,0 @@ -""" -A number of functions that enhance IDLE on Mac OSX. -""" -import sys -import tkinter -import warnings - -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 _initializeTkVariantTests(root): - """ - Initializes OS X Tk variant values for - isAquaTk(), isCarbonTk(), isCocoaTk(), and isXQuartz(). - """ - global _tk_type - if sys.platform == 'darwin': - ws = root.tk.call('tk', 'windowingsystem') - if 'x11' in ws: - _tk_type = "xquartz" - elif 'aqua' not in ws: - _tk_type = "other" - elif 'AppKit' in root.tk.call('winfo', 'server', '.'): - _tk_type = "cocoa" - else: - _tk_type = "carbon" - else: - _tk_type = "other" - -def isAquaTk(): - """ - Returns True if IDLE is using a native OS X Tk (Cocoa or Carbon). - """ - assert _tk_type is not None - return _tk_type == "cocoa" or _tk_type == "carbon" - -def isCarbonTk(): - """ - Returns True if IDLE is using a Carbon Aqua Tk (instead of the - newer Cocoa Aqua Tk). - """ - assert _tk_type is not None - return _tk_type == "carbon" - -def isCocoaTk(): - """ - Returns True if IDLE is using a Cocoa Aqua Tk. - """ - assert _tk_type is not None - return _tk_type == "cocoa" - -def isXQuartz(): - """ - Returns True if IDLE is using an OS X X11 Tk. - """ - assert _tk_type is not None - return _tk_type == "xquartz" - -def tkVersionWarning(root): - """ - Returns a string warning message if the Tk version in use appears to - be one known to cause problems with IDLE. - 1. Apple Cocoa-based Tk 8.5.7 shipped with Mac OS X 10.6 is unusable. - 2. Apple Cocoa-based Tk 8.5.9 in OS X 10.7 and 10.8 is better but - can still crash unexpectedly. - """ - - if isCocoaTk(): - patchlevel = root.tk.call('info', 'patchlevel') - if patchlevel not in ('8.5.7', '8.5.9'): - return False - 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 addOpenEventSupport(root, flist): - """ - This ensures that the application will respond to open AppleEvents, which - makes is feasible to use IDLE as the default application for python files. - """ - def doOpenFile(*args): - for fn in args: - flist.open(fn) - - # The command below is a hook in aquatk that is called whenever the app - # receives a file open event. The callback can have multiple arguments, - # one for every file that should be opened. - root.createcommand("::tk::mac::OpenDocument", doOpenFile) - -def hideTkConsole(root): - try: - root.tk.call('console', 'hide') - except tkinter.TclError: - # Some versions of the Tk framework don't have a console object - pass - -def overrideRootMenu(root, flist): - """ - Replace the Tk root menu by something that is more appropriate for - IDLE with an Aqua Tk. - """ - # The menu that is attached to the Tk root (".") is also used by AquaTk for - # all windows that don't specify a menu of their own. The default menubar - # contains a number of menus, none of which are appropriate for IDLE. The - # Most annoying of those is an 'About Tck/Tk...' menu in the application - # menu. - # - # This function replaces the default menubar by a mostly empty one, it - # should only contain the correct application menu and the window menu. - # - # Due to a (mis-)feature of TkAqua the user will also see an empty Help - # menu. - from tkinter import Menu - from idlelib import Bindings - from idlelib import WindowList - - closeItem = Bindings.menudefs[0][1][-2] - - # Remove the last 3 items of the file menu: a separator, close window and - # quit. Close window will be reinserted just above the save item, where - # it should be according to the HIG. Quit is in the application menu. - del Bindings.menudefs[0][1][-3:] - Bindings.menudefs[0][1].insert(6, closeItem) - - # Remove the 'About' entry from the help menu, it is in the application - # menu - del Bindings.menudefs[-1][1][0:2] - # Remove the 'Configure Idle' entry from the options menu, it is in the - # application menu as 'Preferences' - del Bindings.menudefs[-2][1][0] - menubar = Menu(root) - root.configure(menu=menubar) - menudict = {} - - menudict['windows'] = menu = Menu(menubar, name='windows', tearoff=0) - menubar.add_cascade(label='Window', menu=menu, underline=0) - - def postwindowsmenu(menu=menu): - end = menu.index('end') - if end is None: - end = -1 - - if end > 0: - menu.delete(0, end) - WindowList.add_windows_to_menu(menu) - WindowList.register_callback(postwindowsmenu) - - def about_dialog(event=None): - "Handle Help 'About IDLE' event." - # Synchronize with EditorWindow.EditorWindow.about_dialog. - from idlelib import aboutDialog - aboutDialog.AboutDialog(root, 'About IDLE') - - def config_dialog(event=None): - "Handle Options 'Configure IDLE' event." - # Synchronize with EditorWindow.EditorWindow.config_dialog. - from idlelib import configDialog - - # Ensure that the root object has an instance_dict attribute, - # mirrors code in EditorWindow (although that sets the attribute - # on an EditorWindow instance that is then passed as the first - # argument to ConfigDialog) - root.instance_dict = flist.inversedict - configDialog.ConfigDialog(root, 'Settings') - - def help_dialog(event=None): - "Handle Help 'IDLE Help' event." - # Synchronize with EditorWindow.EditorWindow.help_dialog. - from idlelib import help - help.show_idlehelp(root) - - root.bind('<>', about_dialog) - root.bind('<>', config_dialog) - root.createcommand('::tk::mac::ShowPreferences', config_dialog) - if flist: - root.bind('<>', flist.close_all_callback) - - # The binding above doesn't reliably work on all versions of Tk - # on MacOSX. Adding command definition below does seem to do the - # right thing for now. - root.createcommand('exit', flist.close_all_callback) - - if isCarbonTk(): - # for Carbon AquaTk, replace the default Tk apple menu - menudict['application'] = menu = Menu(menubar, name='apple', - tearoff=0) - menubar.add_cascade(label='IDLE', menu=menu) - Bindings.menudefs.insert(0, - ('application', [ - ('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....', '<>'), - ) - 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 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() - - There are currently three major versions of Tk in use on OS X: - 1. Aqua Cocoa Tk (native default since OS X 10.6) - 2. Aqua Carbon Tk (original native, 32-bit only, deprecated) - 3. X11 (supported by some third-party distributors, deprecated) - There are various differences among the three that affect IDLE - behavior, primarily with menus, mouse key events, and accelerators. - Some one-time customizations are performed here. - Others are dynamically tested throughout idlelib by calls to the - isAquaTk(), isCarbonTk(), isCocoaTk(), isXQuartz() functions which - are initialized here as well. - """ - _initializeTkVariantTests(root) - if isAquaTk(): - hideTkConsole(root) - overrideRootMenu(root, flist) - addOpenEventSupport(root, flist) diff --git a/Lib/idlelib/mainmenu.py b/Lib/idlelib/mainmenu.py new file mode 100644 index 0000000..ab25ff1 --- /dev/null +++ b/Lib/idlelib/mainmenu.py @@ -0,0 +1,94 @@ +"""Define the menu contents, hotkeys, and event bindings. + +There is additional configuration information in the EditorWindow class (and +subclasses): the menus are created there based on the menu_specs (class) +variable, and menus not created are silently skipped in the code here. This +makes it possible, for example, to define a Debug menu which is only present in +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 + +# Warning: menudefs is altered in macosxSupport.overrideRootMenu() +# after it is determined that an OS X Aqua Tk is in use, +# which cannot be done until after Tk() is first called. +# Do not alter the 'file', 'options', or 'help' cascades here +# without altering overrideRootMenu() as well. +# TODO: Make this more robust + +menudefs = [ + # underscore prefixes character to underscore + ('file', [ + ('_New File', '<>'), + ('_Open...', '<>'), + ('Open _Module...', '<>'), + ('Class _Browser', '<>'), + ('_Path Browser', '<>'), + None, + ('_Save', '<>'), + ('Save _As...', '<>'), + ('Save Cop_y As...', '<>'), + None, + ('Prin_t Window', '<>'), + None, + ('_Close', '<>'), + ('E_xit', '<>'), + ]), + ('edit', [ + ('_Undo', '<>'), + ('_Redo', '<>'), + None, + ('Cu_t', '<>'), + ('_Copy', '<>'), + ('_Paste', '<>'), + ('Select _All', '<>'), + None, + ('_Find...', '<>'), + ('Find A_gain', '<>'), + ('Find _Selection', '<>'), + ('Find in Files...', '<>'), + ('R_eplace...', '<>'), + ('Go to _Line', '<>'), + ]), +('format', [ + ('_Indent Region', '<>'), + ('_Dedent Region', '<>'), + ('Comment _Out Region', '<>'), + ('U_ncomment Region', '<>'), + ('Tabify Region', '<>'), + ('Untabify Region', '<>'), + ('Toggle Tabs', '<>'), + ('New Indent Width', '<>'), + ]), + ('run', [ + ('Python Shell', '<>'), + ]), + ('shell', [ + ('_View Last Restart', '<>'), + ('_Restart Shell', '<>'), + ]), + ('debug', [ + ('_Go to File/Line', '<>'), + ('!_Debugger', '<>'), + ('_Stack Viewer', '<>'), + ('!_Auto-open Stack Viewer', '<>'), + ]), + ('options', [ + ('Configure _IDLE', '<>'), + None, + ]), + ('help', [ + ('_About IDLE', '<>'), + None, + ('_IDLE Help', '<>'), + ('Python _Docs', '<>'), + ]), +] + +if find_spec('turtledemo'): + menudefs[-1][1].append(('Turtle Demo', '<>')) + +default_keydefs = idleConf.GetCurrentKeySet() diff --git a/Lib/idlelib/multicall.py b/Lib/idlelib/multicall.py new file mode 100644 index 0000000..251a84d --- /dev/null +++ b/Lib/idlelib/multicall.py @@ -0,0 +1,446 @@ +""" +MultiCall - a class which inherits its methods from a Tkinter widget (Text, for +example), but enables multiple calls of functions per virtual event - all +matching events will be called, not only the most specific one. This is done +by wrapping the event functions - event_add, event_delete and event_info. +MultiCall recognizes only a subset of legal event sequences. Sequences which +are not recognized are treated by the original Tk handling mechanism. A +more-specific event will be called before a less-specific event. + +The recognized sequences are complete one-event sequences (no emacs-style +Ctrl-X Ctrl-C, no shortcuts like <3>), for all types of events. +Key/Button Press/Release events can have modifiers. +The recognized modifiers are Shift, Control, Option and Command for Mac, and +Control, Alt, Shift, Meta/M for other platforms. + +For all events which were handled by MultiCall, a new member is added to the +event instance passed to the binded functions - mc_type. This is one of the +event type constants defined in this module (such as MC_KEYPRESS). +For Key/Button events (which are handled by MultiCall and may receive +modifiers), another member is added - mc_state. This member gives the state +of the recognized modifiers, as a combination of the modifier constants +also defined in this module (for example, MC_SHIFT). +Using these members is absolutely portable. + +The order by which events are called is defined by these rules: +1. A more-specific event will be called before a less-specific event. +2. A recently-binded event will be called before a previously-binded event, + unless this conflicts with the first rule. +Each function will be called at most once for each event. +""" + +import sys +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; +MC_ACTIVATE=4; MC_CIRCULATE=5; MC_COLORMAP=6; MC_CONFIGURE=7; +MC_DEACTIVATE=8; MC_DESTROY=9; MC_ENTER=10; MC_EXPOSE=11; MC_FOCUSIN=12; +MC_FOCUSOUT=13; MC_GRAVITY=14; MC_LEAVE=15; MC_MAP=16; MC_MOTION=17; +MC_MOUSEWHEEL=18; MC_PROPERTY=19; MC_REPARENT=20; MC_UNMAP=21; MC_VISIBILITY=22; +# the modifier state constants, which define the meaning of mc_state +MC_SHIFT = 1<<0; MC_CONTROL = 1<<2; MC_ALT = 1<<3; MC_META = 1<<5 +MC_OPTION = 1<<6; MC_COMMAND = 1<<7 + +# define the list of modifiers, to be used in complex event types. +if sys.platform == "darwin": + _modifiers = (("Shift",), ("Control",), ("Option",), ("Command",)) + _modifier_masks = (MC_SHIFT, MC_CONTROL, MC_OPTION, MC_COMMAND) +else: + _modifiers = (("Control",), ("Alt",), ("Shift",), ("Meta", "M")) + _modifier_masks = (MC_CONTROL, MC_ALT, MC_SHIFT, MC_META) + +# a dictionary to map a modifier name into its number +_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: +# _SimpleBinder handles event types with no modifiers and no detail. +# No Python functions are called when no events are binded. +# _ComplexBinder handles event types with modifiers and a detail. +# A Python function is called each time an event is generated. + +class _SimpleBinder: + def __init__(self, type, widget, widgetinst): + self.type = type + self.sequence = '<'+_types[type][0]+'>' + self.widget = widget + self.widgetinst = widgetinst + self.bindedfuncs = [] + self.handlerid = None + + def bind(self, triplet, func): + if not self.handlerid: + def handler(event, l = self.bindedfuncs, mc_type = self.type): + event.mc_type = mc_type + wascalled = {} + for i in range(len(l)-1, -1, -1): + func = l[i] + if func not in wascalled: + wascalled[func] = True + r = func(event) + if r: + return r + self.handlerid = self.widget.bind(self.widgetinst, + self.sequence, handler) + self.bindedfuncs.append(func) + + def unbind(self, triplet, func): + self.bindedfuncs.remove(func) + if not self.bindedfuncs: + self.widget.unbind(self.widgetinst, self.sequence, self.handlerid) + self.handlerid = None + + 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 + +# An int in range(1 << len(_modifiers)) represents a combination of modifiers +# (if the least significent bit is on, _modifiers[0] is on, and so on). +# _state_subsets gives for each combination of modifiers, or *state*, +# a list of the states which are a subset of it. This list is ordered by the +# number of modifiers is the state - the most specific state comes first. +_states = range(1 << len(_modifiers)) +_state_names = [''.join(m[0]+'-' + for i, m in enumerate(_modifiers) + if (1 << i) & s) + for s in _states] + +def expand_substates(states): + '''For each item of states return a list containing all combinations of + that item with individual bits reset, sorted by the number of set bits. + ''' + def nbits(n): + "number of bits set in n base 2" + nb = 0 + while n: + n, rem = divmod(n, 2) + nb += rem + return nb + statelist = [] + for state in states: + substates = list(set(state & x for x in states)) + substates.sort(key=nbits, reverse=True) + statelist.append(substates) + return statelist + +_state_subsets = expand_substates(_states) + +# _state_codes gives for each state, the portable code to be passed as mc_state +_state_codes = [] +for s in _states: + r = 0 + for i in range(len(_modifiers)): + if (1 << i) & s: + r |= _modifier_masks[i] + _state_codes.append(r) + +class _ComplexBinder: + # This class binds many functions, and only unbinds them when it is deleted. + # self.handlerids is the list of seqs and ids of binded handler functions. + # The binded functions sit in a dictionary of lists of lists, which maps + # a detail (or None) and a state into a list of functions. + # When a new detail is discovered, handlers for all the possible states + # are binded. + + def __create_handler(self, lists, mc_type, mc_state): + def handler(event, lists = lists, + mc_type = mc_type, mc_state = mc_state, + ishandlerrunning = self.ishandlerrunning, + doafterhandler = self.doafterhandler): + ishandlerrunning[:] = [True] + event.mc_type = mc_type + event.mc_state = mc_state + wascalled = {} + r = None + for l in lists: + for i in range(len(l)-1, -1, -1): + func = l[i] + if func not in wascalled: + wascalled[func] = True + r = l[i](event) + if r: + break + if r: + break + ishandlerrunning[:] = [] + # Call all functions in doafterhandler and remove them from list + for f in doafterhandler: + f() + doafterhandler[:] = [] + if r: + return r + return handler + + def __init__(self, type, widget, widgetinst): + self.type = type + self.typename = _types[type][0] + self.widget = widget + self.widgetinst = widgetinst + self.bindedfuncs = {None: [[] for s in _states]} + self.handlerids = [] + # we don't want to change the lists of functions while a handler is + # running - it will mess up the loop and anyway, we usually want the + # change to happen from the next event. So we have a list of functions + # for the handler to run after it finishes calling the binded functions. + # It calls them only once. + # ishandlerrunning is a list. An empty one means no, otherwise - yes. + # this is done so that it would be mutable. + self.ishandlerrunning = [] + self.doafterhandler = [] + for s in _states: + lists = [self.bindedfuncs[None][i] for i in _state_subsets[s]] + handler = self.__create_handler(lists, type, _state_codes[s]) + seq = '<'+_state_names[s]+self.typename+'>' + self.handlerids.append((seq, self.widget.bind(self.widgetinst, + seq, handler))) + + def bind(self, triplet, func): + if triplet[2] not in self.bindedfuncs: + self.bindedfuncs[triplet[2]] = [[] for s in _states] + for s in _states: + lists = [ self.bindedfuncs[detail][i] + for detail in (triplet[2], None) + for i in _state_subsets[s] ] + handler = self.__create_handler(lists, self.type, + _state_codes[s]) + seq = "<%s%s-%s>"% (_state_names[s], self.typename, triplet[2]) + self.handlerids.append((seq, self.widget.bind(self.widgetinst, + seq, handler))) + doit = lambda: self.bindedfuncs[triplet[2]][triplet[0]].append(func) + if not self.ishandlerrunning: + doit() + else: + self.doafterhandler.append(doit) + + def unbind(self, triplet, func): + doit = lambda: self.bindedfuncs[triplet[2]][triplet[0]].remove(func) + if not self.ishandlerrunning: + doit() + else: + self.doafterhandler.append(doit) + + 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 + +# define the list of event types to be handled by MultiEvent. the order is +# compatible with the definition of event type constants. +_types = ( + ("KeyPress", "Key"), ("KeyRelease",), ("ButtonPress", "Button"), + ("ButtonRelease",), ("Activate",), ("Circulate",), ("Colormap",), + ("Configure",), ("Deactivate",), ("Destroy",), ("Enter",), ("Expose",), + ("FocusIn",), ("FocusOut",), ("Gravity",), ("Leave",), ("Map",), + ("Motion",), ("MouseWheel",), ("Property",), ("Reparent",), ("Unmap",), + ("Visibility",), +) + +# which binder should be used for every event type? +_binder_classes = (_ComplexBinder,) * 4 + (_SimpleBinder,) * (len(_types)-4) + +# A dictionary to map a type name into its number +_type_names = dict([(name, number) + for number in range(len(_types)) + for name in _types[number]]) + +_keysym_re = re.compile(r"^\w+$") +_button_re = re.compile(r"^[1-5]$") +def _parse_sequence(sequence): + """Get a string which should describe an event sequence. If it is + successfully parsed as one, return a tuple containing the state (as an int), + the event type (as an index of _types), and the detail - None if none, or a + string if there is one. If the parsing is unsuccessful, return None. + """ + if not sequence or sequence[0] != '<' or sequence[-1] != '>': + return None + words = sequence[1:-1].split('-') + 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 + else: + detail = None + else: + # _ComplexBinder + if type in [_type_names[s] for s in ("KeyPress", "KeyRelease")]: + type_re = _keysym_re + else: + type_re = _button_re + + if not words: + detail = None + elif len(words) == 1 and type_re.match(words[0]): + detail = words[0] + else: + return None + + return modifiers, type, detail + +def _triplet_to_sequence(triplet): + if triplet[2]: + return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'-'+ \ + triplet[2]+'>' + else: + return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'>' + +_multicall_dict = {} +def MultiCallCreator(widget): + """Return a MultiCall class which inherits its methods from the + given widget class (for example, Tkinter.Text). This is used + instead of a templating mechanism. + """ + if widget in _multicall_dict: + return _multicall_dict[widget] + + class MultiCall (widget): + assert issubclass(widget, tkinter.Misc) + + def __init__(self, *args, **kwargs): + widget.__init__(self, *args, **kwargs) + # a dictionary which maps a virtual event to a tuple with: + # 0. the function binded + # 1. a list of triplets - the sequences it is binded to + self.__eventinfo = {} + self.__binders = [_binder_classes[i](i, widget, self) + 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__) + if type(sequence) is str and len(sequence) > 2 and \ + sequence[:2] == "<<" and sequence[-2:] == ">>": + if sequence in self.__eventinfo: + ei = self.__eventinfo[sequence] + if ei[0] is not None: + for triplet in ei[1]: + self.__binders[triplet[1]].unbind(triplet, ei[0]) + ei[0] = func + if ei[0] is not None: + for triplet in ei[1]: + self.__binders[triplet[1]].bind(triplet, func) + else: + self.__eventinfo[sequence] = [func, []] + return widget.bind(self, sequence, func, add) + + def unbind(self, sequence, funcid=None): + if type(sequence) is str and len(sequence) > 2 and \ + sequence[:2] == "<<" and sequence[-2:] == ">>" and \ + sequence in self.__eventinfo: + func, triplets = self.__eventinfo[sequence] + if func is not None: + for triplet in triplets: + self.__binders[triplet[1]].unbind(triplet, func) + self.__eventinfo[sequence][0] = None + return widget.unbind(self, sequence, funcid) + + def event_add(self, virtual, *sequences): + #print("event_add(%s, %s)" % (repr(virtual), repr(sequences)), + # file=sys.__stderr__) + if virtual not in self.__eventinfo: + self.__eventinfo[virtual] = [None, []] + + func, triplets = self.__eventinfo[virtual] + for seq in sequences: + triplet = _parse_sequence(seq) + if triplet is None: + #print("Tkinter event_add(%s)" % seq, file=sys.__stderr__) + widget.event_add(self, virtual, seq) + else: + if func is not None: + self.__binders[triplet[1]].bind(triplet, func) + triplets.append(triplet) + + def event_delete(self, virtual, *sequences): + if virtual not in self.__eventinfo: + return + func, triplets = self.__eventinfo[virtual] + for seq in sequences: + triplet = _parse_sequence(seq) + if triplet is None: + #print("Tkinter event_delete: %s" % seq, file=sys.__stderr__) + widget.event_delete(self, virtual, seq) + else: + if func is not None: + self.__binders[triplet[1]].unbind(triplet, func) + triplets.remove(triplet) + + def event_info(self, virtual=None): + if virtual is None or virtual not in self.__eventinfo: + return widget.event_info(self, virtual) + else: + return tuple(map(_triplet_to_sequence, + self.__eventinfo[virtual][1])) + \ + widget.event_info(self, virtual) + + def __del__(self): + for virtual in self.__eventinfo: + 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 + + _multicall_dict[widget] = MultiCall + return MultiCall + + +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) + text.bind("<>"%n[0], handler) + text.event_add("<>"%n[0], seq) + n[0] += 1 + bindseq("") + bindseq("") + bindseq("") + bindseq("") + bindseq("") + bindseq("") + bindseq("") + bindseq("") + bindseq("") + bindseq("") + bindseq("") + bindseq("") + root.mainloop() + +if __name__ == "__main__": + from idlelib.idle_test.htest import run + run(_multi_call) diff --git a/Lib/idlelib/outwin.py b/Lib/idlelib/outwin.py new file mode 100644 index 0000000..e614f9b --- /dev/null +++ b/Lib/idlelib/outwin.py @@ -0,0 +1,144 @@ +from tkinter import * +from idlelib.EditorWindow import EditorWindow +import re +import tkinter.messagebox as 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("<>", 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"): + if isinstance(s, (bytes, bytes)): + s = s.decode(IOBinding.encoding, "replace") + self.text.insert(mark, s, tags) + self.text.see(mark) + self.text.update() + return len(s) + + def writelines(self, lines): + for line in lines: + self.write(line) + + def flush(self): + pass + + # Our own right-button menu + + rmenu_specs = [ + ("Cut", "<>", "rmenu_check_cut"), + ("Copy", "<>", "rmenu_check_copy"), + ("Paste", "<>", "rmenu_check_paste"), + (None, None, None), + ("Go to 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 OSError: + 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/paragraph.py b/Lib/idlelib/paragraph.py new file mode 100644 index 0000000..7a9d185 --- /dev/null +++ b/Lib/idlelib/paragraph.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', '<>'), + ]) + ] + + 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/parenmatch.py b/Lib/idlelib/parenmatch.py new file mode 100644 index 0000000..19bad8c --- /dev/null +++ b/Lib/idlelib/parenmatch.py @@ -0,0 +1,178 @@ +"""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.configHandler import idleConf + +_openers = {')':'(',']':'[','}':'{'} +CHECK_DELAY = 100 # miliseconds + +class ParenMatch: + """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. + + The supported styles are: + + default -- When a right paren is typed, highlight the matching + left paren for 1/2 sec. + + expression -- When a right paren is typed, highlight the entire + expression from the left paren to the right paren. + + TODO: + - 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", "<>"), + ]) + ] + 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 = "<>" + # We want the restore event be called before the usual return and + # backspace events. + RESTORE_SEQUENCES = ("", "", + "", "") + + def __init__(self, editwin): + self.editwin = editwin + self.text = editwin.text + # Bind the check-restore event to the function restore_event, + # so that we can then use activate_restore (which calls event_add) + # and deactivate_restore (which calls event_delete). + editwin.text.bind(self.RESTORE_VIRTUAL_EVENT_NAME, + self.restore_event) + self.counter = 0 + self.is_restore_active = 0 + self.set_style(self.STYLE) + + def activate_restore(self): + 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): + 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): + indices = (HyperParser(self.editwin, "insert") + .get_surrounding_brackets()) + 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): + # If it was a shortcut and not really a closing paren, quit. + closer = self.text.get("insert-1c") + if closer not in _openers: + return + hp = HyperParser(self.editwin, "insert-1c") + if not hp.is_in_code(): + return + indices = hp.get_surrounding_brackets(_openers[closer], True) + if indices is None: + self.warn_mismatched() + return + self.activate_restore() + self.create_tag(indices) + self.set_timeout() + + def restore_event(self, event=None): + self.text.tag_delete("paren") + self.deactivate_restore() + self.counter += 1 # disable the last timer, if there is one. + + def handle_restore_timer(self, timer_count): + 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_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_expression(self, indices): + """Highlight the entire expression""" + if self.text.get(indices[1]) in (')', ']', '}'): + rightindex = indices[1]+"+1c" + else: + rightindex = indices[1] + self.text.tag_add("paren", indices[0], rightindex) + self.text.tag_config("paren", self.HILITE_CONFIG) + + # any one of the set_timeout_XXX methods can be used depending on + # the style + + def set_timeout_none(self): + """Highlight will remain until user input turns it off + or the insert has moved""" + # After CHECK_DELAY, call a function which disables the "paren" tag + # if the event is for the most recent timer and the insert has changed, + # or schedules another call for itself. + self.counter += 1 + def callme(callme, self=self, c=self.counter, + index=self.text.index("insert")): + if index != self.text.index("insert"): + self.handle_restore_timer(c) + else: + self.editwin.text_frame.after(CHECK_DELAY, callme, callme) + self.editwin.text_frame.after(CHECK_DELAY, callme, callme) + + def set_timeout_last(self): + """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 + self.editwin.text_frame.after( + self.FLASH_DELAY, + lambda self=self, c=self.counter: self.handle_restore_timer(c)) + + +if __name__ == '__main__': + import unittest + unittest.main('idlelib.idle_test.test_parenmatch', verbosity=2) diff --git a/Lib/idlelib/pathbrowser.py b/Lib/idlelib/pathbrowser.py new file mode 100644 index 0000000..9ab7632 --- /dev/null +++ b/Lib/idlelib/pathbrowser.py @@ -0,0 +1,108 @@ +import os +import sys +import importlib.machinery + +from idlelib.TreeWidget import TreeItem +from idlelib.ClassBrowser import ClassBrowser, ModuleBrowserTreeItem +from idlelib.PyShell import PyShellFileList + + +class PathBrowser(ClassBrowser): + + def __init__(self, flist, _htest=False): + """ + _htest - bool, change box location when running htest + """ + self._htest = _htest + self.init(flist) + + def settitle(self): + "Set window titles." + self.top.wm_title("Path Browser") + self.top.wm_iconname("Path Browser") + + def rootnode(self): + return PathBrowserTreeItem() + +class PathBrowserTreeItem(TreeItem): + + def GetText(self): + return "sys.path" + + def GetSubList(self): + sublist = [] + for dir in sys.path: + item = DirBrowserTreeItem(dir) + sublist.append(item) + return sublist + +class DirBrowserTreeItem(TreeItem): + + def __init__(self, dir, packages=[]): + self.dir = dir + self.packages = packages + + def GetText(self): + if not self.packages: + return self.dir + else: + return self.packages[-1] + ": package" + + def GetSubList(self): + try: + names = os.listdir(self.dir or os.curdir) + except OSError: + return [] + packages = [] + for name in names: + file = os.path.join(self.dir, name) + if self.ispackagedir(file): + nn = os.path.normcase(name) + packages.append((nn, name, file)) + packages.sort() + sublist = [] + for nn, name, file in packages: + item = DirBrowserTreeItem(file, self.packages + [name]) + sublist.append(item) + for nn, name in self.listmodules(names): + item = ModuleBrowserTreeItem(os.path.join(self.dir, name)) + sublist.append(item) + 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") + return os.path.exists(init) + + def listmodules(self, allnames): + modules = {} + suffixes = importlib.machinery.EXTENSION_SUFFIXES[:] + suffixes += importlib.machinery.SOURCE_SUFFIXES + suffixes += importlib.machinery.BYTECODE_SUFFIXES + sorted = [] + for suff in suffixes: + i = -len(suff) + for name in allnames[:]: + normed_name = os.path.normcase(name) + if normed_name[i:] == suff: + mod_name = name[:i] + if mod_name not in modules: + modules[mod_name] = None + sorted.append((normed_name, name)) + allnames.remove(name) + sorted.sort() + return sorted + +def _path_browser(parent): # htest # + flist = PyShellFileList(parent) + PathBrowser(flist, _htest=True) + parent.mainloop() + +if __name__ == "__main__": + from unittest import main + main('idlelib.idle_test.test_pathbrowser', verbosity=2, exit=False) + + from idlelib.idle_test.htest import run + run(_path_browser) diff --git a/Lib/idlelib/percolator.py b/Lib/idlelib/percolator.py new file mode 100644 index 0000000..b8be2aa --- /dev/null +++ b/Lib/idlelib/percolator.py @@ -0,0 +1,105 @@ +from idlelib.WidgetRedirector import WidgetRedirector +from idlelib.Delegator import Delegator + + +class Percolator: + + def __init__(self, text): + # XXX would be nice to inherit from Delegator + self.text = text + self.redir = WidgetRedirector(text) + self.top = self.bottom = Delegator(text) + self.bottom.insert = self.redir.register("insert", self.insert) + self.bottom.delete = self.redir.register("delete", self.delete) + self.filters = [] + + def close(self): + 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.text = None + + def insert(self, index, chars, tags=None): + # Could go away if inheriting from Delegator + self.top.insert(index, chars, tags) + + def delete(self, index1, index2=None): + # Could go away if inheriting from Delegator + self.top.delete(index1, index2) + + def insertfilter(self, filter): + # Perhaps rename to pushfilter()? + assert isinstance(filter, Delegator) + assert filter.delegate is None + filter.setdelegate(self.top) + self.top = filter + + def removefilter(self, filter): + # XXX Perhaps should only support popfilter()? + assert isinstance(filter, Delegator) + assert filter.delegate is not None + f = self.top + if f is filter: + self.top = filter.delegate + filter.setdelegate(None) + else: + while f.delegate is not filter: + assert f is not self.bottom + f.resetcache() + f = f.delegate + f.setdelegate(filter.delegate) + filter.setdelegate(None) + + +def _percolator(parent): # htest # + 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) + self.delegate.insert(*args) + + def delete(self, *args): + print(self.name, ": delete", args) + self.delegate.delete(*args) + + box = tk.Toplevel(parent) + box.title("Test Percolator") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + box.geometry("+%d+%d" % (x, y + 150)) + text = tk.Text(box) + p = Percolator(text) + pin = p.insertfilter + pout = p.removefilter + t1 = Tracer("t1") + t2 = Tracer("t2") + + def toggle1(): + (pin if var1.get() else pout)(t1) + def toggle2(): + (pin if var2.get() else pout)(t2) + + text.pack() + var1 = tk.IntVar() + cb1 = tk.Checkbutton(box, text="Tracer1", command=toggle1, variable=var1) + cb1.pack() + var2 = tk.IntVar() + cb2 = tk.Checkbutton(box, text="Tracer2", command=toggle2, variable=var2) + cb2.pack() + +if __name__ == "__main__": + import unittest + unittest.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 new file mode 100644 index 0000000..9ccbb25 --- /dev/null +++ b/Lib/idlelib/pyparse.py @@ -0,0 +1,617 @@ +import re +import sys +from collections import Mapping + +# Reason last stmt is continued (or C_NONE if it's not). +(C_NONE, C_BACKSLASH, C_STRING_FIRST_LINE, + C_STRING_NEXT_LINES, C_BRACKET) = range(5) + +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""" + ^ + [ \t]* + (?: while + | else + | def + | return + | assert + | break + | class + | continue + | elif + | try + | except + | raise + | import + | yield + ) + \b +""", re.VERBOSE | re.MULTILINE).search + +# Match blank line or non-indenting comment line. + +_junkre = re.compile(r""" + [ \t]* + (?: \# \S .* )? + \n +""", re.VERBOSE).match + +# Match any flavor of string; the terminating quote is optional +# so that we're robust in the face of incomplete program text. + +_match_stringre = re.compile(r""" + \""" [^"\\]* (?: + (?: \\. | "(?!"") ) + [^"\\]* + )* + (?: \""" )? + +| " [^"\\\n]* (?: \\. [^"\\\n]* )* "? + +| ''' [^'\\]* (?: + (?: \\. | '(?!'') ) + [^'\\]* + )* + (?: ''' )? + +| ' [^'\\\n]* (?: \\. [^'\\\n]* )* '? +""", re.VERBOSE | re.DOTALL).match + +# Match a line that starts with something interesting; +# used to find the first item of a bracket structure. + +_itemre = re.compile(r""" + [ \t]* + [^\s#\\] # if we match, m.end()-1 is the interesting char +""", re.VERBOSE).match + +# Match start of stmts that should be followed by a dedent. + +_closere = re.compile(r""" + \s* + (?: return + | break + | continue + | raise + | pass + ) + \b +""", re.VERBOSE).match + +# Chew up non-special chars as quickly as possible. If match is +# successful, m.end() less 1 is the index of the last boring char +# matched. If match is unsuccessful, the string starts with an +# interesting char. + +_chew_ordinaryre = re.compile(r""" + [^[\](){}#'"\\]+ +""", re.VERBOSE).match + + +class StringTranslatePseudoMapping(Mapping): + r"""Utility class to be used with str.translate() + + This Mapping class wraps a given dict. When a value for a key is + requested via __getitem__() or get(), the key is looked up in the + given dict. If found there, the value from the dict is returned. + Otherwise, the default value given upon initialization is returned. + + This allows using str.translate() to make some replacements, and to + replace all characters for which no replacement was specified with + a given character instead of leaving them as-is. + + For example, to replace everything except whitespace with 'x': + + >>> whitespace_chars = ' \t\n\r' + >>> preserve_dict = {ord(c): ord(c) for c in whitespace_chars} + >>> mapping = StringTranslatePseudoMapping(preserve_dict, ord('x')) + >>> text = "a + b\tc\nd" + >>> text.translate(mapping) + 'x x x\tx\nx' + """ + def __init__(self, non_defaults, default_value): + self._non_defaults = non_defaults + self._default_value = default_value + + def _get(key, _get=non_defaults.get, _default=default_value): + return _get(key, _default) + self._get = _get + + def __getitem__(self, item): + return self._get(item) + + def __len__(self): + return len(self._non_defaults) + + def __iter__(self): + return iter(self._non_defaults) + + def get(self, key, default=None): + return self._get(key) + + +class Parser: + + def __init__(self, indentwidth, tabwidth): + self.indentwidth = indentwidth + self.tabwidth = tabwidth + + def set_str(self, s): + assert len(s) == 0 or s[-1] == '\n' + self.str = s + 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): + str, pos = self.str, None + + if not is_char_in_string: + # no clue -- make the caller pass everything + return None + + # 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(str) + for tries in range(5): + i = str.rfind(":\n", 0, limit) + if i < 0: + break + 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 + limit = i + if pos is None: + # Nothing looks like a block-opener, or stuff does + # but is_char_in_string keeps returning true; most likely + # we're in or near a giant string, the colorizer hasn't + # caught up enough to be helpful, or there simply *aren't* + # any interesting stmts. In any of these cases we're + # 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(str) + if m and not is_char_in_string(m.start()): + pos = m.start() + return pos + + # Peeking back worked; look forward until _synchre no longer + # matches. + i = pos + 1 + while 1: + m = _synchre(str, i) + if m: + s, i = m.span() + if not is_char_in_string(s): + pos = s + else: + break + return pos + + # Throw away the start of the string. Intended to be called with + # find_good_parse_start's result. + + def set_lo(self, lo): + assert lo == 0 or self.str[lo-1] == '\n' + if lo > 0: + self.str = self.str[lo:] + + # Build a translation table to map uninteresting chars to 'x', open + # brackets to '(', close brackets to ')' while preserving quotes, + # backslashes, newlines and hashes. This is to be passed to + # str.translate() in _study1(). + _tran = {} + _tran.update((ord(c), ord('(')) for c in "({[") + _tran.update((ord(c), ord(')')) for c in ")}]") + _tran.update((ord(c), ord(c)) for c in "\"'\\\n#") + _tran = StringTranslatePseudoMapping(_tran, default_value=ord('x')) + + # As quickly as humanly possible , 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 + + # Map all uninteresting characters to "x", all open brackets + # 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. + str = self.str + str = str.translate(self._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 + # whether & why the last stmt is a continuation. + continuation = C_NONE + level = lno = 0 # level is nesting level; lno is line number + self.goodlines = goodlines = [0] + push_good = goodlines.append + i, n = 0, len(str) + while i < n: + ch = str[i] + i = i+1 + + # cases are checked in decreasing order of frequency + if ch == 'x': + continue + + if ch == '\n': + lno = lno + 1 + if level == 0: + push_good(lno) + # else we're in an unclosed bracket structure + continue + + if ch == '(': + level = level + 1 + continue + + if ch == ')': + if level: + level = level - 1 + # else the program is invalid, but we can't complain + continue + + if ch == '"' or ch == "'": + # consume the string + quote = ch + if str[i-1:i+2] == quote * 3: + quote = quote * 3 + firstlno = lno + w = len(quote) - 1 + i = i+w + while i < n: + ch = str[i] + i = i+1 + + if ch == 'x': + continue + + if str[i-1:i+w] == quote: + i = i+w + break + + if ch == '\n': + lno = lno + 1 + if w == 0: + # unterminated single-quoted string + if level == 0: + push_good(lno) + break + continue + + if ch == '\\': + assert i < n + if str[i] == '\n': + lno = lno + 1 + i = i+1 + continue + + # else comment char or paren inside string + + else: + # didn't break out of the loop, so we're still + # inside a string + if (lno - 1) == firstlno: + # before the previous \n in str, we were in the first + # line of the string + continuation = C_STRING_FIRST_LINE + else: + continuation = C_STRING_NEXT_LINES + continue # with outer loop + + if ch == '#': + # consume the comment + i = str.find('\n', i) + assert i >= 0 + continue + + assert ch == '\\' + assert i < n + if str[i] == '\n': + lno = lno + 1 + if i+1 == n: + continuation = C_BACKSLASH + i = i+1 + + # The last stmt may be continued for all 3 reasons. + # String continuation takes precedence over bracket + # continuation, which beats backslash continuation. + if (continuation != C_STRING_FIRST_LINE + and continuation != C_STRING_NEXT_LINES and level > 0): + continuation = C_BRACKET + self.continuation = continuation + + # Push the final line number as a sentinel value, regardless of + # whether it's continued. + assert (continuation == C_NONE) == (goodlines[-1] == lno) + if goodlines[-1] != lno: + push_good(lno) + + def get_continuation_type(self): + 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): + if self.study_level >= 2: + return + self._study1() + self.study_level = 2 + + # Set p and q to slice indices of last interesting stmt. + str, goodlines = self.str, self.goodlines + i = len(goodlines) - 1 + p = len(str) # index of newest line + while i: + assert p + # 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 = 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(str, p): + i = i-1 + else: + break + if i == 0: + # nothing but junk! + assert p == 0 + q = p + self.stmt_start, self.stmt_end = p, q + + # Analyze this stmt, to find the last open bracket (if any) + # and last interesting character (if any). + lastch = "" + stack = [] # stack of open bracket indices + push_stack = stack.append + bracketing = [(p, 0)] + while p < q: + # suck up all except ()[]{}'"#\\ + 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 str[i] in " \t\n": + i = i-1 + if i >= p: + lastch = str[i] + p = newp + if p >= q: + break + + ch = str[p] + + if ch in "([{": + push_stack(p) + bracketing.append((p, len(stack))) + lastch = ch + p = p+1 + continue + + if ch in ")]}": + if stack: + del stack[-1] + lastch = ch + p = p+1 + bracketing.append((p, len(stack))) + continue + + if ch == '"' or ch == "'": + # consume string + # Note that study1 did this with a Python loop, but + # we use a regexp here; the reason is speed in both + # cases; the string may be huge, but study1 pre-squashed + # strings to a couple of characters per line. study1 + # also needed to keep track of newlines, and we don't + # have to. + bracketing.append((p, len(stack)+1)) + lastch = ch + 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 = str.find('\n', p, q) + 1 + assert p > 0 + bracketing.append((p, len(stack))) + continue + + assert ch == '\\' + p = p+1 # beyond backslash + assert p < q + if str[p] != '\n': + # the program is invalid, but can't complain + lastch = ch + str[p] + p = p+1 # beyond escaped char + + # end while p < q: + + self.lastch = lastch + if stack: + self.lastopenbracketpos = stack[-1] + self.stmt_bracketing = tuple(bracketing) + + # Assuming continuation is C_BRACKET, return the number + # of spaces the next line should be indented. + + def compute_bracket_indent(self): + self._study2() + assert self.continuation == C_BRACKET + j = self.lastopenbracketpos + 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(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 = str.find('\n', j) + 1 + else: + # nothing interesting follows the bracket; + # reproduce the bracket line's indentation + a level + j = i = origi + while str[j] in " \t": + j = j+1 + extra = self.indentwidth + return len(str[i:j].expandtabs(self.tabwidth)) + extra + + # 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). + + def get_num_lines_in_stmt(self): + self._study1() + goodlines = self.goodlines + return goodlines[-1] - goodlines[-2] + + # 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. + + def compute_backslash_indent(self): + self._study2() + assert self.continuation == C_BACKSLASH + str = self.str + i = self.stmt_start + 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 = str.find('\n', startpos) + 1 + found = level = 0 + while i < endpos: + ch = str[i] + if ch in "([{": + level = level + 1 + i = i+1 + elif ch in ")]}": + if level: + level = level - 1 + i = i+1 + elif ch == '"' or ch == "'": + i = _match_stringre(str, i, endpos).end() + elif ch == '#': + break + elif level == 0 and ch == '=' and \ + (i == 0 or str[i-1] not in "=<>!") and \ + str[i+1] != '=': + found = 1 + break + else: + i = i+1 + + if found: + # 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*\\", str[i:endpos]) is None + + if not found: + # oh well ... settle for moving beyond the first chunk + # of non-whitespace chars + i = startpos + while str[i] not in " \t\n": + i = i+1 + + 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): + self._study2() + i, n = self.stmt_start, self.stmt_end + j = i + str = self.str + while j < n and str[j] in " \t": + j = j + 1 + return str[i:j] + + # Did the last interesting stmt open a block? + + def is_block_opener(self): + self._study2() + return self.lastch == ':' + + # Did the last interesting stmt close a block? + + def is_block_closer(self): + self._study2() + return _closere(self.str, self.stmt_start) is not None + + # index of last open bracket ({[, or None if none + lastopenbracketpos = None + + def get_last_open_bracket_pos(self): + self._study2() + 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 + + def get_last_stmt_bracketing(self): + self._study2() + return self.stmt_bracketing diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py new file mode 100755 index 0000000..1bcc9b6 --- /dev/null +++ b/Lib/idlelib/pyshell.py @@ -0,0 +1,1619 @@ +#! /usr/bin/env python3 + +import getopt +import os +import os.path +import re +import socket +import subprocess +import sys +import threading +import time +import tokenize +import io + +import linecache +from code import InteractiveInterpreter +from platform import python_version, system + +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 tkinter.messagebox as 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 import Debugger +from idlelib import RemoteDebugger +from idlelib import macosxSupport + +HOST = '127.0.0.1' # python execution server on localhost loopback +PORT = 0 # someday pass in host, port for remote debug capability + +# 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): + """Show Idle-format warning (after replacing warnings.showwarning). + + The differences are the formatter called, the file=None replacement, + which can be None, the capture of the consequence AttributeError, + and the output of a hard-coded prompt. + """ + if file is None: + file = warning_stream + try: + file.write(idle_formatwarning( + message, category, filename, lineno, line=line)) + file.write(">>> ") + except (AttributeError, OSError): + pass # if file (probably __stderr__) is invalid, skip warning. + +_warnings_showwarning = None + +def capture_warnings(capture): + "Replace warning.showwarning with idle_showwarning, or reverse." + + global _warnings_showwarning + if capture: + if _warnings_showwarning is None: + _warnings_showwarning = warnings.showwarning + warnings.showwarning = idle_showwarning + else: + if _warnings_showwarning is not None: + warnings.showwarning = _warnings_showwarning + _warnings_showwarning = None + +capture_warnings(True) + +def extended_linecache_checkcache(filename=None, + orig_checkcache=linecache.checkcache): + """Extend linecache.checkcache to preserve the entries + + Rather than repeating the linecache code, patch it to save the + entries, call the original linecache.checkcache() + (skipping them), and then restore the saved entries. + + orig_checkcache is bound at definition time to the original + method, allowing it to be patched. + """ + cache = linecache.cache + save = {} + for key in list(cache): + if key[:1] + key[-1:] == '<>': + save[key] = cache.pop(key) + orig_checkcache(filename) + cache.update(save) + +# Patch linecache.checkcache(): +linecache.checkcache = extended_linecache_checkcache + + +class PyShellEditorWindow(EditorWindow): + "Regular text edit window in IDLE, supports breakpoints" + + def __init__(self, *args): + self.breakpoints = [] + EditorWindow.__init__(self, *args) + self.text.bind("<>", self.set_breakpoint_here) + self.text.bind("<>", self.clear_breakpoint_here) + self.text.bind("<>", self.flist.open_shell) + + 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): + self.restore_file_breaks() + old_hook() + self.io.set_filename_change_hook(filename_changed_hook) + if self.io.filename: + self.restore_file_breaks() + self.color_breakpoint_text() + + rmenu_specs = [ + ("Cut", "<>", "rmenu_check_cut"), + ("Copy", "<>", "rmenu_check_copy"), + ("Paste", "<>", "rmenu_check_paste"), + (None, None, None), + ("Set Breakpoint", "<>", None), + ("Clear Breakpoint", "<>", None) + ] + + def color_breakpoint_text(self, color=True): + "Turn colorizing of breakpoint text on or off" + if self.io is None: + # possible due to update in restore_file_breaks + return + if color: + theme = idleConf.CurrentTheme() + cfg = idleConf.GetHighlight(theme, "break") + else: + cfg = {'foreground': '', 'background': ''} + self.text.tag_config('BREAK', cfg) + + def set_breakpoint(self, lineno): + text = self.text + filename = self.io.filename + text.tag_add("BREAK", "%d.0" % lineno, "%d.0" % (lineno+1)) + try: + self.breakpoints.index(lineno) + except ValueError: # only add if missing, i.e. do once + self.breakpoints.append(lineno) + try: # update the subprocess debugger + debug = self.flist.pyshell.interp.debugger + debug.set_breakpoint_here(filename, lineno) + except: # but debugger may not be active right now.... + pass + + def set_breakpoint_here(self, event=None): + text = self.text + filename = self.io.filename + if not filename: + text.bell() + return + lineno = int(float(text.index("insert"))) + self.set_breakpoint(lineno) + + def clear_breakpoint_here(self, event=None): + text = self.text + filename = self.io.filename + if not filename: + text.bell() + return + lineno = int(float(text.index("insert"))) + try: + self.breakpoints.remove(lineno) + except: + pass + text.tag_remove("BREAK", "insert linestart",\ + "insert lineend +1char") + try: + debug = self.flist.pyshell.interp.debugger + debug.clear_breakpoint_here(filename, lineno) + except: + pass + + def clear_file_breaks(self): + if self.breakpoints: + text = self.text + filename = self.io.filename + if not filename: + text.bell() + return + self.breakpoints = [] + text.tag_remove("BREAK", "1.0", END) + try: + debug = self.flist.pyshell.interp.debugger + debug.clear_file_breaks(filename) + except: + pass + + def store_file_breaks(self): + "Save breakpoints when file is saved" + # XXX 13 Dec 2002 KBK Currently the file must be saved before it can + # be run. The breaks are saved at that time. If we introduce + # a temporary file save feature the save breaks functionality + # needs to be re-verified, since the breaks at the time the + # temp file is created may differ from the breaks at the last + # permanent save of the file. Currently, a break introduced + # after a save will be effective, but not persistent. + # This is necessary to keep the saved breaks synched with the + # saved file. + # + # Breakpoints are set as tagged ranges in the text. + # Since a modified file has to be saved before it is + # run, and since self.breakpoints (from which the subprocess + # debugger is loaded) is updated during the save, the visible + # breaks stay synched with the subprocess even if one of these + # unexpected breakpoint deletions occurs. + breaks = self.breakpoints + filename = self.io.filename + try: + with open(self.breakpointPath, "r") as fp: + lines = fp.readlines() + except OSError: + lines = [] + try: + with open(self.breakpointPath, "w") as new_file: + for line in lines: + if not line.startswith(filename + '='): + new_file.write(line) + self.update_breakpoints() + breaks = self.breakpoints + if breaks: + new_file.write(filename + '=' + str(breaks) + '\n') + except OSError as err: + if not getattr(self.root, "breakpoint_error_displayed", False): + self.root.breakpoint_error_displayed = True + tkMessageBox.showerror(title='IDLE Error', + message='Unable to update breakpoint list:\n%s' + % str(err), + parent=self.text) + + def restore_file_breaks(self): + self.text.update() # this enables setting "BREAK" tags to be visible + if self.io is None: + # can happen if IDLE closes due to the .update() call + return + filename = self.io.filename + if filename is None: + return + if os.path.isfile(self.breakpointPath): + with open(self.breakpointPath, "r") as fp: + lines = fp.readlines() + for line in lines: + if line.startswith(filename + '='): + breakpoint_linenumbers = eval(line[len(filename)+1:]) + for breakpoint_linenumber in breakpoint_linenumbers: + self.set_breakpoint(breakpoint_linenumber) + + def update_breakpoints(self): + "Retrieves all the breakpoints in the current window" + text = self.text + ranges = text.tag_ranges("BREAK") + linenumber_list = self.ranges_to_linenumbers(ranges) + self.breakpoints = linenumber_list + + def ranges_to_linenumbers(self, ranges): + lines = [] + for index in range(0, len(ranges), 2): + lineno = int(float(ranges[index].string)) + end = int(float(ranges[index+1].string)) + while lineno < end: + lines.append(lineno) + lineno += 1 + return lines + +# XXX 13 Dec 2002 KBK Not used currently +# def saved_change_hook(self): +# "Extend base method - clear breaks if module is modified" +# if not self.get_saved(): +# self.clear_file_breaks() +# EditorWindow.saved_change_hook(self) + + def _close(self): + "Extend base method - clear breaks when module is closed" + self.clear_file_breaks() + EditorWindow._close(self) + + +class PyShellFileList(FileList): + "Extend base class: IDLE supports a shell and breakpoints" + + # override FileList's class variable, instances return PyShellEditorWindow + # instead of EditorWindow when new edit windows are created. + EditorWindow = PyShellEditorWindow + + pyshell = None + + def open_shell(self, event=None): + if self.pyshell: + self.pyshell.top.wakeup() + else: + self.pyshell = PyShell(self) + if self.pyshell: + if not self.pyshell.begin(): + return None + return self.pyshell + + +class ModifiedColorDelegator(ColorDelegator): + "Extend base class: colorizer for the shell window itself" + + def __init__(self): + ColorDelegator.__init__(self) + self.LoadTagDefs() + + def recolorize_main(self): + self.tag_remove("TODO", "1.0", "iomark") + self.tag_add("SYNC", "1.0", "iomark") + ColorDelegator.recolorize_main(self) + + def LoadTagDefs(self): + ColorDelegator.LoadTagDefs(self) + theme = idleConf.CurrentTheme() + self.tagdefs.update({ + "stdin": {'background':None,'foreground':None}, + "stdout": idleConf.GetHighlight(theme, "stdout"), + "stderr": idleConf.GetHighlight(theme, "stderr"), + "console": idleConf.GetHighlight(theme, "console"), + }) + + def removecolors(self): + # Don't remove shell color tags before "iomark" + for tag in self.tagdefs: + self.tag_remove(tag, "iomark", "end") + +class ModifiedUndoDelegator(UndoDelegator): + "Extend base class: forbid insert/delete before the I/O mark" + + def insert(self, index, chars, tags=None): + try: + if self.delegate.compare(index, "<", "iomark"): + self.delegate.bell() + return + except TclError: + pass + UndoDelegator.insert(self, index, chars, tags) + + def delete(self, index1, index2=None): + try: + if self.delegate.compare(index1, "<", "iomark"): + self.delegate.bell() + return + except TclError: + pass + UndoDelegator.delete(self, index1, index2) + + +class MyRPCClient(rpc.RPCClient): + + def handle_EOF(self): + "Override the base class - just re-raise EOFError" + raise EOFError + + +class ModifiedInterpreter(InteractiveInterpreter): + + def __init__(self, tkconsole): + 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 + self.original_compiler_flags = self.compile.compiler.flags + + _afterid = None + rpcclt = None + rpcsubproc = 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) + + 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] + # 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') + if __name__ == 'idlelib.PyShell': + command = "__import__('idlelib.run').run.main(%r)" % (del_exitf,) + else: + command = "__import__('run').main(%r)" % (del_exitf,) + return [sys.executable] + w + ["-c", command, str(self.port)] + + def start_subprocess(self): + addr = (HOST, self.port) + # GUI makes several attempts to acquire socket, listens for connection + for i in range(3): + time.sleep(i) + try: + self.rpcclt = MyRPCClient(addr) + break + except OSError: + pass + else: + self.display_port_binding_error() + return None + # if PORT was 0, system will assign an 'ephemeral' port. Find it out: + self.port = self.rpcclt.listening_sock.getsockname()[1] + # if PORT was not 0, probably working with a remote execution server + if PORT != 0: + # To allow reconnection within the 2MSL wait (cf. Stevens TCP + # V1, 18.6), set SO_REUSEADDR. Note that this can be problematic + # on Windows since the implementation allows two active sockets on + # the same address! + self.rpcclt.listening_sock.setsockopt(socket.SOL_SOCKET, + socket.SO_REUSEADDR, 1) + self.spawn_subprocess() + #time.sleep(20) # test to simulate GUI not accepting connection + # Accept the connection from the Python execution server + self.rpcclt.listening_sock.settimeout(10) + try: + self.rpcclt.accept() + except socket.timeout: + self.display_no_subprocess_error() + return None + self.rpcclt.register("console", self.tkconsole) + self.rpcclt.register("stdin", self.tkconsole.stdin) + self.rpcclt.register("stdout", self.tkconsole.stdout) + self.rpcclt.register("stderr", self.tkconsole.stderr) + self.rpcclt.register("flist", self.tkconsole.flist) + self.rpcclt.register("linecache", linecache) + self.rpcclt.register("interp", self) + self.transfer_path(with_cwd=True) + self.poll_subprocess() + return self.rpcclt + + def restart_subprocess(self, with_cwd=False, filename=''): + if self.restarting: + return self.rpcclt + self.restarting = True + # close only the subprocess debugger + debug = self.getdebugger() + if debug: + try: + # Only close subprocess debugger, don't unregister gui_adap! + RemoteDebugger.close_subprocess_debugger(self.rpcclt) + except: + pass + # Kill subprocess, spawn a new one, accept connection. + self.rpcclt.close() + self.terminate_subprocess() + console = self.tkconsole + was_executing = console.executing + console.executing = False + self.spawn_subprocess() + try: + self.rpcclt.accept() + except socket.timeout: + self.display_no_subprocess_error() + return None + self.transfer_path(with_cwd=with_cwd) + console.stop_readline() + # annotate restart in shell window and mark it + console.text.delete("iomark", "end-1c") + 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: + console.showprompt() + # restart subprocess debugger + if debug: + # Restarted debugger connects to current instance of debug GUI + RemoteDebugger.restart_subprocess_debugger(self.rpcclt) + # reload remote debugger breakpoints for all PyShellEditWindows + debug.load_breakpoints() + self.compile.compiler.flags = self.original_compiler_flags + self.restarting = False + return self.rpcclt + + def __request_interrupt(self): + self.rpcclt.remotecall("exec", "interrupt_the_server", (), {}) + + def interrupt_subprocess(self): + threading.Thread(target=self.__request_interrupt).start() + + def kill_subprocess(self): + 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.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: + try: + self.rpcsubproc.wait() + except OSError: + return + + def transfer_path(self, with_cwd=False): + if with_cwd: # Issue 13506 + path = [''] # include Current Working Directory + path.extend(sys.path) + else: + path = sys.path + + self.runcommand("""if 1: + import sys as _sys + _sys.path = %r + del _sys + \n""" % (path,)) + + active_seq = None + + def poll_subprocess(self): + clt = self.rpcclt + if clt is None: + return + try: + response = clt.pollresponse(self.active_seq, wait=0.05) + except (EOFError, OSError, KeyboardInterrupt): + # lost connection or subprocess terminated itself, restart + # [the KBI is from rpc.SocketIO.handle_EOF()] + if self.tkconsole.closing: + return + response = None + self.restart_subprocess() + if response: + self.tkconsole.resetoutput() + self.active_seq = None + how, what = response + console = self.tkconsole.console + if how == "OK": + if what is not None: + print(repr(what), file=console) + elif how == "EXCEPTION": + if self.tkconsole.getvar("<>"): + self.remote_stack_viewer() + elif how == "ERROR": + 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: + try: + self.tkconsole.endexecuting() + except AttributeError: # shell may have closed + pass + # Reschedule myself + if not self.tkconsole.closing: + self._afterid = self.tkconsole.text.after( + self.tkconsole.pollinterval, self.poll_subprocess) + + debugger = None + + def setdebugger(self, debugger): + self.debugger = debugger + + def getdebugger(self): + return self.debugger + + def open_remote_stack_viewer(self): + """Initiate the remote stack viewer from a separate thread. + + This method is called from the subprocess, and by returning from this + method we allow the subprocess to unblock. After a bit the shell + requests the subprocess to open the remote stack viewer which returns a + static object looking at the last exception. It is queried through + the RPC mechanism. + + """ + self.tkconsole.text.after(300, self.remote_stack_viewer) + return + + def remote_stack_viewer(self): + from idlelib import RemoteObjectBrowser + oid = self.rpcclt.remotequeue("exec", "stackviewer", ("flist",), {}) + if oid is None: + self.tkconsole.root.bell() + return + 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'] + sc = ScrolledCanvas(top, bg=background, highlightthickness=0) + sc.frame.pack(expand=1, fill="both") + node = TreeNode(sc.canvas, None, item) + node.expand() + # XXX Should GC the remote tree when closing the window + + gid = 0 + + def execsource(self, source): + "Like runsource() but assumes complete exec source" + filename = self.stuffsource(source) + self.execfile(filename, source) + + def execfile(self, filename, source=None): + "Execute an existing file" + if source is None: + with tokenize.open(filename) as fp: + source = fp.read() + try: + code = compile(source, filename, "exec") + except (OverflowError, SyntaxError): + self.tkconsole.resetoutput() + print('*** Error in script or command!\n' + 'Traceback (most recent call last):', + file=self.tkconsole.stderr) + InteractiveInterpreter.showsyntaxerror(self, filename) + self.tkconsole.showprompt() + else: + self.runcode(code) + + def runsource(self, source): + "Extend base class method: Stuff the source in the line cache first" + filename = self.stuffsource(source) + self.more = 0 + self.save_warnings_filters = warnings.filters[:] + warnings.filterwarnings(action="error", category=SyntaxWarning) + # at the moment, InteractiveInterpreter expects str + assert isinstance(source, str) + #if isinstance(source, str): + # from idlelib import IOBinding + # try: + # source = 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" + filename = "" % self.gid + self.gid = self.gid + 1 + lines = source.split("\n") + linecache.cache[filename] = len(source)+1, 0, lines, filename + return filename + + def prepend_syspath(self, filename): + "Prepend sys.path with file's directory if not already included" + self.runcommand("""if 1: + _filename = %r + import sys as _sys + from os.path import dirname as _dirname + _dir = _dirname(_filename) + if not _dir in _sys.path: + _sys.path.insert(0, _dir) + del _filename, _sys, _dirname, _dir + \n""" % (filename,)) + + def showsyntaxerror(self, filename=None): + """Override Interactive Interpreter method: Use 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") + type, value, tb = sys.exc_info() + msg = getattr(value, 'msg', '') or value or "" + 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) + 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() + + def showtraceback(self): + "Extend base class method to reset output properly" + self.tkconsole.resetoutput() + self.checklinecache() + InteractiveInterpreter.showtraceback(self) + if self.tkconsole.getvar("<>"): + self.tkconsole.open_stack_viewer() + + def checklinecache(self): + c = linecache.cache + for key in list(c.keys()): + if key[:1] + key[-1:] != "<>": + del c[key] + + def runcommand(self, code): + "Run the code without invoking the debugger" + # The code better not raise an exception! + if self.tkconsole.executing: + self.display_executing_dialog() + return 0 + if self.rpcclt: + self.rpcclt.remotequeue("exec", "runcode", (code,), {}) + else: + exec(code, self.locals) + return 1 + + def runcode(self, code): + "Override base class method" + 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() + if not debugger and self.rpcclt is not None: + self.active_seq = self.rpcclt.asyncqueue("exec", "runcode", + (code,), {}) + elif debugger: + debugger.run(code, self.locals) + else: + exec(code, self.locals) + except SystemExit: + if not self.tkconsole.closing: + if tkMessageBox.askyesno( + "Exit?", + "Do you want to exit altogether?", + default="yes", + parent=self.tkconsole.text): + raise + else: + self.showtraceback() + else: + raise + except: + if use_subprocess: + print("IDLE internal error in runcode()", + file=self.tkconsole.stderr) + self.showtraceback() + self.tkconsole.endexecuting() + else: + if self.tkconsole.canceled: + self.tkconsole.canceled = False + print("KeyboardInterrupt", file=self.tkconsole.stderr) + else: + self.showtraceback() + finally: + if not use_subprocess: + try: + self.tkconsole.endexecuting() + except AttributeError: # shell may have closed + pass + + def write(self, s): + "Override base class method" + return self.tkconsole.stderr.write(s) + + def display_port_binding_error(self): + tkMessageBox.showerror( + "Port Binding Error", + "IDLE can't bind to a TCP/IP port, which is necessary to " + "communicate with its Python execution server. This might be " + "because no networking is installed on this computer. " + "Run IDLE with the -n command line switch to start without a " + "subprocess and refer to Help/IDLE Help 'Running without a " + "subprocess' for further details.", + parent=self.tkconsole.text) + + def display_no_subprocess_error(self): + tkMessageBox.showerror( + "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): + tkMessageBox.showerror( + "Already executing", + "The Python Shell window is already executing a command; " + "please wait until it is finished.", + parent=self.tkconsole.text) + + +class PyShell(OutputWindow): + + shell_title = "Python " + python_version() + " Shell" + + # Override classes + ColorDelegator = ModifiedColorDelegator + UndoDelegator = ModifiedUndoDelegator + + # Override menus + menu_specs = [ + ("file", "_File"), + ("edit", "_Edit"), + ("debug", "_Debug"), + ("options", "_Options"), + ("windows", "_Window"), + ("help", "_Help"), + ] + + + # New classes + from idlelib.IdleHistory import History + + def __init__(self, flist=None): + if use_subprocess: + ms = self.menu_specs + if ms[2][0] != "shell": + ms.insert(2, ("shell", "She_ll")) + self.interp = ModifiedInterpreter(self) + if flist is None: + root = Tk() + 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.context_use_ps1 = True + # + text = self.text + text.configure(wrap="char") + text.bind("<>", self.enter_callback) + text.bind("<>", self.linefeed_callback) + text.bind("<>", self.cancel_callback) + text.bind("<>", self.eof_callback) + text.bind("<>", self.open_stack_viewer) + text.bind("<>", self.toggle_debugger) + text.bind("<>", self.toggle_jit_stack_viewer) + if use_subprocess: + text.bind("<>", self.view_restart_mark) + text.bind("<>", self.restart_shell) + # + self.save_stdout = sys.stdout + self.save_stderr = sys.stderr + self.save_stdin = sys.stdin + 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 a textView someday, but must work w/o subproc + pydoc.pager = pydoc.plainpager + except: + sys.stderr = sys.__stderr__ + raise + # + self.history = self.History(self.text) + # + self.pollinterval = 50 # millisec + + def get_standard_extension_names(self): + return idleConf.GetExtensions(shell_only=True) + + reading = False + executing = False + canceled = False + endoffile = False + closing = False + _stop_readline_flag = False + + def set_warning_stream(self, stream): + global warning_stream + warning_stream = stream + + def get_warning_stream(self): + return warning_stream + + def toggle_debugger(self, event=None): + if self.executing: + tkMessageBox.showerror("Don't debug now", + "You can only toggle the debugger when idle", + parent=self.text) + self.set_debugger_indicator() + return "break" + else: + db = self.interp.getdebugger() + if db: + self.close_debugger() + else: + self.open_debugger() + + def set_debugger_indicator(self): + db = self.interp.getdebugger() + self.setvar("<>", not not db) + + def toggle_jit_stack_viewer(self, event=None): + pass # All we need is the variable + + def close_debugger(self): + db = self.interp.getdebugger() + if db: + self.interp.setdebugger(None) + db.close() + if self.interp.rpcclt: + RemoteDebugger.close_remote_debugger(self.interp.rpcclt) + self.resetoutput() + self.console.write("[DEBUG OFF]\n") + sys.ps1 = ">>> " + self.showprompt() + self.set_debugger_indicator() + + def open_debugger(self): + if self.interp.rpcclt: + dbg_gui = RemoteDebugger.start_remote_debugger(self.interp.rpcclt, + self) + else: + dbg_gui = Debugger.Debugger(self) + self.interp.setdebugger(dbg_gui) + dbg_gui.load_breakpoints() + sys.ps1 = "[DEBUG ON]\n>>> " + self.showprompt() + self.set_debugger_indicator() + + def beginexecuting(self): + "Helper for ModifiedInterpreter" + self.resetoutput() + self.executing = 1 + + def endexecuting(self): + "Helper for ModifiedInterpreter" + self.executing = 0 + self.canceled = 0 + self.showprompt() + + def close(self): + "Extend EditorWindow.close()" + if self.executing: + response = tkMessageBox.askokcancel( + "Kill?", + "Your program is still running!\n Do you want to kill it?", + default="ok", + parent=self.text) + if response is False: + return "cancel" + self.stop_readline() + self.canceled = True + self.closing = True + return EditorWindow.close(self) + + def _close(self): + "Extend EditorWindow._close(), shut down debugger and execution server" + self.close_debugger() + if use_subprocess: + self.interp.kill_subprocess() + # Restore std streams + sys.stdout = self.save_stdout + sys.stderr = self.save_stderr + sys.stdin = self.save_stdin + # Break cycles + self.interp = None + self.console = None + self.flist.pyshell = None + self.history = None + EditorWindow._close(self) + + def ispythonsource(self, filename): + "Override EditorWindow method: never remove the colorizer" + return True + + def short_title(self): + return self.shell_title + + COPYRIGHT = \ + 'Type "copyright", "credits" or "license()" for more information.' + + def begin(self): + self.text.mark_set("iomark", "insert") + self.resetoutput() + if use_subprocess: + nosub = '' + client = self.interp.start_subprocess() + if not client: + 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 + + 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? + return True + + def stop_readline(self): + if not self.reading: # no nested mainloop to exit. + return + self._stop_readline_flag = True + self.top.quit() + + def readline(self): + save = self.reading + try: + self.reading = 1 + self.top.mainloop() # nested mainloop() + finally: + self.reading = save + if self._stop_readline_flag: + self._stop_readline_flag = False + return "" + line = self.text.get("iomark", "end-1c") + if len(line) == 0: # may be EOF if we quit our mainloop with Ctrl-C + line = "\n" + self.resetoutput() + if self.canceled: + self.canceled = 0 + if not use_subprocess: + raise KeyboardInterrupt + if self.endoffile: + self.endoffile = 0 + line = "" + return line + + def isatty(self): + return True + + def cancel_callback(self, event=None): + try: + if self.text.compare("sel.first", "!=", "sel.last"): + return # Active selection -- always use default binding + except: + pass + if not (self.executing or self.reading): + self.resetoutput() + self.interp.write("KeyboardInterrupt\n") + self.showprompt() + return "break" + self.endoffile = 0 + self.canceled = 1 + if (self.executing and self.interp.rpcclt): + if self.interp.getdebugger(): + self.interp.restart_subprocess() + else: + self.interp.interrupt_subprocess() + if self.reading: + self.top.quit() # exit the nested mainloop() in readline() + return "break" + + def eof_callback(self, event): + if self.executing and not self.reading: + return # Let the default binding (delete next char) take over + if not (self.text.compare("iomark", "==", "insert") and + self.text.compare("insert", "==", "end-1c")): + return # Let the default binding (delete next char) take over + if not self.executing: + self.resetoutput() + self.close() + else: + self.canceled = 0 + self.endoffile = 1 + self.top.quit() + return "break" + + def linefeed_callback(self, event): + # Insert a linefeed without entering anything (still autoindented) + if self.reading: + self.text.insert("insert", "\n") + self.text.see("insert") + else: + self.newline_and_indent_event(event) + return "break" + + def enter_callback(self, event): + if self.executing and not self.reading: + return # Let the default binding (insert '\n') take over + # If some text is selected, recall the selection + # (but only if this before the I/O mark) + try: + sel = self.text.get("sel.first", "sel.last") + if sel: + if self.text.compare("sel.last", "<=", "iomark"): + self.recall(sel, event) + return "break" + except: + pass + # If we're strictly before the line containing iomark, recall + # the current line, less a leading prompt, less leading or + # trailing whitespace + if self.text.compare("insert", "<", "iomark linestart"): + # Check if there's a relevant stdin range -- if so, use it + prev = self.text.tag_prevrange("stdin", "insert") + if prev and self.text.compare("insert", "<", prev[1]): + self.recall(self.text.get(prev[0], prev[1]), event) + return "break" + next = self.text.tag_nextrange("stdin", "insert") + if next and self.text.compare("insert lineend", ">=", next[0]): + self.recall(self.text.get(next[0], next[1]), event) + return "break" + # No stdin mark -- just get the current line, less any prompt + indices = self.text.tag_nextrange("console", "insert linestart") + if indices and \ + self.text.compare(indices[0], "<=", "insert linestart"): + self.recall(self.text.get(indices[1], "insert lineend"), event) + else: + self.recall(self.text.get("insert linestart", "insert lineend"), event) + return "break" + # If we're between the beginning of the line and the iomark, i.e. + # in the prompt area, move to the end of the prompt + if self.text.compare("insert", "<", "iomark"): + self.text.mark_set("insert", "iomark") + # If we're in the current input and there's only whitespace + # beyond the cursor, erase that whitespace first + s = self.text.get("insert", "end-1c") + if s and not s.strip(): + self.text.delete("insert", "end-1c") + # If we're in the current input before its last line, + # insert a newline right at the insert point + if self.text.compare("insert", "<", "end-1c linestart"): + self.newline_and_indent_event(event) + return "break" + # We're in the last line; append a newline and submit it + self.text.mark_set("insert", "end-1c") + if self.reading: + self.text.insert("insert", "\n") + self.text.see("insert") + else: + self.newline_and_indent_event(event) + self.text.tag_add("stdin", "iomark", "end-1c") + self.text.update_idletasks() + if self.reading: + self.top.quit() # Break out of recursive mainloop() + else: + self.runit() + return "break" + + def recall(self, s, event): + # remove leading and trailing empty or whitespace lines + s = re.sub(r'^\s*\n', '' , s) + s = re.sub(r'\n\s*$', '', s) + lines = s.split('\n') + self.text.undo_block_start() + try: + self.text.tag_remove("sel", "1.0", "end") + self.text.mark_set("insert", "end-1c") + prefix = self.text.get("insert linestart", "insert") + if prefix.rstrip().endswith(':'): + self.newline_and_indent_event(event) + prefix = self.text.get("insert linestart", "insert") + self.text.insert("insert", lines[0].strip()) + if len(lines) > 1: + orig_base_indent = re.search(r'^([ \t]*)', lines[0]).group(0) + new_base_indent = re.search(r'^([ \t]*)', prefix).group(0) + for line in lines[1:]: + if line.startswith(orig_base_indent): + # replace orig base indentation with new indentation + line = new_base_indent + line[len(orig_base_indent):] + self.text.insert('insert', '\n'+line.rstrip()) + finally: + self.text.see("insert") + self.text.undo_block_stop() + + def runit(self): + line = self.text.get("iomark", "end-1c") + # Strip off last newline and surrounding whitespace. + # (To allow you to hit return twice to end a statement.) + i = len(line) + while i > 0 and line[i-1] in " \t": + i = i-1 + if i > 0 and line[i-1] == "\n": + i = i-1 + while i > 0 and line[i-1] in " \t": + i = i-1 + line = line[:i] + self.interp.runsource(line) + + def open_stack_viewer(self, event=None): + if self.interp.rpcclt: + return self.interp.remote_stack_viewer() + try: + sys.last_traceback + except: + tkMessageBox.showerror("No stack trace", + "There is no stack trace yet.\n" + "(sys.last_traceback is not defined)", + parent=self.text) + return + from idlelib.StackViewer import StackBrowser + StackBrowser(self.root, self.flist) + + def view_restart_mark(self, event=None): + self.text.see("iomark") + self.text.see("restart") + + def restart_shell(self, event=None): + "Callback for Run/Restart Shell Cntl-F6" + self.interp.restart_subprocess(with_cwd=True) + + def showprompt(self): + self.resetoutput() + 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 resetoutput(self): + source = self.text.get("iomark", "end-1c") + if self.history: + self.history.store(source) + if self.text.get("end-2c") != "\n": + self.text.insert("end-1c", "\n") + self.text.mark_set("iomark", "end-1c") + self.set_line_and_column() + + def write(self, s, tags=()): + if isinstance(s, str) and len(s) and max(s) > '\uffff': + # Tk doesn't support outputting non-BMP characters + # Let's assume what printed string is not very long, + # find first non-BMP character and construct informative + # UnicodeEncodeError exception. + for start, char in enumerate(s): + if char > '\uffff': + break + raise UnicodeEncodeError("UCS-2", char, start, start+1, + 'Non-BMP character not supported in Tk') + try: + self.text.mark_gravity("iomark", "right") + count = 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. + if self.canceled: + self.canceled = 0 + if not use_subprocess: + raise KeyboardInterrupt + return count + + def rmenu_check_cut(self): + try: + if self.text.compare('sel.first', '<', 'iomark'): + return 'disabled' + except TclError: # no selection, so the index 'sel.first' doesn't exist + return 'disabled' + return super().rmenu_check_cut() + + def rmenu_check_paste(self): + if self.text.compare('insert','<','iomark'): + return 'disabled' + return super().rmenu_check_paste() + +class PseudoFile(io.TextIOBase): + + def __init__(self, shell, tags, encoding=None): + self.shell = shell + self.tags = tags + 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) is not str: + if not isinstance(s, str): + raise TypeError('must be str, not ' + type(s).__name__) + # See issue #19481 + s = str.__str__(s) + 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): + 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() + + +usage_msg = """\ + +USAGE: idle [-deins] [-t title] [file]* + idle [-dns] [-t title] (-c cmd | -r file) [arg]* + 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) + +The following options will override the IDLE 'settings' configuration: + + -e open an edit window + -i open a shell window + +The following options imply -i and will open a shell: + + -c cmd run the command in a shell, or + -r file run script from file + + -d enable the debugger + -s run $IDLESTARTUP or $PYTHONSTARTUP before anything else + -t title set title of shell window + +A default edit window will be bypassed when -c, -r, or - are used. + +[arg]* are passed to the command (-c) or script (-r) in sys.argv[1:]. + +Examples: + +idle + Open an edit window or shell depending on IDLE's configuration. + +idle foo.py foobar.py + Edit the files, also open a shell if configured to start with shell. + +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" + Open a shell window and run the command, passing "-c" in sys.argv[0] + and "foo" in sys.argv[1]. + +idle -d -s -r foo.py "Hello World" + Open a shell window, run a startup script, enable the debugger, and + 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" + Open a shell window, run the script piped in, passing '' in sys.argv[0] + and "foobar" in sys.argv[1]. +""" + +def main(): + global flist, root, use_subprocess + + capture_warnings(True) + use_subprocess = True + enable_shell = False + enable_edit = False + debug = False + cmd = None + script = None + startup = False + try: + opts, args = getopt.getopt(sys.argv[1:], "c:deihnr:st:") + except getopt.error as msg: + print("Error: %s\n%s" % (msg, usage_msg), file=sys.stderr) + sys.exit(2) + for o, a in opts: + if o == '-c': + cmd = a + enable_shell = True + if o == '-d': + debug = True + enable_shell = True + if o == '-e': + enable_edit = True + if o == '-h': + sys.stdout.write(usage_msg) + sys.exit() + 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) + sys.exit() + enable_shell = True + if o == '-s': + startup = True + enable_shell = True + if o == '-t': + PyShell.shell_title = a + enable_shell = True + if args and args[0] == '-': + cmd = sys.stdin.read() + enable_shell = True + # process sys.argv and sys.path: + for i in range(len(sys.path)): + sys.path[i] = os.path.abspath(sys.path[i]) + if args and args[0] == '-': + sys.argv = [''] + args[1:] + elif cmd: + sys.argv = ['-c'] + args + elif script: + sys.argv = [script] + args + elif args: + enable_edit = True + pathx = [] + for filename in args: + pathx.append(os.path.dirname(filename)) + for dir in pathx: + dir = os.path.abspath(dir) + if not dir in sys.path: + sys.path.insert(0, dir) + else: + dir = os.getcwd() + if dir not in sys.path: + sys.path.insert(0, dir) + # check the IDLE settings configuration (but command line overrides) + edit_start = idleConf.GetOption('main', 'General', + 'editor-on-startup', type='bool') + enable_edit = enable_edit or edit_start + enable_shell = enable_shell or not enable_edit + # start editor and/or shell windows: + root = Tk(className="Idle") + + # set application icon + icondir = os.path.join(os.path.dirname(__file__), 'Icons') + if system() == 'Windows': + iconfile = os.path.join(icondir, 'idle.ico') + root.wm_iconbitmap(default=iconfile) + elif TkVersion >= 8.5: + 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(file=iconfile) for iconfile in iconfiles] + root.wm_iconphoto(True, *icons) + + fixwordbreaks(root) + root.withdraw() + flist = PyShellFileList(root) + macosxSupport.setupApp(root, flist) + + if macosxSupport.isAquaTk(): + # There are some screwed up <2> class bindings for text + # widgets defined in Tk which we need to do away with. + # See issue #24801. + root.unbind_class('Text', '') + root.unbind_class('Text', '') + root.unbind_class('Text', '<>') + + if enable_edit: + if not (cmd or script): + for filename in args[:]: + if flist.open(filename) is None: + # filename is a directory actually, disconsider it + args.remove(filename) + if not args: + flist.new() + + if enable_shell: + shell = flist.open_shell() + if not shell: + return # couldn't open shell + 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 + # there are open files. + shell.top.lower() + else: + shell = flist.pyshell + + # Handle remaining options. If any of these are set, enable_shell + # was set also, so shell must be true to reach here. + if debug: + shell.open_debugger() + if startup: + filename = os.environ.get("IDLESTARTUP") or \ + os.environ.get("PYTHONSTARTUP") + if filename and os.path.isfile(filename): + shell.interp.execfile(filename) + if cmd or script: + shell.interp.runcommand("""if 1: + import sys as _sys + _sys.argv = %r + del _sys + \n""" % (sys.argv,)) + if cmd: + shell.interp.execsource(cmd) + elif script: + shell.interp.prepend_syspath(script) + shell.interp.execfile(script) + elif shell: + # If there is a shell window and no cmd or script in progress, + # 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.interp.runcommand("print('%s')" % tkversionwarning) + + while flist.inversedict: # keep IDLE running while files are open. + root.mainloop() + root.destroy() + 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/redirector.py b/Lib/idlelib/redirector.py new file mode 100644 index 0000000..b66be9e --- /dev/null +++ b/Lib/idlelib/redirector.py @@ -0,0 +1,176 @@ +from tkinter import TclError + +class WidgetRedirector: + """Support for redirecting arbitrary widget subcommands. + + Some Tk operations don't normally pass through tkinter. For example, if a + character is inserted into a Text widget by pressing a key, a default Tk + binding to the widget's 'insert' operation is activated, and the Tk library + processes the insert without calling back into tkinter. + + Although a binding to could be made via tkinter, what we really want + to do is to hook the Tk 'insert' operation itself. For one thing, we want + a text.insert call in idle code to have the same effect as a key press. + + When a widget is instantiated, a Tcl command is created whose name is the + same as the pathname widget._w. This command is used to invoke the various + widget operations, e.g. insert (for a Text widget). We are going to hook + this command and provide a facility ('register') to intercept the widget + operation. We will also intercept method calls on the tkinter class + instance that represents the tk widget. + + In IDLE, WidgetRedirector is used in Percolator to intercept Text + commands. The function being registered provides access to the top + of a Percolator chain. At the bottom of the chain is a call to the + original Tk widget operation. + """ + def __init__(self, widget): + '''Initialize attributes and setup redirection. + + _operations: dict mapping operation name to new function. + widget: the widget whose tcl command is to be intercepted. + tk: widget.tk, a convenience attribute, probably not needed. + orig: new name of the original tcl command. + + Since renaming to orig fails with TclError when orig already + exists, only one WidgetDirector can exist for a given widget. + ''' + self._operations = {} + self.widget = widget # widget instance + self.tk = tk = widget.tk # widget's root + w = widget._w # widget's (full) Tk pathname + self.orig = w + "_orig" + # Rename the Tcl command within Tcl: + tk.call("rename", w, self.orig) + # Create a new Tcl command whose name is the widget's pathname, and + # whose action is to dispatch on the operation passed to the widget: + tk.createcommand(w, self.dispatch) + + def __repr__(self): + return "%s(%s<%s>)" % (self.__class__.__name__, + self.widget.__class__.__name__, + self.widget._w) + + def close(self): + "Unregister operations and revert redirection created by .__init__." + for operation in list(self._operations): + self.unregister(operation) + widget = self.widget + tk = widget.tk + w = widget._w + # Restore the original widget Tcl command. + tk.deletecommand(w) + tk.call("rename", self.orig, w) + del self.widget, self.tk # Should not be needed + # if instance is deleted after close, as in Percolator. + + def register(self, operation, function): + '''Return OriginalCommand(operation) after registering function. + + Registration adds an operation: function pair to ._operations. + It also adds a widget function attribute that masks the tkinter + class instance method. Method masking operates independently + from command dispatch. + + If a second function is registered for the same operation, the + first function is replaced in both places. + ''' + self._operations[operation] = function + setattr(self.widget, operation, function) + return OriginalCommand(self, operation) + + def unregister(self, operation): + '''Return the function for the operation, or None. + + Deleting the instance attribute unmasks the class attribute. + ''' + if operation in self._operations: + function = self._operations[operation] + del self._operations[operation] + try: + delattr(self.widget, operation) + except AttributeError: + pass + return function + else: + return None + + def dispatch(self, operation, *args): + '''Callback from Tcl which runs when the widget is referenced. + + If an operation has been registered in self._operations, apply the + associated function to the args passed into Tcl. Otherwise, pass the + operation through to Tk via the original Tcl function. + + 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 ColorDelegator.py. + + ''' + m = self._operations.get(operation) + try: + if m: + return m(*args) + else: + return self.tk.call((self.orig, operation) + args) + except TclError: + return "" + + +class OriginalCommand: + '''Callable for original tk command that has been redirected. + + Returned by .register; can be used in the function registered. + redir = WidgetRedirector(text) + def my_insert(*args): + print("insert", args) + original_insert(*args) + original_insert = redir.register("insert", my_insert) + ''' + + def __init__(self, redir, operation): + '''Create .tk_call and .orig_and_operation for .__call__ method. + + .redir and .operation store the input args for __repr__. + .tk and .orig copy attributes of .redir (probably not needed). + ''' + self.redir = redir + self.operation = operation + self.tk = redir.tk # redundant with self.redir + self.orig = redir.orig # redundant with self.redir + # These two could be deleted after checking recipient code. + self.tk_call = redir.tk.call + self.orig_and_operation = (redir.orig, operation) + + def __repr__(self): + return "%s(%r, %r)" % (self.__class__.__name__, + 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 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) + def my_insert(*args): + print("insert", args) + original_insert(*args) + original_insert = redir.register("insert", my_insert) + root.mainloop() + +if __name__ == "__main__": + import unittest + unittest.main('idlelib.idle_test.test_widgetredir', + verbosity=2, exit=False) + from idlelib.idle_test.htest import run + run(_widget_redirector) diff --git a/Lib/idlelib/replace.py b/Lib/idlelib/replace.py new file mode 100644 index 0000000..f2ea22e --- /dev/null +++ b/Lib/idlelib/replace.py @@ -0,0 +1,241 @@ +"""Replace dialog for IDLE. Inherits SearchDialogBase for GUI. +Uses idlelib.SearchEngine for search capability. +Defines various replace related functions like replace, replace all, +replace+find. +""" +from tkinter import * + +from idlelib import SearchEngine +from idlelib.SearchDialogBase import SearchDialogBase +import re + + +def replace(text): + """Returns a singleton ReplaceDialog instance.The single dialog + saves user entries and preferences across instances.""" + root = text._root() + engine = SearchEngine.get(root) + if not hasattr(engine, "_replacedialog"): + engine._replacedialog = ReplaceDialog(root, engine) + dialog = engine._replacedialog + dialog.open(text) + + +class ReplaceDialog(SearchDialogBase): + + title = "Replace Dialog" + icon = "Replace" + + def __init__(self, root, engine): + SearchDialogBase.__init__(self, root, engine) + self.replvar = StringVar(root) + + def open(self, text): + """Display the replace dialog""" + SearchDialogBase.open(self, text) + try: + first = text.index("sel.first") + except TclError: + first = None + try: + last = text.index("sel.last") + except TclError: + last = None + first = first or text.index("insert") + last = last or first + self.show_hit(first, last) + self.ok = 1 + + def create_entries(self): + """Create label and text entry widgets""" + SearchDialogBase.create_entries(self) + self.replent = self.make_entry("Replace with:", self.replvar)[0] + + def create_command_buttons(self): + 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, 1) + self.make_button("Replace All", self.replace_all) + + def find_it(self, event=None): + self.do_find(0) + + def replace_it(self, event=None): + if self.do_find(self.ok): + self.do_replace() + + def default_command(self, event=None): + "Replace and 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(0) + + def _replace_expand(self, m, repl): + """ Helper function for expanding a regular expression + in the replace field, if needed. """ + if self.engine.isre(): + try: + new = m.expand(repl) + except re.error: + self.engine.report_error(repl, 'Invalid Replace Expression') + new = None + else: + new = repl + + return new + + def replace_all(self, event=None): + """Replace all instances of patvar with replvar in text""" + prog = self.engine.getprog() + if not prog: + return + repl = self.replvar.get() + text = self.text + res = self.engine.search_text(text, prog) + if not res: + text.bell() + return + text.tag_remove("sel", "1.0", "end") + text.tag_remove("hit", "1.0", "end") + line = res[0] + col = res[1].start() + if self.engine.iswrap(): + line = 1 + col = 0 + ok = 1 + first = last = None + # XXX ought to replace circular instead of top-to-bottom when wrapping + text.undo_block_start() + while 1: + res = self.engine.search_forward(text, prog, line, col, 0, ok) + if not res: + break + line, m = res + chars = text.get("%d.0" % line, "%d.0" % (line+1)) + orig = m.group() + new = self._replace_expand(m, repl) + if new is None: + break + i, j = m.span() + first = "%d.%d" % (line, i) + last = "%d.%d" % (line, j) + if new == orig: + text.mark_set("insert", last) + else: + text.mark_set("insert", first) + if first != last: + text.delete(first, last) + if new: + text.insert(first, new) + col = i + len(new) + ok = 0 + text.undo_block_stop() + if first and last: + self.show_hit(first, last) + self.close() + + 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: + 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 = 1 + return True + + def do_replace(self): + prog = self.engine.getprog() + if not prog: + return False + text = self.text + try: + first = pos = text.index("sel.first") + last = text.index("sel.last") + except TclError: + pos = None + if not pos: + first = last = pos = text.index("insert") + 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: + return False + new = self._replace_expand(m, self.replvar.get()) + if new is None: + return False + text.mark_set("insert", first) + text.undo_block_start() + if m.group(): + text.delete(first, last) + if new: + text.insert(first, new) + text.undo_block_stop() + self.show_hit(first, text.index("insert")) + self.ok = 0 + return True + + def show_hit(self, first, last): + """Highlight text from 'first' to 'last'. + 'first', 'last' - Text indices""" + text = self.text + text.mark_set("insert", first) + text.tag_remove("sel", "1.0", "end") + text.tag_add("sel", first, last) + text.tag_remove("hit", "1.0", "end") + if first == last: + text.tag_add("hit", first) + else: + text.tag_add("hit", first, last) + text.see("insert") + text.update_idletasks() + + def close(self, event=None): + SearchDialogBase.close(self, event) + self.text.tag_remove("hit", "1.0", "end") + + +def _replace_dialog(parent): # htest # + """htest wrapper function""" + box = Toplevel(parent) + box.title("Test ReplaceDialog") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + box.geometry("+%d+%d"%(x, y + 150)) + + # mock undo delegator methods + def undo_block_start(): + pass + + def undo_block_stop(): + pass + + text = Text(box, inactiveselectbackground='gray') + 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() + + def show_replace(): + text.tag_add(SEL, "1.0", END) + replace(text) + text.tag_remove(SEL, "1.0", END) + + button = Button(box, text="Replace", command=show_replace) + button.pack() + +if __name__ == '__main__': + import unittest + unittest.main('idlelib.idle_test.test_replacedialog', + verbosity=2, exit=False) + + from idlelib.idle_test.htest import run + run(_replace_dialog) diff --git a/Lib/idlelib/rstrip.py b/Lib/idlelib/rstrip.py new file mode 100644 index 0000000..2ce3c7e --- /dev/null +++ b/Lib/idlelib/rstrip.py @@ -0,0 +1,33 @@ +'Provides "Strip trailing whitespace" under the "Format" menu.' + +class RstripExtension: + + menudefs = [ + ('format', [None, ('Strip trailing whitespace', '<>'), ] ), ] + + def __init__(self, editwin): + self.editwin = editwin + self.editwin.text.bind("<>", 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/runscript.py b/Lib/idlelib/runscript.py new file mode 100644 index 0000000..5cb818d --- /dev/null +++ b/Lib/idlelib/runscript.py @@ -0,0 +1,206 @@ +"""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 tabnanny +import tokenize +import tkinter.messagebox as tkMessageBox +from idlelib import PyShell + +from idlelib.configHandler import idleConf +from idlelib import macosxSupport + +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', '<>'), + ('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('<>', 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): + # 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 "" + 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 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). + self.editwin.text_frame.after(200, + lambda: self.editwin.text_frame.event_generate('<>')) + return 'break' + else: + return self._run_module_event(event) + + 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= + self.editwin._filename_to_unicode(filename)) + dirname = os.path.dirname(filename) + # XXX Too often this discards arguments the user just set... + interp.runcommand("""if 1: + __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' + + 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 new file mode 100644 index 0000000..53576b5 --- /dev/null +++ b/Lib/idlelib/scrolledlist.py @@ -0,0 +1,145 @@ +from tkinter import * +from idlelib import macosxSupport + +class ScrolledList: + + default = "(None)" + + def __init__(self, master, **options): + # Create top frame, with scrollbar and listbox + self.master = master + self.frame = frame = Frame(master) + self.frame.pack(fill="both", expand=1) + self.vbar = vbar = Scrollbar(frame, name="vbar") + self.vbar.pack(side="right", fill="y") + self.listbox = listbox = Listbox(frame, exportselection=0, + background="white") + if options: + listbox.configure(options) + listbox.pack(expand=1, fill="both") + # Tie listbox and scrollbar together + vbar["command"] = listbox.yview + listbox["yscrollcommand"] = vbar.set + # Bind events to the list box + listbox.bind("", self.click_event) + listbox.bind("", self.double_click_event) + if macosxSupport.isAquaTk(): + listbox.bind("", self.popup_event) + listbox.bind("", self.popup_event) + else: + listbox.bind("", self.popup_event) + listbox.bind("", self.up_event) + listbox.bind("", self.down_event) + # Mark as empty + self.clear() + + def close(self): + self.frame.destroy() + + def clear(self): + self.listbox.delete(0, "end") + self.empty = 1 + self.listbox.insert("end", self.default) + + def append(self, item): + if self.empty: + self.listbox.delete(0, "end") + self.empty = 0 + self.listbox.insert("end", str(item)) + + def get(self, index): + return self.listbox.get(index) + + def click_event(self, event): + self.listbox.activate("@%d,%d" % (event.x, event.y)) + index = self.listbox.index("active") + self.select(index) + self.on_select(index) + return "break" + + def double_click_event(self, event): + index = self.listbox.index("active") + self.select(index) + self.on_double(index) + return "break" + + menu = None + + def popup_event(self, event): + if not self.menu: + self.make_menu() + menu = self.menu + self.listbox.activate("@%d,%d" % (event.x, event.y)) + index = self.listbox.index("active") + self.select(index) + menu.tk_popup(event.x_root, event.y_root) + + def make_menu(self): + menu = Menu(self.listbox, tearoff=0) + self.menu = menu + self.fill_menu() + + def up_event(self, event): + index = self.listbox.index("active") + if self.listbox.selection_includes(index): + index = index - 1 + else: + index = self.listbox.size() - 1 + if index < 0: + self.listbox.bell() + else: + self.select(index) + self.on_select(index) + return "break" + + def down_event(self, event): + index = self.listbox.index("active") + if self.listbox.selection_includes(index): + index = index + 1 + else: + index = 0 + if index >= self.listbox.size(): + self.listbox.bell() + else: + self.select(index) + self.on_select(index) + return "break" + + def select(self, index): + self.listbox.focus_set() + self.listbox.activate(index) + self.listbox.selection_clear(0, "end") + self.listbox.selection_set(index) + self.listbox.see(index) + + # Methods to override for specific actions + + def fill_menu(self): + pass + + def on_select(self, index): + pass + + def on_double(self, index): + pass + + +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)) + + scrolled_list = MyScrolledList(root) + for i in range(30): + scrolled_list.append("Item %02d" % i) + + root.mainloop() + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(_scrolled_list) diff --git a/Lib/idlelib/search.py b/Lib/idlelib/search.py new file mode 100644 index 0000000..765d53f --- /dev/null +++ b/Lib/idlelib/search.py @@ -0,0 +1,97 @@ +from tkinter import * + +from idlelib import SearchEngine +from idlelib.SearchDialogBase import SearchDialogBase + +def _setup(text): + "Create or find the singleton SearchDialog instance." + root = text._root() + engine = SearchEngine.get(root) + if not hasattr(engine, "_searchdialog"): + engine._searchdialog = SearchDialog(root, engine) + return engine._searchdialog + +def find(text): + "Handle the editor edit menu item and corresponding event." + pat = text.get("sel.first", "sel.last") + return _setup(text).open(text, pat) # Open is inherited from SDBase. + +def find_again(text): + "Handle the editor edit menu item and corresponding event." + return _setup(text).find_again(text) + +def find_selection(text): + "Handle the editor edit menu item and corresponding event." + 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): # htest # + '''Display search test box.''' + box = Toplevel(parent) + box.title("Test SearchDialog") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + box.geometry("+%d+%d"%(x, y + 150)) + text = Text(box, 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(box, text="Search (selection ignored)", command=show_find) + button.pack() + +if __name__ == '__main__': + import unittest + unittest.main('idlelib.idle_test.test_searchdialog', + verbosity=2, exit=False) + from idlelib.idle_test.htest import run + run(_search_dialog) diff --git a/Lib/idlelib/searchbase.py b/Lib/idlelib/searchbase.py new file mode 100644 index 0000000..5fa84e2 --- /dev/null +++ b/Lib/idlelib/searchbase.py @@ -0,0 +1,184 @@ +'''Define SearchDialogBase used by Search, Replace, and Grep dialogs.''' + +from tkinter import (Toplevel, Frame, Entry, Label, Button, + Checkbutton, Radiobutton) + +class SearchDialogBase: + '''Create most of a 3 or 4 row, 3 column search dialog. + + The left and wide middle column contain: + 1 or 2 labeled text entry lines (make_entry, create_entries); + a row of standard Checkbuttons (make_frame, create_option_buttons), + each of which corresponds to a search engine Variable; + a row of dialog-specific Check/Radiobuttons (create_other_buttons). + + The narrow right column contains command buttons + (make_button, create_command_buttons). + These are bound to functions that execute the command. + + Except for command buttons, this base class is not limited to items + common to all three subclasses. Rather, it is the Find dialog minus + the "Find Next" command, its execution function, and the + default_command attribute needed in create_widgets. The other + dialogs override attributes and methods, the latter to replace and + add widgets. + ''' + + title = "Search Dialog" # replace in subclasses + icon = "Search" + needwrapbutton = 1 # not in Find in Files + + def __init__(self, root, engine): + '''Initialize root, engine, and top attributes. + + top (level widget): set in create_widgets() called from open(). + text (Text searched): set in open(), only used in subclasses(). + ent (ry): created in make_entry() called from create_entry(). + row (of grid): 0 in create_widgets(), +1 in make_entry/frame(). + default_command: set in subclasses, used in create_widgers(). + + title (of dialog): class attribute, override in subclasses. + icon (of dialog): ditto, use unclear if cannot minimize dialog. + ''' + self.root = root + self.engine = engine + self.top = None + + def open(self, text, searchphrase=None): + "Make dialog visible on top of others and ready to use." + self.text = text + if not self.top: + self.create_widgets() + else: + self.top.deiconify() + self.top.tkraise() + if searchphrase: + self.ent.delete(0,"end") + self.ent.insert("end",searchphrase) + self.ent.focus_set() + self.ent.selection_range(0, "end") + self.ent.icursor(0) + self.top.grab_set() + + def close(self, event=None): + "Put dialog away for later use." + if self.top: + self.top.grab_release() + self.top.withdraw() + + def create_widgets(self): + '''Create basic 3 row x 3 col search (find) dialog. + + Other dialogs override subsidiary create_x methods as needed. + Replace and Find-in-Files add another entry row. + ''' + top = Toplevel(self.root) + top.bind("", self.default_command) + top.bind("", self.close) + top.protocol("WM_DELETE_WINDOW", self.close) + top.wm_title(self.title) + top.wm_iconname(self.icon) + self.top = top + + self.row = 0 + self.top.grid_columnconfigure(0, pad=2, weight=0) + self.top.grid_columnconfigure(1, pad=2, minsize=100, weight=100) + + self.create_entries() # row 0 (and maybe 1), cols 0, 1 + self.create_option_buttons() # next row, cols 0, 1 + self.create_other_buttons() # next row, cols 0, 1 + self.create_command_buttons() # col 2, all rows + + def make_entry(self, label_text, var): + '''Return (entry, label), . + + entry - gridded labeled Entry for text entry. + label - Label widget, returned for testing. + ''' + label = Label(self.top, text=label_text) + label.grid(row=self.row, column=0, sticky="nw") + entry = Entry(self.top, textvariable=var, exportselection=0) + entry.grid(row=self.row, column=1, sticky="nwe") + self.row = self.row + 1 + return entry, label + + def create_entries(self): + "Create one or more entry lines with make_entry." + self.ent = self.make_entry("Find:", self.engine.patvar)[0] + + def make_frame(self,labeltext=None): + '''Return (frame, label). + + frame - gridded labeled Frame for option or other buttons. + label - Label widget, returned for testing. + ''' + if labeltext: + label = Label(self.top, text=labeltext) + label.grid(row=self.row, column=0, sticky="nw") + else: + label = '' + frame = Frame(self.top) + frame.grid(row=self.row, column=1, columnspan=1, sticky="nwe") + self.row = self.row + 1 + return frame, label + + def create_option_buttons(self): + '''Return (filled frame, options) for testing. + + Options is a list of SearchEngine booleanvar, label pairs. + A gridded frame from make_frame is filled with a Checkbutton + for each pair, bound to the var, with the corresponding label. + ''' + frame = self.make_frame("Options")[0] + engine = self.engine + options = [(engine.revar, "Regular expression"), + (engine.casevar, "Match case"), + (engine.wordvar, "Whole word")] + if self.needwrapbutton: + options.append((engine.wrapvar, "Wrap around")) + for var, label in options: + btn = Checkbutton(frame, anchor="w", variable=var, text=label) + btn.pack(side="left", fill="both") + if var.get(): + btn.select() + return frame, options + + def create_other_buttons(self): + '''Return (frame, others) for testing. + + Others is a list of value, label pairs. + A gridded frame from make_frame is filled with radio buttons. + ''' + frame = self.make_frame("Direction")[0] + var = self.engine.backvar + others = [(1, 'Up'), (0, 'Down')] + for val, label in others: + btn = Radiobutton(frame, anchor="w", + variable=var, value=val, text=label) + btn.pack(side="left", fill="both") + if var.get() == val: + btn.select() + return frame, others + + def make_button(self, label, command, isdef=0): + "Return command button gridded in command frame." + b = Button(self.buttonframe, + text=label, command=command, + default=isdef and "active" or "normal") + cols,rows=self.buttonframe.grid_size() + b.grid(pady=1,row=rows,column=0,sticky="ew") + self.buttonframe.grid(rowspan=rows+1) + return b + + def create_command_buttons(self): + "Place buttons in vertical command frame gridded on right." + f = self.buttonframe = Frame(self.top) + f.grid(row=0,column=2,padx=2,pady=2,ipadx=2,ipady=2) + + b = self.make_button("close", self.close) + b.lower() + +if __name__ == '__main__': + import unittest + unittest.main( + 'idlelib.idle_test.test_searchdialogbase', verbosity=2) diff --git a/Lib/idlelib/searchengine.py b/Lib/idlelib/searchengine.py new file mode 100644 index 0000000..37883bf --- /dev/null +++ b/Lib/idlelib/searchengine.py @@ -0,0 +1,233 @@ +'''Define SearchEngine for search dialogs.''' +import re +from tkinter import StringVar, BooleanVar, TclError +import tkinter.messagebox as tkMessageBox + +def get(root): + '''Return the singleton SearchEngine instance for the process. + + The single SearchEngine saves settings between dialog instances. + If there is not a SearchEngine already, make one. + ''' + if not hasattr(root, "_searchengine"): + root._searchengine = SearchEngine(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.""" + + def __init__(self, root): + '''Initialize Variables that save search state. + + The dialogs bind these to the UI elements present in the dialogs. + ''' + self.root = root # need for report_error() + self.patvar = StringVar(root, '') # search pattern + self.revar = BooleanVar(root, False) # regular expression? + self.casevar = BooleanVar(root, False) # match case? + self.wordvar = BooleanVar(root, False) # match whole word? + self.wrapvar = BooleanVar(root, True) # wrap around buffer? + self.backvar = BooleanVar(root, False) # search backwards? + + # Access methods + + def getpat(self): + return self.patvar.get() + + def setpat(self, pat): + self.patvar.set(pat) + + def isre(self): + return self.revar.get() + + def iscase(self): + return self.casevar.get() + + def isword(self): + return self.wordvar.get() + + def iswrap(self): + return self.wrapvar.get() + + def isback(self): + return self.backvar.get() + + # Higher level access methods + + def setcookedpat(self, pat): + "Set pattern after escaping if re." + # called only in SearchDialog.py: 66 + if self.isre(): + pat = re.escape(pat) + self.setpat(pat) + + def getcookedpat(self): + pat = self.getpat() + if not self.isre(): # if True, see setcookedpat + pat = re.escape(pat) + if self.isword(): + pat = r"\b%s\b" % pat + return pat + + def getprog(self): + "Return compiled cooked search pattern." + pat = self.getpat() + if not pat: + self.report_error(pat, "Empty regular expression") + return None + pat = self.getcookedpat() + flags = 0 + if not self.iscase(): + flags = flags | re.IGNORECASE + try: + prog = re.compile(pat, flags) + except re.error as what: + args = what.args + msg = args[0] + col = args[1] if len(args) >= 2 else -1 + self.report_error(pat, msg, col) + return None + return prog + + def report_error(self, pat, msg, col=-1): + # Derived class could override this with something fancier + msg = "Error: " + str(msg) + if pat: + msg = msg + "\nPattern: " + str(pat) + if col >= 0: + msg = msg + "\nOffset: " + str(col) + tkMessageBox.showerror("Regular expression error", + msg, master=self.root) + + def search_text(self, text, prog=None, ok=0): + '''Return (lineno, matchobj) or None for forward/backward search. + + This function calls the right function with the right arguments. + It directly return the result of that call. + + Text is a text widget. Prog is a precompiled pattern. + The ok parameter is a bit complicated as it has two effects. + + If there is a selection, the search begin at either end, + depending on the direction setting and ok, with ok meaning that + the search starts with the selection. Otherwise, search begins + at the insert mark. + + To aid progress, the search functions do not return an empty + match at the starting position unless ok is True. + ''' + + if not prog: + prog = self.getprog() + if not prog: + return None # Compilation failed -- stop + wrap = self.wrapvar.get() + first, last = get_selection(text) + if self.isback(): + if ok: + start = last + else: + start = first + line, col = get_line_col(start) + res = self.search_backward(text, prog, line, col, wrap, ok) + else: + if ok: + start = first + else: + start = last + line, col = get_line_col(start) + res = self.search_forward(text, prog, line, col, wrap, ok) + return res + + def search_forward(self, text, prog, line, col, wrap, ok=0): + wrapped = 0 + startline = line + chars = text.get("%d.0" % line, "%d.0" % (line+1)) + while chars: + m = prog.search(chars[:-1], col) + if m: + if ok or m.end() > col: + return line, m + line = line + 1 + if wrapped and line > startline: + break + col = 0 + ok = 1 + chars = text.get("%d.0" % line, "%d.0" % (line+1)) + if not chars and wrap: + wrapped = 1 + wrap = 0 + line = 1 + chars = text.get("1.0", "2.0") + return None + + def search_backward(self, text, prog, line, col, wrap, ok=0): + wrapped = 0 + startline = line + chars = text.get("%d.0" % line, "%d.0" % (line+1)) + while 1: + m = search_reverse(prog, chars[:-1], col) + if m: + if ok or m.start() < col: + return line, m + line = line - 1 + if wrapped and line < startline: + break + ok = 1 + if line <= 0: + if not wrap: + break + wrapped = 1 + wrap = 0 + pos = text.index("end-1c") + line, col = map(int, pos.split(".")) + chars = text.get("%d.0" % line, "%d.0" % (line+1)) + col = len(chars) - 1 + return None + +def search_reverse(prog, chars, col): + '''Search backwards and return an re match object or None. + + This is done by searching forwards until there is no match. + Prog: compiled re object with a search method returning a match. + Chars: line of text, without \\n. + Col: stop index for the search; the limit for match.end(). + ''' + m = prog.search(chars) + if not m: + return None + found = None + i, j = m.span() # m.start(), m.end() == match slice indexes + while i < col and j <= col: + found = m + if i == j: + j = j+1 + m = prog.search(chars, j) + if not m: + break + i, j = m.span() + return found + +def get_selection(text): + '''Return tuple of 'line.col' indexes from selection or insert mark. + ''' + try: + first = text.index("sel.first") + last = text.index("sel.last") + except TclError: + first = last = None + if not first: + first = text.index("insert") + if not last: + last = first + return first, last + +def get_line_col(index): + '''Return (line, col) tuple of ints from 'line.col' string.''' + line, col = map(int, index.split(".")) # Fails on invalid index + return line, col + +if __name__ == "__main__": + 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 new file mode 100644 index 0000000..ccc755c --- /dev/null +++ b/Lib/idlelib/stackviewer.py @@ -0,0 +1,151 @@ +import os +import sys +import linecache +import re +import tkinter as tk + +from idlelib.TreeWidget import TreeNode, TreeItem, ScrolledCanvas +from idlelib.ObjectBrowser import ObjectTreeItem, make_objecttreeitem +from idlelib.PyShell import PyShellFileList + +def StackBrowser(root, flist=None, tb=None, top=None): + if top is None: + top = tk.Toplevel(root) + sc = ScrolledCanvas(top, bg="white", highlightthickness=0) + sc.frame.pack(expand=1, fill="both") + item = StackTreeItem(flist, tb) + node = TreeNode(sc.canvas, None, item) + node.expand() + +class StackTreeItem(TreeItem): + + def __init__(self, flist=None, tb=None): + self.flist = flist + self.stack = self.get_stack(tb) + self.text = self.get_exception() + + def get_stack(self, tb): + if tb is None: + tb = sys.last_traceback + stack = [] + if tb and tb.tb_frame is None: + tb = tb.tb_next + while tb is not None: + stack.append((tb.tb_frame, tb.tb_lineno)) + tb = tb.tb_next + return stack + + def get_exception(self): + type = sys.last_type + value = sys.last_value + if hasattr(type, "__name__"): + type = type.__name__ + s = str(type) + if value is not None: + s = s + ": " + str(value) + return s + + def GetText(self): + return self.text + + def GetSubList(self): + sublist = [] + for info in self.stack: + item = FrameTreeItem(info, self.flist) + sublist.append(item) + return sublist + +class FrameTreeItem(TreeItem): + + def __init__(self, info, flist): + self.info = info + self.flist = flist + + def GetText(self): + frame, lineno = self.info + try: + modname = frame.f_globals["__name__"] + except: + modname = "?" + code = frame.f_code + filename = code.co_filename + funcname = code.co_name + sourceline = linecache.getline(filename, lineno) + sourceline = sourceline.strip() + if funcname in ("?", "", None): + item = "%s, line %d: %s" % (modname, lineno, sourceline) + else: + item = "%s.%s(...), line %d: %s" % (modname, funcname, + lineno, sourceline) + return item + + def GetSubList(self): + frame, lineno = self.info + sublist = [] + if frame.f_globals is not frame.f_locals: + item = VariablesTreeItem("", frame.f_locals, self.flist) + sublist.append(item) + item = VariablesTreeItem("", frame.f_globals, self.flist) + sublist.append(item) + return sublist + + def OnDoubleClick(self): + if self.flist: + frame, lineno = self.info + filename = frame.f_code.co_filename + if os.path.isfile(filename): + self.flist.gotofileline(filename, lineno) + +class VariablesTreeItem(ObjectTreeItem): + + def GetText(self): + return self.labeltext + + def GetLabelText(self): + return None + + def IsExpandable(self): + return len(self.object) > 0 + + def GetSubList(self): + sublist = [] + for key in self.object.keys(): + try: + value = self.object[key] + except KeyError: + continue + def setfunction(value, key=key, object=self.object): + object[key] = value + item = make_objecttreeitem(key + " =", value, setfunction) + sublist.append(item) + return sublist + + def keys(self): # unused, left for possible 3rd party use + return list(self.object.keys()) + +def _stack_viewer(parent): + root = tk.Tk() + root.title("Test StackViewer") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) + flist = PyShellFileList(root) + try: # to obtain a traceback object + intentional_name_error + except NameError: + exc_type, exc_value, exc_tb = sys.exc_info() + + # inject stack trace to sys + sys.last_type = exc_type + sys.last_value = exc_value + sys.last_traceback = exc_tb + + StackBrowser(root, flist=flist, top=root, tb=exc_tb) + + # restore sys to original state + del sys.last_type + del sys.last_value + del sys.last_traceback + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(_stack_viewer) diff --git a/Lib/idlelib/statusbar.py b/Lib/idlelib/statusbar.py new file mode 100644 index 0000000..e82ba9a --- /dev/null +++ b/Lib/idlelib/statusbar.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/textView.py b/Lib/idlelib/textView.py deleted file mode 100644 index 01b2d8f..0000000 --- a/Lib/idlelib/textView.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Simple text browser for IDLE - -""" - -from tkinter import * -import tkinter.messagebox as 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('',self.Ok) #dismiss dialog - self.bind('',self.Ok) #dismiss dialog - self.textView.insert(0.0, text) - self.textView.config(state=DISABLED) - - if 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): - 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: - with open(filename, 'r', encoding=encoding) as file: - contents = file.read() - except IOError: - tkMessageBox.showerror(title='File Load Error', - message='Unable to load file %r .' % filename, - parent=parent) - else: - return view_text(parent, title, contents, modal) - -if __name__ == '__main__': - 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 new file mode 100644 index 0000000..01b2d8f --- /dev/null +++ b/Lib/idlelib/textview.py @@ -0,0 +1,86 @@ +"""Simple text browser for IDLE + +""" + +from tkinter import * +import tkinter.messagebox as 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('',self.Ok) #dismiss dialog + self.bind('',self.Ok) #dismiss dialog + self.textView.insert(0.0, text) + self.textView.config(state=DISABLED) + + if 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): + 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: + with open(filename, 'r', encoding=encoding) as file: + contents = file.read() + except IOError: + tkMessageBox.showerror(title='File Load Error', + message='Unable to load file %r .' % filename, + parent=parent) + else: + return view_text(parent, title, contents, modal) + +if __name__ == '__main__': + 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/tooltip.py b/Lib/idlelib/tooltip.py new file mode 100644 index 0000000..964107e --- /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("", self.enter) + self._id2 = self.button.bind("", self.leave) + self._id3 = self.button.bind("", 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/tree.py new file mode 100644 index 0000000..a19578f --- /dev/null +++ b/Lib/idlelib/tree.py @@ -0,0 +1,466 @@ +# XXX TO DO: +# - popup menu +# - support partial or total redisplay +# - key bindings (instead of quick-n-dirty bindings on Canvas): +# - up/down arrow keys to move focus around +# - ditto for page up/down, home/end +# - left/right arrows to expand/collapse & move out/in +# - more doc strings +# - add icons for "file", "module", "class", "method"; better "python" icon +# - callback for selection??? +# - multiple-item selection +# - tooltips +# - redo geometry without magic numbers +# - keep track of object ids to allow more careful cleaning +# - optimize tree redraw after expand of subnode + +import os +from tkinter import * + +from idlelib import ZoomHeight +from idlelib.configHandler import idleConf + +ICONDIR = "Icons" + +# Look for Icons subdirectory in the same directory as this module +try: + _icondir = os.path.join(os.path.dirname(__file__), ICONDIR) +except NameError: + _icondir = ICONDIR +if os.path.isdir(_icondir): + ICONDIR = _icondir +elif not os.path.isdir(ICONDIR): + raise RuntimeError("can't find icon directory (%r)" % (ICONDIR,)) + +def listicons(icondir=ICONDIR): + """Utility to display the available icons.""" + root = Tk() + import glob + list = glob.glob(os.path.join(icondir, "*.gif")) + list.sort() + images = [] + row = column = 0 + for file in list: + name = os.path.splitext(os.path.basename(file))[0] + image = PhotoImage(file=file, master=root) + images.append(image) + label = Label(root, image=image, bd=1, relief="raised") + label.grid(row=row, column=column) + label = Label(root, text=name) + label.grid(row=row+1, column=column) + column = column + 1 + if column >= 10: + row = row+2 + column = 0 + root.images = images + + +class TreeNode: + + def __init__(self, canvas, parent, item): + self.canvas = canvas + self.parent = parent + self.item = item + self.state = 'collapsed' + self.selected = False + self.children = [] + self.x = self.y = None + self.iconimages = {} # cache of PhotoImage instances for icons + + def destroy(self): + for c in self.children[:]: + self.children.remove(c) + c.destroy() + self.parent = None + + def geticonimage(self, name): + try: + return self.iconimages[name] + except KeyError: + pass + file, ext = os.path.splitext(name) + ext = ext or ".gif" + fullname = os.path.join(ICONDIR, file + ext) + image = PhotoImage(master=self.canvas, file=fullname) + self.iconimages[name] = image + return image + + def select(self, event=None): + if self.selected: + return + self.deselectall() + self.selected = True + self.canvas.delete(self.image_id) + self.drawicon() + self.drawtext() + + def deselect(self, event=None): + if not self.selected: + return + self.selected = False + self.canvas.delete(self.image_id) + self.drawicon() + self.drawtext() + + def deselectall(self): + if self.parent: + self.parent.deselectall() + else: + self.deselecttree() + + def deselecttree(self): + if self.selected: + self.deselect() + for child in self.children: + child.deselecttree() + + def flip(self, event=None): + if self.state == 'expanded': + self.collapse() + else: + self.expand() + self.item.OnDoubleClick() + return "break" + + def expand(self, event=None): + if not self.item._IsExpandable(): + return + if self.state != 'expanded': + self.state = 'expanded' + self.update() + self.view() + + def collapse(self, event=None): + if self.state != 'collapsed': + self.state = 'collapsed' + self.update() + + def view(self): + top = self.y - 2 + bottom = self.lastvisiblechild().y + 17 + height = bottom - top + visible_top = self.canvas.canvasy(0) + visible_height = self.canvas.winfo_height() + visible_bottom = self.canvas.canvasy(visible_height) + if visible_top <= top and bottom <= visible_bottom: + return + x0, y0, x1, y1 = self.canvas._getints(self.canvas['scrollregion']) + if top >= visible_top and height <= visible_height: + fraction = top + height - visible_height + else: + fraction = top + fraction = float(fraction) / y1 + self.canvas.yview_moveto(fraction) + + def lastvisiblechild(self): + if self.children and self.state == 'expanded': + return self.children[-1].lastvisiblechild() + else: + return self + + def update(self): + if self.parent: + self.parent.update() + else: + oldcursor = self.canvas['cursor'] + self.canvas['cursor'] = "watch" + self.canvas.update() + self.canvas.delete(ALL) # XXX could be more subtle + self.draw(7, 2) + x0, y0, x1, y1 = self.canvas.bbox(ALL) + self.canvas.configure(scrollregion=(0, 0, x1, y1)) + self.canvas['cursor'] = oldcursor + + def draw(self, x, y): + # XXX This hard-codes too many geometry constants! + dy = 20 + self.x, self.y = x, y + self.drawicon() + self.drawtext() + if self.state != 'expanded': + return y + dy + # draw children + if not self.children: + sublist = self.item._GetSubList() + if not sublist: + # _IsExpandable() was mistaken; that's allowed + return y+17 + for item in sublist: + child = self.__class__(self.canvas, self, item) + self.children.append(child) + cx = x+20 + cy = y + dy + cylast = 0 + for child in self.children: + cylast = cy + self.canvas.create_line(x+9, cy+7, cx, cy+7, fill="gray50") + cy = child.draw(cx, cy) + if child.item._IsExpandable(): + if child.state == 'expanded': + iconname = "minusnode" + callback = child.collapse + else: + iconname = "plusnode" + callback = child.expand + image = self.geticonimage(iconname) + id = self.canvas.create_image(x+9, cylast+7, image=image) + # XXX This leaks bindings until canvas is deleted: + self.canvas.tag_bind(id, "<1>", callback) + self.canvas.tag_bind(id, "", lambda x: None) + id = self.canvas.create_line(x+9, y+10, x+9, cylast+7, + ##stipple="gray50", # XXX Seems broken in Tk 8.0.x + fill="gray50") + self.canvas.tag_lower(id) # XXX .lower(id) before Python 1.5.2 + return cy + + def drawicon(self): + if self.selected: + imagename = (self.item.GetSelectedIconName() or + self.item.GetIconName() or + "openfolder") + else: + imagename = self.item.GetIconName() or "folder" + image = self.geticonimage(imagename) + id = self.canvas.create_image(self.x, self.y, anchor="nw", image=image) + self.image_id = id + self.canvas.tag_bind(id, "<1>", self.select) + self.canvas.tag_bind(id, "", self.flip) + + def drawtext(self): + textx = self.x+20-1 + texty = self.y-4 + labeltext = self.item.GetLabelText() + if labeltext: + id = self.canvas.create_text(textx, texty, anchor="nw", + text=labeltext) + self.canvas.tag_bind(id, "<1>", self.select) + self.canvas.tag_bind(id, "", self.flip) + x0, y0, x1, y1 = self.canvas.bbox(id) + textx = max(x1, 200) + 10 + text = self.item.GetText() or "" + try: + self.entry + except AttributeError: + pass + else: + self.edit_finish() + try: + self.label + except AttributeError: + # padding carefully selected (on Windows) to match Entry widget: + self.label = Label(self.canvas, text=text, bd=0, padx=2, pady=2) + theme = idleConf.CurrentTheme() + if self.selected: + self.label.configure(idleConf.GetHighlight(theme, 'hilite')) + else: + self.label.configure(idleConf.GetHighlight(theme, 'normal')) + id = self.canvas.create_window(textx, texty, + anchor="nw", window=self.label) + self.label.bind("<1>", self.select_or_edit) + self.label.bind("", self.flip) + self.text_id = id + + def select_or_edit(self, event=None): + if self.selected and self.item.IsEditable(): + self.edit(event) + else: + self.select(event) + + def edit(self, event=None): + self.entry = Entry(self.label, bd=0, highlightthickness=1, width=0) + self.entry.insert(0, self.label['text']) + self.entry.selection_range(0, END) + self.entry.pack(ipadx=5) + self.entry.focus_set() + self.entry.bind("", self.edit_finish) + self.entry.bind("", self.edit_cancel) + + def edit_finish(self, event=None): + try: + entry = self.entry + del self.entry + except AttributeError: + return + text = entry.get() + entry.destroy() + if text and text != self.item.GetText(): + self.item.SetText(text) + text = self.item.GetText() + self.label['text'] = text + self.drawtext() + self.canvas.focus_set() + + def edit_cancel(self, event=None): + try: + entry = self.entry + del self.entry + except AttributeError: + return + entry.destroy() + self.drawtext() + self.canvas.focus_set() + + +class TreeItem: + + """Abstract class representing tree items. + + Methods should typically be overridden, otherwise a default action + is used. + + """ + + def __init__(self): + """Constructor. Do whatever you need to do.""" + + def GetText(self): + """Return text string to display.""" + + def GetLabelText(self): + """Return label text string to display in front of text (if any).""" + + expandable = None + + def _IsExpandable(self): + """Do not override! Called by TreeNode.""" + if self.expandable is None: + self.expandable = self.IsExpandable() + return self.expandable + + def IsExpandable(self): + """Return whether there are subitems.""" + return 1 + + def _GetSubList(self): + """Do not override! Called by TreeNode.""" + if not self.IsExpandable(): + return [] + sublist = self.GetSubList() + if not sublist: + self.expandable = 0 + return sublist + + def IsEditable(self): + """Return whether the item's text may be edited.""" + + def SetText(self, text): + """Change the item's text (if it is editable).""" + + def GetIconName(self): + """Return name of icon to be displayed normally.""" + + def GetSelectedIconName(self): + """Return name of icon to be displayed when selected.""" + + def GetSubList(self): + """Return list of items forming sublist.""" + + def OnDoubleClick(self): + """Called on a double-click on the item.""" + + +# Example application + +class FileTreeItem(TreeItem): + + """Example TreeItem subclass -- browse the file system.""" + + def __init__(self, path): + self.path = path + + def GetText(self): + return os.path.basename(self.path) or self.path + + def IsEditable(self): + return os.path.basename(self.path) != "" + + def SetText(self, text): + newpath = os.path.dirname(self.path) + newpath = os.path.join(newpath, text) + if os.path.dirname(newpath) != os.path.dirname(self.path): + return + try: + os.rename(self.path, newpath) + self.path = newpath + except OSError: + pass + + def GetIconName(self): + if not self.IsExpandable(): + return "python" # XXX wish there was a "file" icon + + def IsExpandable(self): + return os.path.isdir(self.path) + + def GetSubList(self): + try: + names = os.listdir(self.path) + except OSError: + return [] + names.sort(key = os.path.normcase) + sublist = [] + for name in names: + item = FileTreeItem(os.path.join(self.path, name)) + sublist.append(item) + return sublist + + +# 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 + self.master = master + self.frame = Frame(master) + self.frame.rowconfigure(0, weight=1) + self.frame.columnconfigure(0, weight=1) + self.canvas = Canvas(self.frame, **opts) + self.canvas.grid(row=0, column=0, sticky="nsew") + self.vbar = Scrollbar(self.frame, name="vbar") + self.vbar.grid(row=0, column=1, sticky="nse") + self.hbar = Scrollbar(self.frame, name="hbar", orient="horizontal") + self.hbar.grid(row=1, column=0, sticky="ews") + self.canvas['yscrollcommand'] = self.vbar.set + self.vbar['command'] = self.canvas.yview + self.canvas['xscrollcommand'] = self.hbar.set + self.hbar['command'] = self.canvas.xview + self.canvas.bind("", self.page_up) + self.canvas.bind("", self.page_down) + self.canvas.bind("", self.unit_up) + self.canvas.bind("", self.unit_down) + #if isinstance(master, Toplevel) or isinstance(master, Tk): + self.canvas.bind("", self.zoom_height) + self.canvas.focus_set() + def page_up(self, event): + self.canvas.yview_scroll(-1, "page") + return "break" + def page_down(self, event): + self.canvas.yview_scroll(1, "page") + return "break" + def unit_up(self, event): + self.canvas.yview_scroll(-1, "unit") + return "break" + def unit_down(self, event): + self.canvas.yview_scroll(1, "unit") + return "break" + def zoom_height(self, event): + ZoomHeight.zoom_height(self.master) + return "break" + + +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(os.getcwd()) + node = TreeNode(sc.canvas, None, item) + node.expand() + root.mainloop() + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(_tree_widget) diff --git a/Lib/idlelib/undo.py b/Lib/idlelib/undo.py new file mode 100644 index 0000000..1c2502d --- /dev/null +++ b/Lib/idlelib/undo.py @@ -0,0 +1,368 @@ +import string +from tkinter import * + +from idlelib.Delegator import Delegator + +#$ event <> +#$ win +#$ unix + +#$ event <> +#$ win +#$ unix + +#$ event <> +#$ win +#$ unix + + +class UndoDelegator(Delegator): + + max_undo = 1000 + + def __init__(self): + Delegator.__init__(self) + self.reset_undo() + + def setdelegate(self, delegate): + if self.delegate is not None: + self.unbind("<>") + self.unbind("<>") + self.unbind("<>") + Delegator.setdelegate(self, delegate) + if delegate is not None: + self.bind("<>", self.undo_event) + self.bind("<>", self.redo_event) + self.bind("<>", self.dump_event) + + 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()) + pprint(self.undolist[self.pointer:]) + return "break" + + def reset_undo(self): + self.was_saved = -1 + self.pointer = 0 + self.undolist = [] + self.undoblock = 0 # or a CommandSequence instance + self.set_saved(1) + + def set_saved(self, flag): + if flag: + self.saved = self.pointer + else: + self.saved = -1 + self.can_merge = False + self.check_saved() + + def get_saved(self): + return self.saved == self.pointer + + saved_change_hook = None + + def set_saved_change_hook(self, hook): + self.saved_change_hook = hook + + was_saved = -1 + + def check_saved(self): + is_saved = self.get_saved() + if is_saved != self.was_saved: + self.was_saved = is_saved + if self.saved_change_hook: + self.saved_change_hook() + + def insert(self, index, chars, tags=None): + self.addcmd(InsertCommand(index, chars, tags)) + + def delete(self, index1, index2=None): + self.addcmd(DeleteCommand(index1, index2)) + + # Clients should call undo_block_start() and undo_block_stop() + # around a sequence of editing cmds to be treated as a unit by + # undo & redo. Nested matching calls are OK, and the inner calls + # then act like nops. OK too if no editing cmds, or only one + # editing cmd, is issued in between: if no cmds, the whole + # sequence has no effect; and if only one cmd, that cmd is entered + # directly into the undo list, as if undo_block_xxx hadn't been + # called. The intent of all that is to make this scheme easy + # to use: all the client has to worry about is making sure each + # _start() call is matched by a _stop() call. + + def undo_block_start(self): + if self.undoblock == 0: + self.undoblock = CommandSequence() + self.undoblock.bump_depth() + + def undo_block_stop(self): + if self.undoblock.bump_depth(-1) == 0: + cmd = self.undoblock + self.undoblock = 0 + if len(cmd) > 0: + if len(cmd) == 1: + # no need to wrap a single cmd + cmd = cmd.getcmd(0) + # this blk of cmds, or single cmd, has already + # been done, so don't execute it again + self.addcmd(cmd, 0) + + def addcmd(self, cmd, execute=True): + if execute: + cmd.do(self.delegate) + if self.undoblock != 0: + self.undoblock.append(cmd) + return + if self.can_merge and self.pointer > 0: + lastcmd = self.undolist[self.pointer-1] + if lastcmd.merge(cmd): + return + self.undolist[self.pointer:] = [cmd] + if self.saved > self.pointer: + self.saved = -1 + self.pointer = self.pointer + 1 + if len(self.undolist) > self.max_undo: + ##print "truncating undo list" + del self.undolist[0] + self.pointer = self.pointer - 1 + if self.saved >= 0: + self.saved = self.saved - 1 + self.can_merge = True + self.check_saved() + + def undo_event(self, event): + if self.pointer == 0: + self.bell() + return "break" + cmd = self.undolist[self.pointer - 1] + cmd.undo(self.delegate) + self.pointer = self.pointer - 1 + self.can_merge = False + self.check_saved() + return "break" + + def redo_event(self, event): + if self.pointer >= len(self.undolist): + self.bell() + return "break" + cmd = self.undolist[self.pointer] + cmd.redo(self.delegate) + self.pointer = self.pointer + 1 + self.can_merge = False + self.check_saved() + return "break" + + +class Command: + + # Base class for Undoable commands + + tags = None + + def __init__(self, index1, index2, chars, tags=None): + self.marks_before = {} + self.marks_after = {} + self.index1 = index1 + self.index2 = index2 + self.chars = chars + if tags: + self.tags = tags + + def __repr__(self): + s = self.__class__.__name__ + t = (self.index1, self.index2, self.chars, self.tags) + if self.tags is None: + t = t[:-1] + return s + repr(t) + + def do(self, text): + pass + + def redo(self, text): + pass + + def undo(self, text): + pass + + def merge(self, cmd): + return 0 + + def save_marks(self, text): + marks = {} + for name in text.mark_names(): + if name != "insert" and name != "current": + marks[name] = text.index(name) + return marks + + def set_marks(self, text, marks): + for name, index in marks.items(): + text.mark_set(name, index) + + +class InsertCommand(Command): + + # Undoable insert command + + def __init__(self, index1, chars, tags=None): + Command.__init__(self, index1, None, chars, tags) + + def do(self, text): + self.marks_before = self.save_marks(text) + self.index1 = text.index(self.index1) + if text.compare(self.index1, ">", "end-1c"): + # Insert before the final newline + self.index1 = text.index("end-1c") + text.insert(self.index1, self.chars, self.tags) + self.index2 = text.index("%s+%dc" % (self.index1, len(self.chars))) + self.marks_after = self.save_marks(text) + ##sys.__stderr__.write("do: %s\n" % self) + + def redo(self, text): + text.mark_set('insert', self.index1) + text.insert(self.index1, self.chars, self.tags) + self.set_marks(text, self.marks_after) + text.see('insert') + ##sys.__stderr__.write("redo: %s\n" % self) + + def undo(self, text): + text.mark_set('insert', self.index1) + text.delete(self.index1, self.index2) + self.set_marks(text, self.marks_before) + text.see('insert') + ##sys.__stderr__.write("undo: %s\n" % self) + + def merge(self, cmd): + if self.__class__ is not cmd.__class__: + return False + if self.index2 != cmd.index1: + return False + if self.tags != cmd.tags: + return False + if len(cmd.chars) != 1: + return False + if self.chars and \ + self.classify(self.chars[-1]) != self.classify(cmd.chars): + return False + self.index2 = cmd.index2 + self.chars = self.chars + cmd.chars + return True + + alphanumeric = string.ascii_letters + string.digits + "_" + + def classify(self, c): + if c in self.alphanumeric: + return "alphanumeric" + if c == "\n": + return "newline" + return "punctuation" + + +class DeleteCommand(Command): + + # Undoable delete command + + def __init__(self, index1, index2=None): + Command.__init__(self, index1, index2, None, None) + + def do(self, text): + self.marks_before = self.save_marks(text) + self.index1 = text.index(self.index1) + if self.index2: + self.index2 = text.index(self.index2) + else: + self.index2 = text.index(self.index1 + " +1c") + if text.compare(self.index2, ">", "end-1c"): + # Don't delete the final newline + self.index2 = text.index("end-1c") + self.chars = text.get(self.index1, self.index2) + text.delete(self.index1, self.index2) + self.marks_after = self.save_marks(text) + ##sys.__stderr__.write("do: %s\n" % self) + + def redo(self, text): + text.mark_set('insert', self.index1) + text.delete(self.index1, self.index2) + self.set_marks(text, self.marks_after) + text.see('insert') + ##sys.__stderr__.write("redo: %s\n" % self) + + def undo(self, text): + text.mark_set('insert', self.index1) + text.insert(self.index1, self.chars) + self.set_marks(text, self.marks_before) + 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 + + def __init__(self): + self.cmds = [] + self.depth = 0 + + def __repr__(self): + s = self.__class__.__name__ + strs = [] + for cmd in self.cmds: + strs.append(" %r" % (cmd,)) + return s + "(\n" + ",\n".join(strs) + "\n)" + + def __len__(self): + return len(self.cmds) + + def append(self, cmd): + self.cmds.append(cmd) + + def getcmd(self, i): + return self.cmds[i] + + def redo(self, text): + for cmd in self.cmds: + cmd.redo(text) + + def undo(self, text): + cmds = self.cmds[:] + cmds.reverse() + for cmd in cmds: + cmd.undo(text) + + def bump_depth(self, incr=1): + self.depth = self.depth + incr + return self.depth + + +def _undo_delegator(parent): # htest # + import re + import tkinter as tk + from idlelib.Percolator import Percolator + undowin = tk.Toplevel() + undowin.title("Test UndoDelegator") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + undowin.geometry("+%d+%d"%(x, y + 150)) + + text = Text(undowin, 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.pack(side='left') + redo = Button(undowin, text="Redo", command=lambda:d.redo_event(None)) + redo.pack(side='left') + dump = Button(undowin, text="Dump", command=lambda:d.dump_event(None)) + dump.pack(side='left') + +if __name__ == "__main__": + import unittest + unittest.main('idlelib.idle_test.test_undodelegator', verbosity=2, + exit=False) + from idlelib.idle_test.htest import run + run(_undo_delegator) diff --git a/Lib/idlelib/windows.py b/Lib/idlelib/windows.py new file mode 100644 index 0000000..bc74348 --- /dev/null +++ b/Lib/idlelib/windows.py @@ -0,0 +1,90 @@ +from tkinter import * + +class WindowList: + + def __init__(self): + self.dict = {} + self.callbacks = [] + + def add(self, window): + window.after_idle(self.call_callbacks) + self.dict[str(window)] = window + + def delete(self, window): + try: + del self.dict[str(window)] + except KeyError: + # Sometimes, destroy() is called twice + pass + self.call_callbacks() + + def add_windows_to_menu(self, menu): + list = [] + for key in self.dict: + window = self.dict[key] + try: + title = window.get_title() + except TclError: + continue + list.append((title, key, window)) + list.sort() + for title, key, window in list: + menu.add_command(label=title, command=window.wakeup) + + def register_callback(self, callback): + self.callbacks.append(callback) + + def unregister_callback(self, callback): + try: + self.callbacks.remove(callback) + except ValueError: + pass + + def call_callbacks(self): + for callback in self.callbacks: + try: + callback() + except: + t, v, tb = sys.exc_info() + print("warning: callback failed in WindowList", t, ":", v) + +registry = WindowList() + +add_windows_to_menu = registry.add_windows_to_menu +register_callback = registry.register_callback +unregister_callback = registry.unregister_callback + + +class ListedToplevel(Toplevel): + + def __init__(self, master, **kw): + Toplevel.__init__(self, master, kw) + registry.add(self) + self.focused_widget = self + + def destroy(self): + registry.delete(self) + Toplevel.destroy(self) + # If this is Idle's last window then quit the mainloop + # (Needed for clean exit on Windows 98) + if not registry.dict: + self.quit() + + def update_windowlist_registry(self, window): + registry.call_callbacks() + + def get_title(self): + # Subclass can override + return self.wm_title() + + def wakeup(self): + try: + if self.wm_state() == "iconic": + self.wm_withdraw() + self.wm_deiconify() + self.tkraise() + self.focused_widget.focus_set() + except TclError: + # This can happen when the window menu was torn off. + # Simply ignore it. + pass 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', '<>'), + ]) + ] + + 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) -- cgit v0.12 From 6fa5bdc6e85ec48925bc0d856b134f59d01c300f Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Sat, 28 May 2016 13:22:31 -0400 Subject: Issue #24225: Within idlelib files, update idlelib module names. This follows the previous patch that changed idlelib file names. Class names that matched old module names are not changed. Change idlelib imports in turtledemo.__main__. Exception: config-extensions.def. Previously, extension section names, file names, and class names had to match. Changing section names would create cross-version conflicts in config-extensions.cfg (user customizations). Instead map old names to new file names at point of import in editor.EditorWindow.load_extension. Patch extensively tested with test_idle, idle_test.htest.py, a custom import-all test, running IDLE in a console to catch messages, and testing each menu item. Based on a patch by Al Sweigart. --- Lib/idlelib/__main__.py | 4 +- Lib/idlelib/autocomplete.py | 10 +-- Lib/idlelib/autocomplete_w.py | 10 +-- Lib/idlelib/browser.py | 16 ++-- Lib/idlelib/calltip_w.py | 4 +- Lib/idlelib/calltips.py | 8 +- Lib/idlelib/codecontext.py | 6 +- Lib/idlelib/colorizer.py | 6 +- Lib/idlelib/config.py | 10 +-- Lib/idlelib/config_sec.py | 2 +- Lib/idlelib/configdialog.py | 16 ++-- Lib/idlelib/debugger.py | 16 ++-- Lib/idlelib/debugger_r.py | 8 +- Lib/idlelib/debugobj.py | 4 +- Lib/idlelib/editor.py | 129 ++++++++++++++++------------- Lib/idlelib/filelist.py | 4 +- Lib/idlelib/grep.py | 10 +-- Lib/idlelib/help.py | 4 +- Lib/idlelib/help_about.py | 6 +- Lib/idlelib/history.py | 4 +- Lib/idlelib/hyperparser.py | 4 +- Lib/idlelib/idle.py | 4 +- Lib/idlelib/idle.pyw | 12 +-- Lib/idlelib/idle_test/htest.py | 66 +++++++-------- Lib/idlelib/idle_test/mock_idle.py | 4 +- Lib/idlelib/idle_test/test_autocomplete.py | 6 +- Lib/idlelib/idle_test/test_autoexpand.py | 4 +- Lib/idlelib/idle_test/test_calltips.py | 2 +- Lib/idlelib/idle_test/test_config_help.py | 4 +- Lib/idlelib/idle_test/test_config_name.py | 75 ----------------- Lib/idlelib/idle_test/test_config_sec.py | 75 +++++++++++++++++ Lib/idlelib/idle_test/test_configdialog.py | 6 +- Lib/idlelib/idle_test/test_delegator.py | 2 +- Lib/idlelib/idle_test/test_editor.py | 2 +- Lib/idlelib/idle_test/test_grep.py | 6 +- Lib/idlelib/idle_test/test_history.py | 4 +- Lib/idlelib/idle_test/test_hyperparser.py | 6 +- Lib/idlelib/idle_test/test_iomenu.py | 2 +- Lib/idlelib/idle_test/test_paragraph.py | 8 +- Lib/idlelib/idle_test/test_parenmatch.py | 4 +- Lib/idlelib/idle_test/test_pathbrowser.py | 8 +- Lib/idlelib/idle_test/test_percolator.py | 4 +- Lib/idlelib/idle_test/test_redirector.py | 4 +- Lib/idlelib/idle_test/test_replace.py | 6 +- Lib/idlelib/idle_test/test_rstrip.py | 4 +- Lib/idlelib/idle_test/test_search.py | 6 +- Lib/idlelib/idle_test/test_searchbase.py | 6 +- Lib/idlelib/idle_test/test_searchengine.py | 4 +- Lib/idlelib/idle_test/test_textview.py | 4 +- Lib/idlelib/idle_test/test_undo.py | 6 +- Lib/idlelib/idle_test/test_warning.py | 6 +- Lib/idlelib/iomenu.py | 5 +- Lib/idlelib/macosx.py | 40 ++++----- Lib/idlelib/mainmenu.py | 4 +- Lib/idlelib/outwin.py | 6 +- Lib/idlelib/paragraph.py | 4 +- Lib/idlelib/parenmatch.py | 4 +- Lib/idlelib/pathbrowser.py | 6 +- Lib/idlelib/percolator.py | 4 +- Lib/idlelib/pyshell.py | 68 +++++++-------- Lib/idlelib/redirector.py | 2 +- Lib/idlelib/replace.py | 8 +- Lib/idlelib/run.py | 42 +++++----- Lib/idlelib/runscript.py | 16 ++-- Lib/idlelib/scrolledlist.py | 4 +- Lib/idlelib/search.py | 6 +- Lib/idlelib/searchbase.py | 2 +- Lib/idlelib/searchengine.py | 2 +- Lib/idlelib/stackviewer.py | 6 +- Lib/idlelib/tooltip.py | 4 +- Lib/idlelib/tree.py | 6 +- Lib/idlelib/undo.py | 4 +- Lib/idlelib/zoomheight.py | 4 +- Lib/turtledemo/__main__.py | 6 +- 74 files changed, 451 insertions(+), 433 deletions(-) delete mode 100644 Lib/idlelib/idle_test/test_config_name.py create mode 100644 Lib/idlelib/idle_test/test_config_sec.py diff --git a/Lib/idlelib/__main__.py b/Lib/idlelib/__main__.py index 2edf5f7..6349ec7 100644 --- a/Lib/idlelib/__main__.py +++ b/Lib/idlelib/__main__.py @@ -3,6 +3,6 @@ IDLE main entry point Run IDLE as python -m idlelib """ -import idlelib.PyShell -idlelib.PyShell.main() +import idlelib.pyshell +idlelib.pyshell.main() # This file does not work for 2.7; See issue 24212. diff --git a/Lib/idlelib/autocomplete.py b/Lib/idlelib/autocomplete.py index b9ec539..6e75f67 100644 --- a/Lib/idlelib/autocomplete.py +++ b/Lib/idlelib/autocomplete.py @@ -1,4 +1,4 @@ -"""AutoComplete.py - An IDLE extension for automatically completing names. +"""autocomplete.py - An IDLE extension for automatically completing names. This extension can complete either attribute names of file names. It can pop a window with all available names, for the user to select from. @@ -7,7 +7,7 @@ import os import sys import string -from idlelib.configHandler import idleConf +from idlelib.config import idleConf # This string includes all chars that may be in an identifier ID_CHARS = string.ascii_letters + string.digits + "_" @@ -15,8 +15,8 @@ ID_CHARS = string.ascii_letters + string.digits + "_" # 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 +from idlelib import autocomplete_w +from idlelib.hyperparser import HyperParser import __main__ @@ -49,7 +49,7 @@ class AutoComplete: self._delayed_completion_index = None def _make_autocomplete_window(self): - return AutoCompleteWindow.AutoCompleteWindow(self.text) + return autocomplete_w.AutoCompleteWindow(self.text) def _remove_autocomplete_window(self, event=None): if self.autocompletewindow: diff --git a/Lib/idlelib/autocomplete_w.py b/Lib/idlelib/autocomplete_w.py index 2ee6878..c66b3df 100644 --- a/Lib/idlelib/autocomplete_w.py +++ b/Lib/idlelib/autocomplete_w.py @@ -1,9 +1,9 @@ """ -An auto-completion window for IDLE, used by the AutoComplete extension +An auto-completion window for IDLE, used by the autocomplete extension """ from tkinter import * -from idlelib.MultiCall import MC_SHIFT -from idlelib.AutoComplete import COMPLETE_FILES, COMPLETE_ATTRIBUTES +from idlelib.multicall import MC_SHIFT +from idlelib.autocomplete import COMPLETE_FILES, COMPLETE_ATTRIBUTES HIDE_VIRTUAL_EVENT_NAME = "<>" HIDE_SEQUENCES = ("", "") @@ -34,8 +34,8 @@ class AutoCompleteWindow: self.completions = None # A list with more completions, or None self.morecompletions = None - # The completion mode. Either AutoComplete.COMPLETE_ATTRIBUTES or - # AutoComplete.COMPLETE_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 diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py index d09c52f..9968333 100644 --- a/Lib/idlelib/browser.py +++ b/Lib/idlelib/browser.py @@ -14,13 +14,13 @@ 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 +from idlelib import pyshell +from idlelib.windows import ListedToplevel +from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas +from idlelib.config import idleConf file_open = None # Method...Item and Class...Item use this. -# Normally PyShell.flist.open, but there is no PyShell.flist for htest. +# Normally pyshell.flist.open, but there is no pyshell.flist for htest. class ClassBrowser: @@ -32,7 +32,7 @@ class ClassBrowser: """ global file_open if not _htest: - file_open = PyShell.flist.open + file_open = pyshell.flist.open self.name = name self.file = os.path.join(path[0], self.name + ".py") self._htest = _htest @@ -95,7 +95,7 @@ class ModuleBrowserTreeItem(TreeItem): return if not os.path.exists(self.file): return - PyShell.flist.open(self.file) + pyshell.flist.open(self.file) def IsExpandable(self): return os.path.normcase(self.file[-3:]) == ".py" @@ -226,7 +226,7 @@ def _class_browser(parent): #Wrapper for htest file = sys.argv[0] dir, file = os.path.split(file) name = os.path.splitext(file)[0] - flist = PyShell.PyShellFileList(parent) + flist = pyshell.PyShellFileList(parent) global file_open file_open = flist.open ClassBrowser(flist, name, [dir], _htest=True) diff --git a/Lib/idlelib/calltip_w.py b/Lib/idlelib/calltip_w.py index 8e68a76..9f6cdc1 100644 --- a/Lib/idlelib/calltip_w.py +++ b/Lib/idlelib/calltip_w.py @@ -1,7 +1,7 @@ """A CallTip window class for Tkinter/IDLE. -After ToolTip.py, which uses ideas gleaned from PySol -Used by the CallTips IDLE extension. +After tooltip.py, which uses ideas gleaned from PySol +Used by the calltips IDLE extension. """ from tkinter import Toplevel, Label, LEFT, SOLID, TclError diff --git a/Lib/idlelib/calltips.py b/Lib/idlelib/calltips.py index 81bd5f1..3a9b1c6 100644 --- a/Lib/idlelib/calltips.py +++ b/Lib/idlelib/calltips.py @@ -1,4 +1,4 @@ -"""CallTips.py - An IDLE Extension to Jog Your Memory +"""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 @@ -12,8 +12,8 @@ import sys import textwrap import types -from idlelib import CallTipWindow -from idlelib.HyperParser import HyperParser +from idlelib import calltip_w +from idlelib.hyperparser import HyperParser class CallTips: @@ -37,7 +37,7 @@ class CallTips: def _make_tk_calltip_window(self): # See __init__ for usage - return CallTipWindow.CallTip(self.text) + return calltip_w.CallTip(self.text) def _remove_calltip_window(self, event=None): if self.active_calltip: diff --git a/Lib/idlelib/codecontext.py b/Lib/idlelib/codecontext.py index 7d25ada..2a21a1f 100644 --- a/Lib/idlelib/codecontext.py +++ b/Lib/idlelib/codecontext.py @@ -1,11 +1,11 @@ -"""CodeContext - Extension to display the block context above the edit window +"""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 +variable in the codecontext section of config-extensions.def. Lines which do not open blocks are not shown in the context hints pane. """ @@ -13,7 +13,7 @@ import tkinter from tkinter.constants import TOP, LEFT, X, W, SUNKEN import re from sys import maxsize as INFINITY -from idlelib.configHandler import idleConf +from idlelib.config import idleConf BLOCKOPENERS = {"class", "def", "elif", "else", "except", "finally", "for", "if", "try", "while", "with"} diff --git a/Lib/idlelib/colorizer.py b/Lib/idlelib/colorizer.py index 9f31349..ca365f7 100644 --- a/Lib/idlelib/colorizer.py +++ b/Lib/idlelib/colorizer.py @@ -2,8 +2,8 @@ import time import re import keyword import builtins -from idlelib.Delegator import Delegator -from idlelib.configHandler import idleConf +from idlelib.delegator import Delegator +from idlelib.config import idleConf DEBUG = False @@ -235,7 +235,7 @@ class ColorDelegator(Delegator): def _color_delegator(parent): # htest # from tkinter import Toplevel, Text - from idlelib.Percolator import Percolator + from idlelib.percolator import Percolator top = Toplevel(parent) top.title("Test ColorDelegator") diff --git a/Lib/idlelib/config.py b/Lib/idlelib/config.py index 8ac1f60..b9e1c6d 100644 --- a/Lib/idlelib/config.py +++ b/Lib/idlelib/config.py @@ -7,7 +7,7 @@ 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. +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 @@ -230,7 +230,7 @@ class IdleConf: return self.userCfg[configType].Get(section, option, type=type, raw=raw) except ValueError: - warning = ('\n Warning: configHandler.py - IdleConf.GetOption -\n' + warning = ('\n Warning: config.py - IdleConf.GetOption -\n' ' invalid %r value for configuration option %r\n' ' from section %r: %r' % (type, option, section, @@ -247,7 +247,7 @@ class IdleConf: pass #returning default, print warning if warn_on_default: - warning = ('\n Warning: configHandler.py - IdleConf.GetOption -\n' + warning = ('\n Warning: config.py - IdleConf.GetOption -\n' ' problem retrieving configuration option %r\n' ' from section %r.\n' ' returning default value: %r' % @@ -358,7 +358,7 @@ class IdleConf: for element in theme: if not cfgParser.has_option(themeName, element): # Print warning that will return a default color - warning = ('\n Warning: configHandler.IdleConf.GetThemeDict' + warning = ('\n Warning: config.IdleConf.GetThemeDict' ' -\n problem retrieving theme element %r' '\n from theme %r.\n' ' returning default color: %r' % @@ -644,7 +644,7 @@ class IdleConf: if binding: keyBindings[event] = binding else: #we are going to return a default, print warning - warning=('\n Warning: configHandler.py - IdleConf.GetCoreKeys' + 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' % diff --git a/Lib/idlelib/config_sec.py b/Lib/idlelib/config_sec.py index 5137836..7b59124 100644 --- a/Lib/idlelib/config_sec.py +++ b/Lib/idlelib/config_sec.py @@ -1,7 +1,7 @@ """ 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, +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 * diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index b702253..b58806e 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -14,14 +14,14 @@ import tkinter.messagebox as tkMessageBox import tkinter.colorchooser as tkColorChooser import tkinter.font as 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.config import idleConf +from idlelib.dynoption import DynOptionMenu +from idlelib.config_key import GetKeysDialog +from idlelib.config_sec import GetCfgSectionNameDialog +from idlelib.config_help import GetHelpSourceDialog from idlelib.tabbedpages import TabbedPageSet -from idlelib.textView import view_text -from idlelib import macosxSupport +from idlelib.textview import view_text +from idlelib import macosx class ConfigDialog(Toplevel): @@ -91,7 +91,7 @@ class ConfigDialog(Toplevel): self.create_action_buttons().pack(side=BOTTOM) def create_action_buttons(self): - if macosxSupport.isAquaTk(): + if macosx.isAquaTk(): # Changing the default padding on OSX results in unreadable # text in the buttons paddingArgs = {} diff --git a/Lib/idlelib/debugger.py b/Lib/idlelib/debugger.py index d5e217d..9af626c 100644 --- a/Lib/idlelib/debugger.py +++ b/Lib/idlelib/debugger.py @@ -1,9 +1,9 @@ import os import bdb from tkinter import * -from idlelib.WindowList import ListedToplevel -from idlelib.ScrolledList import ScrolledList -from idlelib import macosxSupport +from idlelib.windows import ListedToplevel +from idlelib.scrolledlist import ScrolledList +from idlelib import macosx class Idb(bdb.Bdb): @@ -34,8 +34,10 @@ class Idb(bdb.Bdb): return True else: prev_frame = frame.f_back - if prev_frame.f_code.co_filename.count('Debugger.py'): - # (that test will catch both Debugger.py and RemoteDebugger.py) + 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 return False return self.in_rpc_code(prev_frame) @@ -370,7 +372,7 @@ class Debugger: class StackViewer(ScrolledList): def __init__(self, master, flist, gui): - if macosxSupport.isAquaTk(): + if macosx.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. @@ -502,7 +504,7 @@ class NamespaceViewer: # # 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 RemoteDebugger.DictProxy. + # dict[1], dict[2], etc from the debugger_r.DictProxy. ### keys_list = dict.keys() names = sorted(keys_list) diff --git a/Lib/idlelib/debugger_r.py b/Lib/idlelib/debugger_r.py index be2262f..bc97127 100644 --- a/Lib/idlelib/debugger_r.py +++ b/Lib/idlelib/debugger_r.py @@ -21,7 +21,7 @@ barrier, in particular frame and traceback objects. """ import types -from idlelib import Debugger +from idlelib import debugger debugging = 0 @@ -187,7 +187,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 @@ -362,7 +362,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 +373,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) diff --git a/Lib/idlelib/debugobj.py b/Lib/idlelib/debugobj.py index 7b57aa4..4016c03 100644 --- a/Lib/idlelib/debugobj.py +++ b/Lib/idlelib/debugobj.py @@ -11,7 +11,7 @@ import re -from idlelib.TreeWidget import TreeItem, TreeNode, ScrolledCanvas +from idlelib.tree import TreeItem, TreeNode, ScrolledCanvas from reprlib import Repr @@ -126,7 +126,7 @@ def _object_browser(parent): import sys from tkinter import Tk root = Tk() - root.title("Test ObjectBrowser") + root.title("Test debug object browser") 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") diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index b5868be..3aa5278 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -12,15 +12,15 @@ import tkinter.messagebox as tkMessageBox import traceback import webbrowser -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.multicall import MultiCallCreator +from idlelib import windows +from idlelib import search +from idlelib import grep +from idlelib import replace +from idlelib import pyparse +from idlelib.config import idleConf +from idlelib import help_about, textview, configdialog +from idlelib import macosx from idlelib import help # The default tab setting for a Text widget, in average-width characters. @@ -67,7 +67,7 @@ class HelpDialog(object): 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) + self.dlg = dlg = textview.view_file(parent,'Help',fn, modal=False) dlg.bind('', self.destroy, '+') def nearwindow(self, near): @@ -89,13 +89,13 @@ helpDialog = HelpDialog() # singleton instance, no longer used class EditorWindow(object): - 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 idlelib.percolator import Percolator + from idlelib.colorizer import ColorDelegator + from idlelib.undo import UndoDelegator + from idlelib.iomenu import IOBinding, filesystemencoding, encoding + from idlelib import mainmenu from tkinter import Toplevel - from idlelib.MultiStatusBar import MultiStatusBar + from idlelib.statusbar import MultiStatusBar help_url = None @@ -136,11 +136,11 @@ class EditorWindow(object): except AttributeError: sys.ps1 = '>>> ' self.menubar = Menu(root) - self.top = top = WindowList.ListedToplevel(root, menu=self.menubar) + self.top = top = windows.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 @@ -173,7 +173,7 @@ class EditorWindow(object): self.top.protocol("WM_DELETE_WINDOW", self.close) self.top.bind("<>", self.close_event) - if macosxSupport.isAquaTk(): + if macosx.isAquaTk(): # Command-W on editorwindows doesn't work without this. text.bind('<>', self.close_event) # Some OS X systems have only one mouse button, so use @@ -309,7 +309,7 @@ class EditorWindow(object): menu.add_separator() end = end + 1 self.wmenu_end = end - WindowList.register_callback(self.postwindowsmenu) + windows.register_callback(self.postwindowsmenu) # Some abstractions so IDLE extensions are cross-IDE self.askyesno = tkMessageBox.askyesno @@ -418,7 +418,7 @@ 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 macosxSupport.isCarbonTk(): + if macosx.isCarbonTk(): # Insert the application menu menudict['application'] = menu = Menu(mbar, name='apple', tearoff=0) @@ -439,7 +439,7 @@ class EditorWindow(object): end = -1 if end > self.wmenu_end: menu.delete(self.wmenu_end+1, end) - WindowList.add_windows_to_menu(menu) + windows.add_windows_to_menu(menu) rmenu = None @@ -507,17 +507,17 @@ class EditorWindow(object): def about_dialog(self, event=None): "Handle Help 'About IDLE' event." - # Synchronize with macosxSupport.overrideRootMenu.about_dialog. - aboutDialog.AboutDialog(self.top,'About IDLE') + # Synchronize with macosx.overrideRootMenu.about_dialog. + help_about.AboutDialog(self.top,'About IDLE') def config_dialog(self, event=None): "Handle Options 'Configure IDLE' event." - # Synchronize with macosxSupport.overrideRootMenu.config_dialog. - configDialog.ConfigDialog(self.top,'Settings') + # Synchronize with macosx.overrideRootMenu.config_dialog. + configdialog.ConfigDialog(self.top,'Settings') def help_dialog(self, event=None): "Handle Help 'IDLE Help' event." - # Synchronize with macosxSupport.overrideRootMenu.help_dialog. + # Synchronize with macosx.overrideRootMenu.help_dialog. if self.root: parent = self.root else: @@ -590,23 +590,23 @@ class EditorWindow(object): return "break" def find_event(self, event): - SearchDialog.find(self.text) + search.find(self.text) return "break" def find_again_event(self, event): - SearchDialog.find_again(self.text) + search.find_again(self.text) return "break" def find_selection_event(self, event): - SearchDialog.find_selection(self.text) + search.find_selection(self.text) return "break" def find_in_files_event(self, event): - GrepDialog.grep(self.text, self.io, self.flist) + grep.grep(self.text, self.io, self.flist) return "break" def replace_event(self, event): - ReplaceDialog.replace(self.text) + replace.replace(self.text) return "break" def goto_line_event(self, event): @@ -673,12 +673,12 @@ class EditorWindow(object): return head, tail = os.path.split(filename) base, ext = os.path.splitext(tail) - from idlelib import ClassBrowser - ClassBrowser.ClassBrowser(self.flist, base, [head]) + from idlelib import browser + browser.ClassBrowser(self.flist, base, [head]) def open_path_browser(self, event=None): - from idlelib import PathBrowser - PathBrowser.PathBrowser(self.flist) + from idlelib import pathbrowser + pathbrowser.PathBrowser(self.flist) def open_turtle_demo(self, event = None): import subprocess @@ -739,7 +739,7 @@ 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() theme = idleConf.CurrentTheme() @@ -772,14 +772,14 @@ class EditorWindow(object): def ResetFont(self): "Update the text widgets' font if it is changed" - # Called from configDialog.py + # 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.Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet() + # Called from configdialog.py + self.mainmenu.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(): @@ -790,8 +790,8 @@ class EditorWindow(object): def ApplyKeybindings(self): "Update the keybindings after they are changed" - # Called from configDialog.py - self.Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet() + # Called from configdialog.py + self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet() self.apply_bindings() for extensionName in self.get_standard_extension_names(): xkeydefs = idleConf.GetExtensionBindings(extensionName) @@ -799,7 +799,7 @@ class EditorWindow(object): self.apply_bindings(xkeydefs) #update menu accelerators menuEventDict = {} - for menu in self.Bindings.menudefs: + for menu in self.mainmenu.menudefs: menuEventDict[menu[0]] = {} for item in menu[1]: if item: @@ -826,7 +826,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') @@ -1006,7 +1006,7 @@ class EditorWindow(object): def _close(self): if self.io.filename: self.update_recent_files_list(new_file=self.io.filename) - WindowList.unregister_callback(self.postwindowsmenu) + windows.unregister_callback(self.postwindowsmenu) self.unload_extensions() self.io.close() self.io = None @@ -1044,12 +1044,25 @@ class EditorWindow(object): def get_standard_extension_names(self): return idleConf.GetExtensions(editor_only=True) + extfiles = { # map config-extension section names to new file names + 'AutoComplete': 'autocomplete', + 'AutoExpand': 'autoexpand', + 'CallTips': 'calltips', + 'CodeContext': 'codecontext', + 'FormatParagraph': 'paragraph', + 'ParenMatch': 'parenmatch', + 'RstripExtension': 'rstrip', + 'ScriptBinding': 'runscript', + 'ZoomHeight': 'zoomheight', + } + def load_extension(self, name): + fname = self.extfiles.get(name, name) try: try: - mod = importlib.import_module('.' + name, package=__package__) + mod = importlib.import_module('.' + fname, package=__package__) except (ImportError, TypeError): - mod = importlib.import_module(name) + mod = importlib.import_module(fname) except ImportError: print("\nFailed to import extension: ", name) raise @@ -1073,7 +1086,7 @@ class EditorWindow(object): def apply_bindings(self, keydefs=None): if keydefs is None: - keydefs = self.Bindings.default_keydefs + keydefs = self.mainmenu.default_keydefs text = self.text text.keydefs = keydefs for event, keylist in keydefs.items(): @@ -1086,9 +1099,9 @@ class EditorWindow(object): Menus that are absent or None in self.menudict are ignored. """ if menudefs is None: - menudefs = self.Bindings.menudefs + menudefs = self.mainmenu.menudefs if keydefs is None: - keydefs = self.Bindings.default_keydefs + keydefs = self.mainmenu.default_keydefs menudict = self.menudict text = self.text for mname, entrylist in menudefs: @@ -1315,7 +1328,7 @@ 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) + 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) @@ -1339,22 +1352,22 @@ class EditorWindow(object): 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 @@ -1657,7 +1670,7 @@ def get_accelerator(keydefs, eventname): keylist = keydefs.get(eventname) # issue10940: temporary workaround to prevent hang with OS X Cocoa Tk 8.5 # if not keylist: - if (not keylist) or (macosxSupport.isCocoaTk() and eventname in { + if (not keylist) or (macosx.isCocoaTk() and eventname in { "<>", "<>", "<>"}): @@ -1692,7 +1705,7 @@ def _editor_window(parent): # htest # filename = sys.argv[1] else: filename = None - macosxSupport.setupApp(root, None) + macosx.setupApp(root, None) edit = EditorWindow(root=root, filename=filename) edit.text.bind("<>", edit.close_event) # Does not stop error, neither does following diff --git a/Lib/idlelib/filelist.py b/Lib/idlelib/filelist.py index a9989a8..b5af90cc 100644 --- a/Lib/idlelib/filelist.py +++ b/Lib/idlelib/filelist.py @@ -6,7 +6,7 @@ import tkinter.messagebox as tkMessageBox class FileList: # N.B. this import overridden in PyShellFileList. - from idlelib.EditorWindow import EditorWindow + from idlelib.editor import EditorWindow def __init__(self, root): self.root = root @@ -111,7 +111,7 @@ class FileList: def _test(): - from idlelib.EditorWindow import fixwordbreaks + from idlelib.editor import fixwordbreaks import sys root = Tk() fixwordbreaks(root) diff --git a/Lib/idlelib/grep.py b/Lib/idlelib/grep.py index 721b231..28132a8 100644 --- a/Lib/idlelib/grep.py +++ b/Lib/idlelib/grep.py @@ -4,14 +4,14 @@ 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 +from idlelib import searchengine +from idlelib.searchbase 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) + engine = searchengine.get(root) if not hasattr(engine, "_grepdialog"): engine._grepdialog = GrepDialog(root, engine, flist) dialog = engine._grepdialog @@ -67,7 +67,7 @@ class GrepDialog(SearchDialogBase): if not path: self.top.bell() return - from idlelib.OutputWindow import OutputWindow # leave here! + from idlelib.outwin import OutputWindow # leave here! save = sys.stdout try: sys.stdout = OutputWindow(self.flist) @@ -131,7 +131,7 @@ class GrepDialog(SearchDialogBase): def _grep_dialog(parent): # htest # - from idlelib.PyShell import PyShellFileList + from idlelib.pyshell import PyShellFileList root = Tk() root.title("Test GrepDialog") width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) diff --git a/Lib/idlelib/help.py b/Lib/idlelib/help.py index 4cb8c76..0093015 100644 --- a/Lib/idlelib/help.py +++ b/Lib/idlelib/help.py @@ -4,7 +4,7 @@ Contents are subject to revision at any time, without notice. Help => About IDLE: diplay About Idle dialog - + Help => IDLE Help: Display help.html with proper formatting. @@ -28,7 +28,7 @@ from html.parser import HTMLParser from os.path import abspath, dirname, isfile, join from tkinter import Toplevel, Frame, Text, Scrollbar, Menu, Menubutton from tkinter import font as tkfont -from idlelib.configHandler import idleConf +from idlelib.config import idleConf use_ttk = False # until available to import if use_ttk: diff --git a/Lib/idlelib/help_about.py b/Lib/idlelib/help_about.py index 3112e6a..4b6c528 100644 --- a/Lib/idlelib/help_about.py +++ b/Lib/idlelib/help_about.py @@ -5,7 +5,7 @@ import os from sys import version from tkinter import * -from idlelib import textView +from idlelib import textview class AboutDialog(Toplevel): """Modal about dialog for idle @@ -135,11 +135,11 @@ class AboutDialog(Toplevel): def display_printer_text(self, title, printer): printer._Printer__setup() text = '\n'.join(printer._Printer__lines) - textView.view_text(self, title, text) + 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) + textview.view_file(self, title, fn, encoding) def Ok(self, event=None): self.destroy() diff --git a/Lib/idlelib/history.py b/Lib/idlelib/history.py index 078af29..6068d4f 100644 --- a/Lib/idlelib/history.py +++ b/Lib/idlelib/history.py @@ -1,11 +1,11 @@ "Implement Idle Shell history mechanism with History class" -from idlelib.configHandler import idleConf +from idlelib.config 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 <> event (default Alt-N). history_prev - Bound to <> event (default Alt-P). diff --git a/Lib/idlelib/hyperparser.py b/Lib/idlelib/hyperparser.py index 77cb057..f904a39 100644 --- a/Lib/idlelib/hyperparser.py +++ b/Lib/idlelib/hyperparser.py @@ -7,7 +7,7 @@ the structure of code. import string from keyword import iskeyword -from idlelib import PyParse +from idlelib import pyparse # all ASCII chars that may be in an identifier @@ -30,7 +30,7 @@ class HyperParser: 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)) diff --git a/Lib/idlelib/idle.py b/Lib/idlelib/idle.py index a249557..c01cf99 100644 --- a/Lib/idlelib/idle.py +++ b/Lib/idlelib/idle.py @@ -7,5 +7,5 @@ import sys idlelib_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, idlelib_dir) -import idlelib.PyShell -idlelib.PyShell.main() +import idlelib.pyshell +idlelib.pyshell.main() diff --git a/Lib/idlelib/idle.pyw b/Lib/idlelib/idle.pyw index 142cb32..e73c049 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: + from . 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/htest.py b/Lib/idlelib/idle_test/htest.py index 58e62cb..4675645 100644 --- a/Lib/idlelib/idle_test/htest.py +++ b/Lib/idlelib/idle_test/htest.py @@ -59,19 +59,19 @@ msg: master window hints about testing the widget. Modules and classes not being tested at the moment: -PyShell.PyShellEditorWindow -Debugger.Debugger -AutoCompleteWindow.AutoCompleteWindow -OutputWindow.OutputWindow (indirectly being tested with grep test) +pyshell.PyShellEditorWindow +debugger.Debugger +autocomplete_w.AutoCompleteWindow +outwin.OutputWindow (indirectly being tested with grep test) ''' from importlib import import_module -from idlelib.macosxSupport import _initializeTkVariantTests +from idlelib.macosx import _initializeTkVariantTests import tkinter as tk AboutDialog_spec = { - 'file': 'aboutDialog', - 'kwds': {'title': 'aboutDialog test', + 'file': 'help_about', + 'kwds': {'title': 'help_about test', '_htest': True, }, 'msg': "Test every button. Ensure Python, TK and IDLE versions " @@ -79,14 +79,14 @@ AboutDialog_spec = { } _calltip_window_spec = { - 'file': 'CallTipWindow', + 'file': 'calltip_w', 'kwds': {}, 'msg': "Typing '(' should display a calltip.\n" "Typing ') should hide the calltip.\n" } _class_browser_spec = { - 'file': 'ClassBrowser', + 'file': 'browser', 'kwds': {}, 'msg': "Inspect names of module, class(with superclass if " "applicable), methods and functions.\nToggle nested items.\n" @@ -95,7 +95,7 @@ _class_browser_spec = { } _color_delegator_spec = { - 'file': 'ColorDelegator', + 'file': 'colorizer', 'kwds': {}, 'msg': "The text is sample Python code.\n" "Ensure components like comments, keywords, builtins,\n" @@ -104,7 +104,7 @@ _color_delegator_spec = { } ConfigDialog_spec = { - 'file': 'configDialog', + 'file': 'configdialog', 'kwds': {'title': 'ConfigDialogTest', '_htest': True,}, 'msg': "IDLE preferences dialog.\n" @@ -121,7 +121,7 @@ ConfigDialog_spec = { # TODO Improve message _dyn_option_menu_spec = { - 'file': 'dynOptionMenuWidget', + 'file': 'dynoption', 'kwds': {}, 'msg': "Select one of the many options in the 'old option set'.\n" "Click the button to change the option set.\n" @@ -130,14 +130,14 @@ _dyn_option_menu_spec = { # TODO edit wrapper _editor_window_spec = { - 'file': 'EditorWindow', + 'file': 'editor', 'kwds': {}, 'msg': "Test editor functions of interest.\n" "Best to close editor first." } GetCfgSectionNameDialog_spec = { - 'file': 'configSectionNameDialog', + 'file': 'config_sec', 'kwds': {'title':'Get Name', 'message':'Enter something', 'used_names': {'abc'}, @@ -149,7 +149,7 @@ GetCfgSectionNameDialog_spec = { } GetHelpSourceDialog_spec = { - 'file': 'configHelpSourceEdit', + 'file': 'config_help', 'kwds': {'title': 'Get helpsource', '_htest': True}, 'msg': "Enter menu item name and help file path\n " @@ -162,7 +162,7 @@ GetHelpSourceDialog_spec = { # Update once issue21519 is resolved. GetKeysDialog_spec = { - 'file': 'keybindingDialog', + 'file': 'config_key', 'kwds': {'title': 'Test keybindings', 'action': 'find-again', 'currentKeySequences': [''] , @@ -177,7 +177,7 @@ GetKeysDialog_spec = { } _grep_dialog_spec = { - 'file': 'GrepDialog', + 'file': 'grep', 'kwds': {}, 'msg': "Click the 'Show GrepDialog' button.\n" "Test the various 'Find-in-files' functions.\n" @@ -187,7 +187,7 @@ _grep_dialog_spec = { } _io_binding_spec = { - 'file': 'IOBinding', + 'file': 'iomenu', 'kwds': {}, 'msg': "Test the following bindings.\n" " to open file from dialog.\n" @@ -200,7 +200,7 @@ _io_binding_spec = { } _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, " @@ -210,14 +210,14 @@ _multi_call_spec = { } _multistatus_bar_spec = { - 'file': 'MultiStatusBar', + 'file': 'statusbar', 'kwds': {}, 'msg': "Ensure presence of multi-status bar below text area.\n" "Click 'Update Status' to change the multi-status text" } _object_browser_spec = { - 'file': 'ObjectBrowser', + 'file': 'debugobj', 'kwds': {}, 'msg': "Double click on items upto the lowest level.\n" "Attributes of the objects and related information " @@ -225,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" @@ -234,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" @@ -245,7 +245,7 @@ _percolator_spec = { } _replace_dialog_spec = { - 'file': 'ReplaceDialog', + 'file': 'replace', 'kwds': {}, 'msg': "Click the 'Replace' button.\n" "Test various replace options in the 'Replace dialog'.\n" @@ -253,7 +253,7 @@ _replace_dialog_spec = { } _search_dialog_spec = { - 'file': 'SearchDialog', + 'file': 'search', 'kwds': {}, 'msg': "Click the 'Search' button.\n" "Test various search options in the 'Search dialog'.\n" @@ -261,7 +261,7 @@ _search_dialog_spec = { } _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 " @@ -277,7 +277,7 @@ show_idlehelp_spec = { } _stack_viewer_spec = { - 'file': 'StackViewer', + 'file': 'stackviewer', 'kwds': {}, 'msg': "A stacktrace for a NameError exception.\n" "Expand 'idlelib ...' and ''.\n" @@ -295,8 +295,8 @@ _tabbed_pages_spec = { } TextViewer_spec = { - 'file': 'textView', - 'kwds': {'title': 'Test textView', + '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" @@ -304,21 +304,21 @@ TextViewer_spec = { } _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': 'TreeWidget', + 'file': 'tree', 'kwds': {}, 'msg': "The canvas is scrollable.\n" "Click on folders upto to the lowest level." } _undo_delegator_spec = { - 'file': 'UndoDelegator', + 'file': 'undo', 'kwds': {}, 'msg': "Click [Undo] to undo any action.\n" "Click [Redo] to redo any action.\n" @@ -327,7 +327,7 @@ _undo_delegator_spec = { } _widget_redirector_spec = { - 'file': 'WidgetRedirector', + 'file': 'redirector', 'kwds': {}, 'msg': "Every text insert should be printed to the console." "or the IDLE shell." diff --git a/Lib/idlelib/idle_test/mock_idle.py b/Lib/idlelib/idle_test/mock_idle.py index 1672a34..c7b49ef 100644 --- a/Lib/idlelib/idle_test/mock_idle.py +++ b/Lib/idlelib/idle_test/mock_idle.py @@ -33,7 +33,7 @@ class Func: class Editor: - '''Minimally imitate EditorWindow.EditorWindow class. + '''Minimally imitate editor.EditorWindow class. ''' def __init__(self, flist=None, filename=None, key=None, root=None): self.text = Text() @@ -46,7 +46,7 @@ class Editor: class UndoDelegator: - '''Minimally imitate UndoDelegator,UndoDelegator class. + '''Minimally imitate undo.UndoDelegator class. ''' # A real undo block is only needed for user interaction. def undo_block_start(*args): diff --git a/Lib/idlelib/idle_test/test_autocomplete.py b/Lib/idlelib/idle_test/test_autocomplete.py index 3a2192e..833737e 100644 --- a/Lib/idlelib/idle_test/test_autocomplete.py +++ b/Lib/idlelib/idle_test/test_autocomplete.py @@ -2,9 +2,9 @@ import unittest from test.support import requires from tkinter import Tk, Text -import idlelib.AutoComplete as ac -import idlelib.AutoCompleteWindow as acw -import idlelib.macosxSupport as mac +import idlelib.autocomplete as ac +import idlelib.autocomplete_w as acw +import idlelib.macosx as mac from idlelib.idle_test.mock_idle import Func from idlelib.idle_test.mock_tk import Event diff --git a/Lib/idlelib/idle_test/test_autoexpand.py b/Lib/idlelib/idle_test/test_autoexpand.py index 7ca941e..801976a 100644 --- a/Lib/idlelib/idle_test/test_autoexpand.py +++ b/Lib/idlelib/idle_test/test_autoexpand.py @@ -1,9 +1,9 @@ -"""Unit tests for idlelib.AutoExpand""" +"""Unit tests for idlelib.autoexpand""" import unittest from test.support import requires from tkinter import Text, Tk #from idlelib.idle_test.mock_tk import Text -from idlelib.AutoExpand import AutoExpand +from idlelib.autoexpand import AutoExpand class Dummy_Editwin: diff --git a/Lib/idlelib/idle_test/test_calltips.py b/Lib/idlelib/idle_test/test_calltips.py index b2a733c..0b11602 100644 --- a/Lib/idlelib/idle_test/test_calltips.py +++ b/Lib/idlelib/idle_test/test_calltips.py @@ -1,5 +1,5 @@ import unittest -import idlelib.CallTips as ct +import idlelib.calltips as ct import textwrap import types diff --git a/Lib/idlelib/idle_test/test_config_help.py b/Lib/idlelib/idle_test/test_config_help.py index 664f8ed..b89b4e3 100644 --- a/Lib/idlelib/idle_test/test_config_help.py +++ b/Lib/idlelib/idle_test/test_config_help.py @@ -1,7 +1,7 @@ -"""Unittests for idlelib.configHelpSourceEdit""" +"""Unittests for idlelib.config_help.py""" import unittest from idlelib.idle_test.mock_tk import Var, Mbox, Entry -from idlelib import configHelpSourceEdit as help_dialog_module +from idlelib import config_help as help_dialog_module help_dialog = help_dialog_module.GetHelpSourceDialog diff --git a/Lib/idlelib/idle_test/test_config_name.py b/Lib/idlelib/idle_test/test_config_name.py deleted file mode 100644 index 40e72b9..0000000 --- a/Lib/idlelib/idle_test/test_config_name.py +++ /dev/null @@ -1,75 +0,0 @@ -"""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: - # Mock for testing the following methods of name_dialog - name_ok = name_dialog.name_ok - Ok = name_dialog.Ok - Cancel = name_dialog.Cancel - # Attributes, constant or variable, needed for tests - used_names = ['used'] - name = Var() - result = None - destroyed = False - 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_config_sec.py b/Lib/idlelib/idle_test/test_config_sec.py new file mode 100644 index 0000000..a98b484 --- /dev/null +++ b/Lib/idlelib/idle_test/test_config_sec.py @@ -0,0 +1,75 @@ +"""Unit tests for idlelib.config_sec""" +import unittest +from idlelib.idle_test.mock_tk import Var, Mbox +from idlelib import config_sec as name_dialog_module + +name_dialog = name_dialog_module.GetCfgSectionNameDialog + +class Dummy_name_dialog: + # Mock for testing the following methods of name_dialog + name_ok = name_dialog.name_ok + Ok = name_dialog.Ok + Cancel = name_dialog.Cancel + # Attributes, constant or variable, needed for tests + used_names = ['used'] + name = Var() + result = None + destroyed = False + 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 fab860b..3188432 100644 --- a/Lib/idlelib/idle_test/test_configdialog.py +++ b/Lib/idlelib/idle_test/test_configdialog.py @@ -1,4 +1,4 @@ -'''Unittests for idlelib/configHandler.py +'''Unittests for idlelib/config.py Coverage: 46% just by creating dialog. The other half is change code. @@ -6,8 +6,8 @@ Coverage: 46% just by creating dialog. The other half is change code. import unittest from test.support import requires from tkinter import Tk -from idlelib.configDialog import ConfigDialog -from idlelib.macosxSupport import _initializeTkVariantTests +from idlelib.configdialog import ConfigDialog +from idlelib.macosx import _initializeTkVariantTests class ConfigDialogTest(unittest.TestCase): diff --git a/Lib/idlelib/idle_test/test_delegator.py b/Lib/idlelib/idle_test/test_delegator.py index 1f0baa9..85624fb 100644 --- a/Lib/idlelib/idle_test/test_delegator.py +++ b/Lib/idlelib/idle_test/test_delegator.py @@ -1,5 +1,5 @@ import unittest -from idlelib.Delegator import Delegator +from idlelib.delegator import Delegator class DelegatorTest(unittest.TestCase): diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index a31d26d..e9d29d4 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -1,6 +1,6 @@ import unittest from tkinter import Tk, Text -from idlelib.EditorWindow import EditorWindow +from idlelib.editor import EditorWindow from test.support import requires class Editor_func_test(unittest.TestCase): diff --git a/Lib/idlelib/idle_test/test_grep.py b/Lib/idlelib/idle_test/test_grep.py index 0d8ff0d..6b54c13 100644 --- a/Lib/idlelib/idle_test/test_grep.py +++ b/Lib/idlelib/idle_test/test_grep.py @@ -1,5 +1,5 @@ """ !Changing this line will break Test_findfile.test_found! -Non-gui unit tests for idlelib.GrepDialog methods. +Non-gui unit tests for grep.GrepDialog methods. dummy_command calls grep_it calls findfiles. An exception raised in one method will fail callers. Otherwise, tests are mostly independent. @@ -8,7 +8,7 @@ Otherwise, tests are mostly independent. import unittest from test.support import captured_stdout from idlelib.idle_test.mock_tk import Var -from idlelib.GrepDialog import GrepDialog +from idlelib.grep import GrepDialog import re class Dummy_searchengine: @@ -72,7 +72,7 @@ class Grep_itTest(unittest.TestCase): self.assertTrue(lines[4].startswith('(Hint:')) class Default_commandTest(unittest.TestCase): - # To write this, mode OutputWindow import to top of GrepDialog + # To write this, move outwin import to top of GrepDialog # so it can be replaced by captured_stdout in class setup/teardown. pass diff --git a/Lib/idlelib/idle_test/test_history.py b/Lib/idlelib/idle_test/test_history.py index d7c3d70..6e8269c 100644 --- a/Lib/idlelib/idle_test/test_history.py +++ b/Lib/idlelib/idle_test/test_history.py @@ -4,8 +4,8 @@ from test.support import requires import tkinter as tk from tkinter import Text as tkText from idlelib.idle_test.mock_tk import Text as mkText -from idlelib.IdleHistory import History -from idlelib.configHandler import idleConf +from idlelib.history import History +from idlelib.config import idleConf line1 = 'a = 7' line2 = 'b = a' diff --git a/Lib/idlelib/idle_test/test_hyperparser.py b/Lib/idlelib/idle_test/test_hyperparser.py index edfc783..067e5b1 100644 --- a/Lib/idlelib/idle_test/test_hyperparser.py +++ b/Lib/idlelib/idle_test/test_hyperparser.py @@ -1,9 +1,9 @@ -"""Unittest for idlelib.HyperParser""" +"""Unittest for idlelib.hyperparser.py.""" import unittest from test.support import requires from tkinter import Tk, Text -from idlelib.EditorWindow import EditorWindow -from idlelib.HyperParser import HyperParser +from idlelib.editor import EditorWindow +from idlelib.hyperparser import HyperParser class DummyEditwin: def __init__(self, text): diff --git a/Lib/idlelib/idle_test/test_iomenu.py b/Lib/idlelib/idle_test/test_iomenu.py index e0e3b98..f8ff112 100644 --- a/Lib/idlelib/idle_test/test_iomenu.py +++ b/Lib/idlelib/idle_test/test_iomenu.py @@ -1,6 +1,6 @@ import unittest import io -from idlelib.PyShell import PseudoInputFile, PseudoOutputFile +from idlelib.pyshell import PseudoInputFile, PseudoOutputFile class S(str): diff --git a/Lib/idlelib/idle_test/test_paragraph.py b/Lib/idlelib/idle_test/test_paragraph.py index f6039e6..6949bbb 100644 --- a/Lib/idlelib/idle_test/test_paragraph.py +++ b/Lib/idlelib/idle_test/test_paragraph.py @@ -1,7 +1,7 @@ -# Test the functions and main class method of FormatParagraph.py +# Test the functions and main class method of paragraph.py import unittest -from idlelib import FormatParagraph as fp -from idlelib.EditorWindow import EditorWindow +from idlelib import paragraph as fp +from idlelib.editor import EditorWindow from tkinter import Tk, Text from test.support import requires @@ -38,7 +38,7 @@ class Is_Get_Test(unittest.TestCase): class FindTest(unittest.TestCase): - """Test the find_paragraph function in FormatParagraph. + """Test the find_paragraph function in paragraph module. Using the runcase() function, find_paragraph() is called with 'mark' set at multiple indexes before and inside the test paragraph. diff --git a/Lib/idlelib/idle_test/test_parenmatch.py b/Lib/idlelib/idle_test/test_parenmatch.py index e153952..794a3aa 100644 --- a/Lib/idlelib/idle_test/test_parenmatch.py +++ b/Lib/idlelib/idle_test/test_parenmatch.py @@ -1,4 +1,4 @@ -"""Test idlelib.ParenMatch.""" +"""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. from test.support import requires @@ -7,7 +7,7 @@ requires('gui') import unittest from unittest.mock import Mock from tkinter import Tk, Text -from idlelib.ParenMatch import ParenMatch +from idlelib.parenmatch import ParenMatch class DummyEditwin: def __init__(self, text): diff --git a/Lib/idlelib/idle_test/test_pathbrowser.py b/Lib/idlelib/idle_test/test_pathbrowser.py index afb886f..813cbcc 100644 --- a/Lib/idlelib/idle_test/test_pathbrowser.py +++ b/Lib/idlelib/idle_test/test_pathbrowser.py @@ -2,13 +2,13 @@ import unittest import os import sys import idlelib -from idlelib import PathBrowser +from idlelib import pathbrowser class PathBrowserTest(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()) @@ -17,11 +17,11 @@ class PathBrowserTest(unittest.TestCase): self.assertEqual(d.ispackagedir(dir + '/Icons'), False) 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) + 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 index bd2d666..dc3ed49 100644 --- a/Lib/idlelib/idle_test/test_percolator.py +++ b/Lib/idlelib/idle_test/test_percolator.py @@ -1,10 +1,10 @@ -'''Test Percolator''' +'''Test percolator.py.''' from test.support import requires requires('gui') import unittest from tkinter import Text, Tk, END -from idlelib.Percolator import Percolator, Delegator +from idlelib.percolator import Percolator, Delegator class MyFilter(Delegator): diff --git a/Lib/idlelib/idle_test/test_redirector.py b/Lib/idlelib/idle_test/test_redirector.py index 6440561..998fd65 100644 --- a/Lib/idlelib/idle_test/test_redirector.py +++ b/Lib/idlelib/idle_test/test_redirector.py @@ -1,4 +1,4 @@ -"""Unittest for idlelib.WidgetRedirector +"""Unittest for idlelib.redirector 100% coverage """ @@ -6,7 +6,7 @@ from test.support import requires import unittest from idlelib.idle_test.mock_idle import Func from tkinter import Tk, Text, TclError -from idlelib.WidgetRedirector import WidgetRedirector +from idlelib.redirector import WidgetRedirector class InitCloseTest(unittest.TestCase): diff --git a/Lib/idlelib/idle_test/test_replace.py b/Lib/idlelib/idle_test/test_replace.py index 09669f8..acaae84 100644 --- a/Lib/idlelib/idle_test/test_replace.py +++ b/Lib/idlelib/idle_test/test_replace.py @@ -1,4 +1,4 @@ -"""Unittest for idlelib.ReplaceDialog""" +"""Unittest for idlelib.replace.py""" from test.support import requires requires('gui') @@ -6,8 +6,8 @@ import unittest from unittest.mock import Mock from tkinter import Tk, Text from idlelib.idle_test.mock_tk import Mbox -import idlelib.SearchEngine as se -import idlelib.ReplaceDialog as rd +import idlelib.searchengine as se +import idlelib.replace as rd orig_mbox = se.tkMessageBox showerror = Mbox.showerror diff --git a/Lib/idlelib/idle_test/test_rstrip.py b/Lib/idlelib/idle_test/test_rstrip.py index 1c90b93..130e6be 100644 --- a/Lib/idlelib/idle_test/test_rstrip.py +++ b/Lib/idlelib/idle_test/test_rstrip.py @@ -1,5 +1,5 @@ import unittest -import idlelib.RstripExtension as rs +import idlelib.rstrip as rs from idlelib.idle_test.mock_idle import Editor class rstripTest(unittest.TestCase): @@ -21,7 +21,7 @@ class rstripTest(unittest.TestCase): 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 idlelib.editor import EditorWindow as Editor ## from tkinter import Tk ## editor = Editor(root=Tk()) text = editor.text diff --git a/Lib/idlelib/idle_test/test_search.py b/Lib/idlelib/idle_test/test_search.py index 190c866..0735d84 100644 --- a/Lib/idlelib/idle_test/test_search.py +++ b/Lib/idlelib/idle_test/test_search.py @@ -1,4 +1,4 @@ -"""Test SearchDialog class in SearchDialogue.py""" +"""Test SearchDialog class in idlelib.search.py""" # Does not currently test the event handler wrappers. # A usage test should simulate clicks and check hilighting. @@ -11,8 +11,8 @@ requires('gui') import unittest import tkinter as tk from tkinter import BooleanVar -import idlelib.SearchEngine as se -import idlelib.SearchDialog as sd +import idlelib.searchengine as se +import idlelib.search as sd class SearchDialogTest(unittest.TestCase): diff --git a/Lib/idlelib/idle_test/test_searchbase.py b/Lib/idlelib/idle_test/test_searchbase.py index 8036b91..5ff9476 100644 --- a/Lib/idlelib/idle_test/test_searchbase.py +++ b/Lib/idlelib/idle_test/test_searchbase.py @@ -1,4 +1,4 @@ -'''Unittests for idlelib/SearchDialogBase.py +'''Unittests for idlelib/searchbase.py Coverage: 99%. The only thing not covered is inconsequential -- testing skipping of suite when self.needwrapbutton is false. @@ -7,8 +7,8 @@ testing skipping of suite when self.needwrapbutton is false. import unittest from test.support import requires from tkinter import Tk, Toplevel, Frame ##, BooleanVar, StringVar -from idlelib import SearchEngine as se -from idlelib import SearchDialogBase as sdb +from idlelib import searchengine as se +from idlelib import searchbase as sdb from idlelib.idle_test.mock_idle import Func ## from idlelib.idle_test.mock_tk import Var diff --git a/Lib/idlelib/idle_test/test_searchengine.py b/Lib/idlelib/idle_test/test_searchengine.py index edbd558..7e6f8b7 100644 --- a/Lib/idlelib/idle_test/test_searchengine.py +++ b/Lib/idlelib/idle_test/test_searchengine.py @@ -1,4 +1,4 @@ -'''Test functions and SearchEngine class in SearchEngine.py.''' +'''Test functions and SearchEngine class in idlelib.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) @@ -10,7 +10,7 @@ import unittest # from test.support import requires from tkinter import BooleanVar, StringVar, TclError # ,Tk, Text import tkinter.messagebox as tkMessageBox -from idlelib import SearchEngine as se +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 diff --git a/Lib/idlelib/idle_test/test_textview.py b/Lib/idlelib/idle_test/test_textview.py index 68e5b82..bd0a46c 100644 --- a/Lib/idlelib/idle_test/test_textview.py +++ b/Lib/idlelib/idle_test/test_textview.py @@ -1,4 +1,4 @@ -'''Test the functions and main class method of textView.py. +'''Test the functions and main class method of textview.py. Since all methods and functions create (or destroy) a TextViewer, which is a widget containing multiple widgets, all tests must be gui tests. @@ -13,7 +13,7 @@ requires('gui') import unittest import os from tkinter import Tk -from idlelib import textView as tv +from idlelib import textview as tv from idlelib.idle_test.mock_idle import Func from idlelib.idle_test.mock_tk import Mbox diff --git a/Lib/idlelib/idle_test/test_undo.py b/Lib/idlelib/idle_test/test_undo.py index 2657984..9a167c9 100644 --- a/Lib/idlelib/idle_test/test_undo.py +++ b/Lib/idlelib/idle_test/test_undo.py @@ -1,4 +1,4 @@ -"""Unittest for UndoDelegator in idlelib.UndoDelegator. +"""Unittest for UndoDelegator in idlelib.undo.py. Coverage about 80% (retest). """ @@ -8,8 +8,8 @@ requires('gui') import unittest from unittest.mock import Mock from tkinter import Text, Tk -from idlelib.UndoDelegator import UndoDelegator -from idlelib.Percolator import Percolator +from idlelib.undo import UndoDelegator +from idlelib.percolator import Percolator class UndoDelegatorTest(unittest.TestCase): diff --git a/Lib/idlelib/idle_test/test_warning.py b/Lib/idlelib/idle_test/test_warning.py index 18627dd..f3269f1 100644 --- a/Lib/idlelib/idle_test/test_warning.py +++ b/Lib/idlelib/idle_test/test_warning.py @@ -1,4 +1,4 @@ -'''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. @@ -17,9 +17,9 @@ showwarning = warnings.showwarning running_in_idle = 'idle' in showwarning.__name__ from idlelib import run -from idlelib import PyShell as shell +from idlelib import pyshell as shell -# The following was generated from PyShell.idle_formatwarning +# The following was generated from pyshell.idle_formatwarning # and checked as matching expectation. idlemsg = ''' Warning (from warnings module): diff --git a/Lib/idlelib/iomenu.py b/Lib/idlelib/iomenu.py index 84f39a2..f6a7f14 100644 --- a/Lib/idlelib/iomenu.py +++ b/Lib/idlelib/iomenu.py @@ -10,7 +10,7 @@ import tkinter.filedialog as tkFileDialog import tkinter.messagebox as tkMessageBox from tkinter.simpledialog import askstring -from idlelib.configHandler import idleConf +from idlelib.config import idleConf # Try setting the locale, so that we can find out @@ -107,6 +107,9 @@ def coding_spec(data): 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 diff --git a/Lib/idlelib/macosx.py b/Lib/idlelib/macosx.py index 268426f..1e16f2c 100644 --- a/Lib/idlelib/macosx.py +++ b/Lib/idlelib/macosx.py @@ -123,23 +123,23 @@ 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 Bindings - from idlelib import WindowList + from idlelib import mainmenu + from idlelib import windows - closeItem = Bindings.menudefs[0][1][-2] + closeItem = mainmenu.menudefs[0][1][-2] # Remove the last 3 items of the file menu: a separator, close window and # quit. Close window will be reinserted just above the save item, where # it should be according to the HIG. Quit is in the application menu. - del Bindings.menudefs[0][1][-3:] - Bindings.menudefs[0][1].insert(6, closeItem) + del mainmenu.menudefs[0][1][-3:] + mainmenu.menudefs[0][1].insert(6, closeItem) # Remove the 'About' entry from the help menu, it is in the application # menu - del Bindings.menudefs[-1][1][0:2] + del mainmenu.menudefs[-1][1][0:2] # Remove the 'Configure Idle' entry from the options menu, it is in the # application menu as 'Preferences' - del Bindings.menudefs[-2][1][0] + del mainmenu.menudefs[-2][1][0] menubar = Menu(root) root.configure(menu=menubar) menudict = {} @@ -154,30 +154,30 @@ def overrideRootMenu(root, flist): if end > 0: menu.delete(0, end) - WindowList.add_windows_to_menu(menu) - WindowList.register_callback(postwindowsmenu) + windows.add_windows_to_menu(menu) + Windows.register_callback(postwindowsmenu) def about_dialog(event=None): "Handle Help 'About IDLE' event." - # Synchronize with EditorWindow.EditorWindow.about_dialog. - from idlelib import aboutDialog - aboutDialog.AboutDialog(root, 'About IDLE') + # Synchronize with editor.EditorWindow.about_dialog. + from idlelib import help_about + help_about.AboutDialog(root, 'About IDLE') def config_dialog(event=None): "Handle Options 'Configure IDLE' event." - # Synchronize with EditorWindow.EditorWindow.config_dialog. - from idlelib import configDialog + # 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) 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 EditorWindow.EditorWindow.help_dialog. + # Synchronize with editor.EditorWindow.help_dialog. from idlelib import help help.show_idlehelp(root) @@ -197,7 +197,7 @@ def overrideRootMenu(root, flist): menudict['application'] = menu = Menu(menubar, name='apple', tearoff=0) menubar.add_cascade(label='IDLE', menu=menu) - Bindings.menudefs.insert(0, + mainmenu.menudefs.insert(0, ('application', [ ('About IDLE', '<>'), None, @@ -205,7 +205,7 @@ def overrideRootMenu(root, flist): 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( + mainmenu.menudefs[0][1].append( ('_Preferences....', '<>'), ) if isCocoaTk(): @@ -214,12 +214,12 @@ def overrideRootMenu(root, flist): # replace default "Help" item in Help menu root.createcommand('::tk::mac::ShowHelp', help_dialog) # remove redundant "IDLE Help" from menu - del Bindings.menudefs[-1][1][0] + del mainmenu.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) diff --git a/Lib/idlelib/mainmenu.py b/Lib/idlelib/mainmenu.py index ab25ff1..965ada3 100644 --- a/Lib/idlelib/mainmenu.py +++ b/Lib/idlelib/mainmenu.py @@ -10,9 +10,9 @@ windows. """ from importlib.util import find_spec -from idlelib.configHandler import idleConf +from idlelib.config import idleConf -# Warning: menudefs is altered in macosxSupport.overrideRootMenu() +# Warning: menudefs is altered in macosx.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 diff --git a/Lib/idlelib/outwin.py b/Lib/idlelib/outwin.py index e614f9b..b3bc786 100644 --- a/Lib/idlelib/outwin.py +++ b/Lib/idlelib/outwin.py @@ -1,8 +1,8 @@ from tkinter import * -from idlelib.EditorWindow import EditorWindow +from idlelib.editor import EditorWindow import re import tkinter.messagebox as tkMessageBox -from idlelib import IOBinding +from idlelib import iomenu class OutputWindow(EditorWindow): @@ -36,7 +36,7 @@ class OutputWindow(EditorWindow): def write(self, s, tags=(), mark="insert"): if isinstance(s, (bytes, bytes)): - s = s.decode(IOBinding.encoding, "replace") + s = s.decode(iomenu.encoding, "replace") self.text.insert(mark, s, tags) self.text.see(mark) self.text.update() diff --git a/Lib/idlelib/paragraph.py b/Lib/idlelib/paragraph.py index 7a9d185..0323b53 100644 --- a/Lib/idlelib/paragraph.py +++ b/Lib/idlelib/paragraph.py @@ -16,7 +16,7 @@ Known problems with comment reformatting: """ import re -from idlelib.configHandler import idleConf +from idlelib.config import idleConf class FormatParagraph: @@ -191,5 +191,5 @@ def get_comment_header(line): if __name__ == "__main__": import unittest - unittest.main('idlelib.idle_test.test_formatparagraph', + unittest.main('idlelib.idle_test.test_paragraph', verbosity=2, exit=False) diff --git a/Lib/idlelib/parenmatch.py b/Lib/idlelib/parenmatch.py index 19bad8c..e98fac1 100644 --- a/Lib/idlelib/parenmatch.py +++ b/Lib/idlelib/parenmatch.py @@ -5,8 +5,8 @@ paren. Paren here is used generically; the matching applies to parentheses, square brackets, and curly braces. """ -from idlelib.HyperParser import HyperParser -from idlelib.configHandler import idleConf +from idlelib.hyperparser import HyperParser +from idlelib.config import idleConf _openers = {')':'(',']':'[','}':'{'} CHECK_DELAY = 100 # miliseconds diff --git a/Lib/idlelib/pathbrowser.py b/Lib/idlelib/pathbrowser.py index 9ab7632..966af4b 100644 --- a/Lib/idlelib/pathbrowser.py +++ b/Lib/idlelib/pathbrowser.py @@ -2,9 +2,9 @@ import os import sys import importlib.machinery -from idlelib.TreeWidget import TreeItem -from idlelib.ClassBrowser import ClassBrowser, ModuleBrowserTreeItem -from idlelib.PyShell import PyShellFileList +from idlelib.tree import TreeItem +from idlelib.browser import ClassBrowser, ModuleBrowserTreeItem +from idlelib.pyshell import PyShellFileList class PathBrowser(ClassBrowser): diff --git a/Lib/idlelib/percolator.py b/Lib/idlelib/percolator.py index b8be2aa..2271445 100644 --- a/Lib/idlelib/percolator.py +++ b/Lib/idlelib/percolator.py @@ -1,5 +1,5 @@ -from idlelib.WidgetRedirector import WidgetRedirector -from idlelib.Delegator import Delegator +from idlelib.redirector import WidgetRedirector +from idlelib.delegator import Delegator class Percolator: diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 1bcc9b6..8341cd9 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -24,16 +24,16 @@ except ImportError: sys.exit(1) import tkinter.messagebox as 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.editor import EditorWindow, fixwordbreaks +from idlelib.filelist import FileList +from idlelib.colorizer import ColorDelegator +from idlelib.undo import UndoDelegator +from idlelib.outwin import OutputWindow +from idlelib.config import idleConf from idlelib import rpc -from idlelib import Debugger -from idlelib import RemoteDebugger -from idlelib import macosxSupport +from idlelib import debugger +from idlelib import debugger_r +from idlelib import macosx HOST = '127.0.0.1' # python execution server on localhost loopback PORT = 0 # someday pass in host, port for remote debug capability @@ -410,7 +410,7 @@ class ModifiedInterpreter(InteractiveInterpreter): # run from the IDLE source directory. del_exitf = idleConf.GetOption('main', 'General', 'delete-exitfunc', default=False, type='bool') - if __name__ == 'idlelib.PyShell': + if __name__ == 'idlelib.pyshell': command = "__import__('idlelib.run').run.main(%r)" % (del_exitf,) else: command = "__import__('run').main(%r)" % (del_exitf,) @@ -468,7 +468,7 @@ class ModifiedInterpreter(InteractiveInterpreter): if debug: try: # Only close subprocess debugger, don't unregister gui_adap! - RemoteDebugger.close_subprocess_debugger(self.rpcclt) + debugger_r.close_subprocess_debugger(self.rpcclt) except: pass # Kill subprocess, spawn a new one, accept connection. @@ -497,7 +497,7 @@ class ModifiedInterpreter(InteractiveInterpreter): # restart subprocess debugger if debug: # Restarted debugger connects to current instance of debug GUI - RemoteDebugger.restart_subprocess_debugger(self.rpcclt) + debugger_r.restart_subprocess_debugger(self.rpcclt) # reload remote debugger breakpoints for all PyShellEditWindows debug.load_breakpoints() self.compile.compiler.flags = self.original_compiler_flags @@ -578,7 +578,7 @@ class ModifiedInterpreter(InteractiveInterpreter): if self.tkconsole.getvar("<>"): 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: @@ -613,13 +613,13 @@ class ModifiedInterpreter(InteractiveInterpreter): return def remote_stack_viewer(self): - from idlelib import RemoteObjectBrowser + from idlelib import debugobj_r oid = self.rpcclt.remotequeue("exec", "stackviewer", ("flist",), {}) if oid is None: self.tkconsole.root.bell() return - item = RemoteObjectBrowser.StubObjectTreeItem(self.rpcclt, oid) - from idlelib.TreeWidget import ScrolledCanvas, TreeNode + item = debugobj_r.StubObjectTreeItem(self.rpcclt, oid) + from idlelib.tree import ScrolledCanvas, TreeNode top = Toplevel(self.tkconsole.root) theme = idleConf.CurrentTheme() background = idleConf.GetHighlight(theme, 'normal')['background'] @@ -662,9 +662,9 @@ class ModifiedInterpreter(InteractiveInterpreter): # at the moment, InteractiveInterpreter expects str assert isinstance(source, str) #if isinstance(source, str): - # from idlelib import IOBinding + # from idlelib import iomenu # try: - # source = source.encode(IOBinding.encoding) + # source = source.encode(iomenu.encoding) # except UnicodeError: # self.tkconsole.resetoutput() # self.write("Unsupported characters in input\n") @@ -850,7 +850,7 @@ class PyShell(OutputWindow): # New classes - from idlelib.IdleHistory import History + from idlelib.history import History def __init__(self, flist=None): if use_subprocess: @@ -888,11 +888,11 @@ class PyShell(OutputWindow): self.save_stdout = sys.stdout self.save_stderr = sys.stderr self.save_stdin = sys.stdin - 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) + from idlelib import iomenu + self.stdin = PseudoInputFile(self, "stdin", iomenu.encoding) + self.stdout = PseudoOutputFile(self, "stdout", iomenu.encoding) + self.stderr = PseudoOutputFile(self, "stderr", iomenu.encoding) + self.console = PseudoOutputFile(self, "console", iomenu.encoding) if not use_subprocess: sys.stdout = self.stdout sys.stderr = self.stderr @@ -900,7 +900,7 @@ class PyShell(OutputWindow): try: # page help() text to shell. import pydoc # import must be done here to capture i/o rebinding. - # XXX KBK 27Dec07 use a textView someday, but must work w/o subproc + # XXX KBK 27Dec07 use TextViewer someday, but must work w/o subproc pydoc.pager = pydoc.plainpager except: sys.stderr = sys.__stderr__ @@ -954,7 +954,7 @@ class PyShell(OutputWindow): self.interp.setdebugger(None) db.close() if self.interp.rpcclt: - RemoteDebugger.close_remote_debugger(self.interp.rpcclt) + debugger_r.close_remote_debugger(self.interp.rpcclt) self.resetoutput() self.console.write("[DEBUG OFF]\n") sys.ps1 = ">>> " @@ -963,10 +963,10 @@ class PyShell(OutputWindow): def open_debugger(self): if self.interp.rpcclt: - dbg_gui = RemoteDebugger.start_remote_debugger(self.interp.rpcclt, + dbg_gui = debugger_r.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() sys.ps1 = "[DEBUG ON]\n>>> " @@ -1241,7 +1241,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): @@ -1546,9 +1546,9 @@ def main(): fixwordbreaks(root) root.withdraw() flist = PyShellFileList(root) - macosxSupport.setupApp(root, flist) + macosx.setupApp(root, flist) - if macosxSupport.isAquaTk(): + if macosx.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. @@ -1569,7 +1569,7 @@ def main(): shell = flist.open_shell() if not shell: return # couldn't open shell - if macosxSupport.isAquaTk() and flist.dict: + if macosx.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 @@ -1603,7 +1603,7 @@ def main(): # 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) + tkversionwarning = macosx.tkVersionWarning(root) if tkversionwarning: shell.interp.runcommand("print('%s')" % tkversionwarning) @@ -1613,7 +1613,7 @@ def main(): capture_warnings(False) if __name__ == "__main__": - sys.modules['PyShell'] = sys.modules['__main__'] + sys.modules['pyshell'] = sys.modules['__main__'] main() capture_warnings(False) # Make sure turned off; see issue 18081 diff --git a/Lib/idlelib/redirector.py b/Lib/idlelib/redirector.py index b66be9e..5ca7a2d 100644 --- a/Lib/idlelib/redirector.py +++ b/Lib/idlelib/redirector.py @@ -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 ColorDelegator.py. + to *args to accomplish that. For an example, see colorizer.py. ''' m = self._operations.get(operation) diff --git a/Lib/idlelib/replace.py b/Lib/idlelib/replace.py index f2ea22e..589b814 100644 --- a/Lib/idlelib/replace.py +++ b/Lib/idlelib/replace.py @@ -5,8 +5,8 @@ replace+find. """ from tkinter import * -from idlelib import SearchEngine -from idlelib.SearchDialogBase import SearchDialogBase +from idlelib import searchengine +from idlelib.searchbase import SearchDialogBase import re @@ -14,7 +14,7 @@ def replace(text): """Returns a singleton ReplaceDialog instance.The single dialog saves user entries and preferences across instances.""" root = text._root() - engine = SearchEngine.get(root) + engine = searchengine.get(root) if not hasattr(engine, "_replacedialog"): engine._replacedialog = ReplaceDialog(root, engine) dialog = engine._replacedialog @@ -164,7 +164,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: diff --git a/Lib/idlelib/run.py b/Lib/idlelib/run.py index 595e7bc..eb34944 100644 --- a/Lib/idlelib/run.py +++ b/Lib/idlelib/run.py @@ -7,15 +7,15 @@ import threading import queue import tkinter -from idlelib import CallTips -from idlelib import AutoComplete +from idlelib import calltips +from idlelib import autocomplete -from idlelib import RemoteDebugger -from idlelib import RemoteObjectBrowser -from idlelib import StackViewer +from idlelib import debugger_r +from idlelib import debugobj_r +from idlelib import stackviewer from idlelib import rpc -from idlelib import PyShell -from idlelib import IOBinding +from idlelib import pyshell +from idlelib import iomenu import __main__ @@ -32,7 +32,7 @@ def idle_showwarning_subproc( if file is None: file = sys.stderr try: - file.write(PyShell.idle_formatwarning( + file.write(pyshell.idle_formatwarning( message, category, filename, lineno, line)) except IOError: pass # the file (probably stderr) is invalid - this warning gets lost. @@ -82,7 +82,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. RemoteDebugger.Debugger.start_debugger()). The latter, in turn, can + (e.g. debugger_r.Debugger.start_debugger()). The latter, in turn, can call MyHandler(SocketIO) register/unregister methods via the reference to register and unregister themselves. @@ -204,7 +204,7 @@ def print_exception(): tbe = traceback.extract_tb(tb) print('Traceback (most recent call last):', file=efile) exclude = ("run.py", "rpc.py", "threading.py", "queue.py", - "RemoteDebugger.py", "bdb.py") + "debugger_r.py", "bdb.py") cleanup_traceback(tbe, exclude) traceback.print_list(tbe, file=efile) lines = traceback.format_exception_only(typ, exc) @@ -298,12 +298,12 @@ class MyHandler(rpc.RPCHandler): executive = Executive(self) self.register("exec", executive) self.console = self.get_remote_proxy("console") - 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) + sys.stdin = pyshell.PseudoInputFile(self.console, "stdin", + iomenu.encoding) + sys.stdout = pyshell.PseudoOutputFile(self.console, "stdout", + iomenu.encoding) + sys.stderr = pyshell.PseudoOutputFile(self.console, "stderr", + iomenu.encoding) sys.displayhook = rpc.displayhook # page help() text to shell. @@ -339,8 +339,8 @@ class Executive(object): def __init__(self, rpchandler): self.rpchandler = rpchandler self.locals = __main__.__dict__ - self.calltip = CallTips.CallTips() - self.autocomplete = AutoComplete.AutoComplete() + self.calltip = calltips.CallTips() + self.autocomplete = autocomplete.AutoComplete() def runcode(self, code): global interruptable @@ -372,7 +372,7 @@ class Executive(object): thread.interrupt_main() def start_the_debugger(self, gui_adap_oid): - return RemoteDebugger.start_debugger(self.rpchandler, gui_adap_oid) + return debugger_r.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" @@ -396,7 +396,7 @@ class Executive(object): tb = tb.tb_next sys.last_type = typ sys.last_value = val - item = StackViewer.StackTreeItem(flist, tb) - return RemoteObjectBrowser.remote_object_tree_item(item) + item = stackviewer.StackTreeItem(flist, tb) + return debugobj_r.remote_object_tree_item(item) capture_warnings(False) # Make sure turned off; see issue 18081 diff --git a/Lib/idlelib/runscript.py b/Lib/idlelib/runscript.py index 5cb818d..7e7524a 100644 --- a/Lib/idlelib/runscript.py +++ b/Lib/idlelib/runscript.py @@ -21,10 +21,10 @@ import os import tabnanny import tokenize import tkinter.messagebox as tkMessageBox -from idlelib import PyShell +from idlelib import pyshell -from idlelib.configHandler import idleConf -from idlelib import macosxSupport +from idlelib.config import idleConf +from idlelib import macosx indent_message = """Error: Inconsistent indentation detected! @@ -46,12 +46,12 @@ class ScriptBinding: def __init__(self, editwin): self.editwin = editwin - # Provide instance variables referenced by Debugger + # 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(): + if macosx.isCocoaTk(): self.editwin.text_frame.bind('<>', self._run_module_event) def check_module_event(self, event): @@ -112,7 +112,7 @@ class ScriptBinding: shell.set_warning_stream(saved_stream) def run_module_event(self, event): - if macosxSupport.isCocoaTk(): + 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 @@ -142,7 +142,7 @@ class ScriptBinding: if not self.tabnanny(filename): return 'break' interp = self.shell.interp - if PyShell.use_subprocess: + if pyshell.use_subprocess: interp.restart_subprocess(with_cwd=False, filename= self.editwin._filename_to_unicode(filename)) dirname = os.path.dirname(filename) @@ -161,7 +161,7 @@ class ScriptBinding: 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. + # Need to change streams in pyshell.ModifiedInterpreter. interp.runcode(code) return 'break' diff --git a/Lib/idlelib/scrolledlist.py b/Lib/idlelib/scrolledlist.py index 53576b5..80df0f8 100644 --- a/Lib/idlelib/scrolledlist.py +++ b/Lib/idlelib/scrolledlist.py @@ -1,5 +1,5 @@ from tkinter import * -from idlelib import macosxSupport +from idlelib import macosx class ScrolledList: @@ -23,7 +23,7 @@ class ScrolledList: # Bind events to the list box listbox.bind("", self.click_event) listbox.bind("", self.double_click_event) - if macosxSupport.isAquaTk(): + if macosx.isAquaTk(): listbox.bind("", self.popup_event) listbox.bind("", self.popup_event) else: diff --git a/Lib/idlelib/search.py b/Lib/idlelib/search.py index 765d53f..a609fd9 100644 --- a/Lib/idlelib/search.py +++ b/Lib/idlelib/search.py @@ -1,12 +1,12 @@ from tkinter import * -from idlelib import SearchEngine -from idlelib.SearchDialogBase import SearchDialogBase +from idlelib import searchengine +from idlelib.searchbase import SearchDialogBase def _setup(text): "Create or find the singleton SearchDialog instance." root = text._root() - engine = SearchEngine.get(root) + engine = searchengine.get(root) if not hasattr(engine, "_searchdialog"): engine._searchdialog = SearchDialog(root, engine) return engine._searchdialog diff --git a/Lib/idlelib/searchbase.py b/Lib/idlelib/searchbase.py index 5fa84e2..9206bf5 100644 --- a/Lib/idlelib/searchbase.py +++ b/Lib/idlelib/searchbase.py @@ -125,7 +125,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. ''' diff --git a/Lib/idlelib/searchengine.py b/Lib/idlelib/searchengine.py index 37883bf..2e3700e 100644 --- a/Lib/idlelib/searchengine.py +++ b/Lib/idlelib/searchengine.py @@ -57,7 +57,7 @@ class SearchEngine: def setcookedpat(self, pat): "Set pattern after escaping if re." - # called only in SearchDialog.py: 66 + # called only in search.py: 66 if self.isre(): pat = re.escape(pat) self.setpat(pat) diff --git a/Lib/idlelib/stackviewer.py b/Lib/idlelib/stackviewer.py index ccc755c..5c188f0 100644 --- a/Lib/idlelib/stackviewer.py +++ b/Lib/idlelib/stackviewer.py @@ -4,9 +4,9 @@ import linecache import re import tkinter as tk -from idlelib.TreeWidget import TreeNode, TreeItem, ScrolledCanvas -from idlelib.ObjectBrowser import ObjectTreeItem, make_objecttreeitem -from idlelib.PyShell import PyShellFileList +from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas +from idlelib.debugobj import ObjectTreeItem, make_objecttreeitem +from idlelib.pyshell import PyShellFileList def StackBrowser(root, flist=None, tb=None, top=None): if top is None: diff --git a/Lib/idlelib/tooltip.py b/Lib/idlelib/tooltip.py index 964107e..c3eafed 100644 --- a/Lib/idlelib/tooltip.py +++ b/Lib/idlelib/tooltip.py @@ -1,4 +1,4 @@ -# general purpose 'tooltip' routines - currently unused in idlefork +# general purpose 'tooltip' routines - currently unused in idlelib # (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 @@ -76,7 +76,7 @@ class ListboxToolTip(ToolTipBase): for item in self.items: listbox.insert(END, item) -def _tooltip(parent): +def _tooltip(parent): # htest # root = Tk() root.title("Test tooltip") width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) diff --git a/Lib/idlelib/tree.py b/Lib/idlelib/tree.py index a19578f..08cc637 100644 --- a/Lib/idlelib/tree.py +++ b/Lib/idlelib/tree.py @@ -17,8 +17,8 @@ import os from tkinter import * -from idlelib import ZoomHeight -from idlelib.configHandler import idleConf +from idlelib import zoomheight +from idlelib.config import idleConf ICONDIR = "Icons" @@ -445,7 +445,7 @@ 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" diff --git a/Lib/idlelib/undo.py b/Lib/idlelib/undo.py index 1c2502d..3e94b69 100644 --- a/Lib/idlelib/undo.py +++ b/Lib/idlelib/undo.py @@ -1,7 +1,7 @@ import string from tkinter import * -from idlelib.Delegator import Delegator +from idlelib.delegator import Delegator #$ event <> #$ win @@ -340,7 +340,7 @@ class CommandSequence(Command): def _undo_delegator(parent): # htest # import re import tkinter as tk - from idlelib.Percolator import Percolator + from idlelib.percolator import Percolator undowin = tk.Toplevel() undowin.title("Test UndoDelegator") width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) diff --git a/Lib/idlelib/zoomheight.py b/Lib/idlelib/zoomheight.py index a5d679e..0016e9d 100644 --- a/Lib/idlelib/zoomheight.py +++ b/Lib/idlelib/zoomheight.py @@ -3,7 +3,7 @@ import re import sys -from idlelib import macosxSupport +from idlelib import macosx class ZoomHeight: @@ -32,7 +32,7 @@ def zoom_height(top): newy = 0 newheight = newheight - 72 - elif macosxSupport.isAquaTk(): + elif macosx.isAquaTk(): # The '88' below is a magic number that avoids placing the bottom # of the window below the panel on my machine. I don't know how # to calculate the correct value for this with tkinter. diff --git a/Lib/turtledemo/__main__.py b/Lib/turtledemo/__main__.py index d9b3dd5..07a1c82 100644 --- a/Lib/turtledemo/__main__.py +++ b/Lib/turtledemo/__main__.py @@ -89,9 +89,9 @@ import sys import os from tkinter import * -from idlelib.Percolator import Percolator -from idlelib.ColorDelegator import ColorDelegator -from idlelib.textView import view_text +from idlelib.percolator import Percolator +from idlelib.colorizer import ColorDelegator +from idlelib.textview import view_text from turtledemo import __doc__ as about_turtledemo import turtle -- cgit v0.12