diff options
author | nkozlovskiy <[email protected]> | 2023-09-29 12:24:06 +0300 |
---|---|---|
committer | nkozlovskiy <[email protected]> | 2023-09-29 12:41:34 +0300 |
commit | e0e3e1717e3d33762ce61950504f9637a6e669ed (patch) | |
tree | bca3ff6939b10ed60c3d5c12439963a1146b9711 /contrib/tools/python/src/Lib/idlelib | |
parent | 38f2c5852db84c7b4d83adfcb009eb61541d1ccd (diff) |
add ydb deps
Diffstat (limited to 'contrib/tools/python/src/Lib/idlelib')
83 files changed, 20707 insertions, 0 deletions
diff --git a/contrib/tools/python/src/Lib/idlelib/AutoComplete.py b/contrib/tools/python/src/Lib/idlelib/AutoComplete.py new file mode 100644 index 00000000000..9381bdadd15 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/AutoComplete.py @@ -0,0 +1,229 @@ +"""AutoComplete.py - An IDLE extension for automatically completing names. + +This extension can complete either attribute names or file names. It can pop +a window with all available names, for the user to select from. +""" +import os +import sys +import string + +from idlelib.configHandler import idleConf + +# This string includes all chars that may be in a file name (without a path +# separator) +FILENAME_CHARS = string.ascii_letters + string.digits + os.curdir + "._~#$:-" +# This string includes all chars that may be in an identifier +ID_CHARS = string.ascii_letters + string.digits + "_" + +# These constants represent the two different types of completions +COMPLETE_ATTRIBUTES, COMPLETE_FILES = range(1, 2+1) + +from idlelib import AutoCompleteWindow +from idlelib.HyperParser import HyperParser + +import __main__ + +SEPS = os.sep +if os.altsep: # e.g. '/' on Windows... + SEPS += os.altsep + +class AutoComplete: + + menudefs = [ + ('edit', [ + ("Show Completions", "<<force-open-completions>>"), + ]) + ] + + popupwait = idleConf.GetOption("extensions", "AutoComplete", + "popupwait", type="int", default=0) + + def __init__(self, editwin=None): + self.editwin = editwin + if editwin is 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): + self._remove_autocomplete_window() + mode = COMPLETE_FILES + while i and curline[i-1] in FILENAME_CHARS: + i -= 1 + comp_start = curline[i:j] + j = i + while i and curline[i-1] in FILENAME_CHARS + SEPS: + i -= 1 + comp_what = curline[i:j] + elif hp.is_in_code() and (not mode or mode==COMPLETE_ATTRIBUTES): + self._remove_autocomplete_window() + mode = COMPLETE_ATTRIBUTES + while i and curline[i-1] in ID_CHARS: + i -= 1 + comp_start = curline[i:j] + if i and curline[i-1] == '.': + 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/contrib/tools/python/src/Lib/idlelib/AutoCompleteWindow.py b/contrib/tools/python/src/Lib/idlelib/AutoCompleteWindow.py new file mode 100644 index 00000000000..205a29be6ce --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/AutoCompleteWindow.py @@ -0,0 +1,407 @@ +""" +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 = "<<autocompletewindow-hide>>" +HIDE_SEQUENCES = ("<FocusOut>", "<ButtonPress>") +KEYPRESS_VIRTUAL_EVENT_NAME = "<<autocompletewindow-keypress>>" +# We need to bind event beyond <Key> so that the function will be called +# before the default specific IDLE function +KEYPRESS_SEQUENCES = ("<Key>", "<Key-BackSpace>", "<Key-Return>", "<Key-Tab>", + "<Key-Up>", "<Key-Down>", "<Key-Home>", "<Key-End>", + "<Key-Prior>", "<Key-Next>") +KEYRELEASE_VIRTUAL_EVENT_NAME = "<<autocompletewindow-keyrelease>>" +KEYRELEASE_SEQUENCE = "<KeyRelease>" +LISTUPDATE_SEQUENCE = "<B1-ButtonRelease>" +WINCONFIG_SEQUENCE = "<Configure>" +DOUBLECLICK_SEQUENCE = "<B1-Double-ButtonRelease>" + +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 + + 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/contrib/tools/python/src/Lib/idlelib/AutoExpand.py b/contrib/tools/python/src/Lib/idlelib/AutoExpand.py new file mode 100644 index 00000000000..70590542818 --- /dev/null +++ b/contrib/tools/python/src/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 <<expand-word>> +###$ win <Alt-slash> +###$ unix <Alt-slash> + +class AutoExpand: + + menudefs = [ + ('edit', [ + ('E_xpand Word', '<<expand-word>>'), + ]), + ] + + wordchars = string.ascii_letters + string.digits + "_" + + def __init__(self, editwin): + self.text = editwin.text + self.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/contrib/tools/python/src/Lib/idlelib/Bindings.py b/contrib/tools/python/src/Lib/idlelib/Bindings.py new file mode 100644 index 00000000000..2fd8532dc1e --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/Bindings.py @@ -0,0 +1,91 @@ +"""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 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-new-window>>'), + ('_Open...', '<<open-window-from-file>>'), + ('Open _Module...', '<<open-module>>'), + ('Class _Browser', '<<open-class-browser>>'), + ('_Path Browser', '<<open-path-browser>>'), + None, + ('_Save', '<<save-window>>'), + ('Save _As...', '<<save-window-as-file>>'), + ('Save Cop_y As...', '<<save-copy-of-window-as-file>>'), + None, + ('Prin_t Window', '<<print-window>>'), + None, + ('_Close', '<<close-window>>'), + ('E_xit', '<<close-all-windows>>'), + ]), + ('edit', [ + ('_Undo', '<<undo>>'), + ('_Redo', '<<redo>>'), + None, + ('Cu_t', '<<cut>>'), + ('_Copy', '<<copy>>'), + ('_Paste', '<<paste>>'), + ('Select _All', '<<select-all>>'), + None, + ('_Find...', '<<find>>'), + ('Find A_gain', '<<find-again>>'), + ('Find _Selection', '<<find-selection>>'), + ('Find in Files...', '<<find-in-files>>'), + ('R_eplace...', '<<replace>>'), + ('Go to _Line', '<<goto-line>>'), + ]), +('format', [ + ('_Indent Region', '<<indent-region>>'), + ('_Dedent Region', '<<dedent-region>>'), + ('Comment _Out Region', '<<comment-region>>'), + ('U_ncomment Region', '<<uncomment-region>>'), + ('Tabify Region', '<<tabify-region>>'), + ('Untabify Region', '<<untabify-region>>'), + ('Toggle Tabs', '<<toggle-tabs>>'), + ('New Indent Width', '<<change-indentwidth>>'), + ]), + ('run', [ + ('Python Shell', '<<open-python-shell>>'), + ]), + ('shell', [ + ('_View Last Restart', '<<view-restart>>'), + ('_Restart Shell', '<<restart-shell>>'), + None, + ('_Interrupt Execution', '<<interrupt-execution>>'), + ]), + ('debug', [ + ('_Go to File/Line', '<<goto-file-line>>'), + ('!_Debugger', '<<toggle-debugger>>'), + ('_Stack Viewer', '<<open-stack-viewer>>'), + ('!_Auto-open Stack Viewer', '<<toggle-jit-stack-viewer>>'), + ]), + ('options', [ + ('Configure _IDLE', '<<open-config-dialog>>'), + None, + ]), + ('help', [ + ('_About IDLE', '<<about-idle>>'), + None, + ('_IDLE Help', '<<help>>'), + ('Python _Docs', '<<python-docs>>'), + ]), +] + +default_keydefs = idleConf.GetCurrentKeySet() diff --git a/contrib/tools/python/src/Lib/idlelib/CallTipWindow.py b/contrib/tools/python/src/Lib/idlelib/CallTipWindow.py new file mode 100644 index 00000000000..2a453d06f5b --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/CallTipWindow.py @@ -0,0 +1,162 @@ +"""A CallTip window class for Tkinter/IDLE. + +After ToolTip.py, which uses ideas gleaned from PySol +Used by the CallTips IDLE extension. +""" +from Tkinter import Toplevel, Label, LEFT, SOLID, TclError + +HIDE_VIRTUAL_EVENT_NAME = "<<calltipwindow-hide>>" +HIDE_SEQUENCES = ("<Key-Escape>", "<FocusOut>") +CHECKHIDE_VIRTUAL_EVENT_NAME = "<<calltipwindow-checkhide>>" +CHECKHIDE_SEQUENCES = ("<KeyRelease>", "<ButtonRelease>") +CHECKHIDE_TIME = 100 # milliseconds + +MARK_RIGHT = "calltipwindowregion_right" + +class CallTip: + + def __init__(self, widget): + self.widget = widget + self.tipwindow = self.label = None + self.parenline = self.parencol = None + self.lastline = None + self.hideid = self.checkhideid = None + self.checkhide_after_id = None + + def position_window(self): + """Check if needs to reposition the window, and if so - do it.""" + curline = int(self.widget.index("insert").split('.')[0]) + if curline == self.lastline: + return + self.lastline = curline + self.widget.see("insert") + if curline == self.parenline: + box = self.widget.bbox("%d.%d" % (self.parenline, + self.parencol)) + else: + box = self.widget.bbox("%d.0" % curline) + if not box: + box = list(self.widget.bbox("insert")) + # align to left of window + box[0] = 0 + box[2] = 0 + x = box[0] + self.widget.winfo_rootx() + 2 + y = box[1] + box[3] + self.widget.winfo_rooty() + self.tipwindow.wm_geometry("+%d+%d" % (x, y)) + + def showtip(self, text, parenleft, parenright): + """Show the calltip, bind events which will close it and reposition it. + """ + # Only called in CallTips, where lines are truncated + self.text = text + if self.tipwindow or not self.text: + return + + self.widget.mark_set(MARK_RIGHT, parenright) + self.parenline, self.parencol = map( + int, self.widget.index(parenleft).split(".")) + + self.tipwindow = tw = Toplevel(self.widget) + self.position_window() + # remove border on calltip window + tw.wm_overrideredirect(1) + try: + # This command is only needed and available on Tk >= 8.4.0 for OSX + # Without it, call tips intrude on the typing process by grabbing + # the focus. + tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w, + "help", "noActivates") + except TclError: + pass + self.label = Label(tw, text=self.text, justify=LEFT, + background="#ffffe0", relief=SOLID, borderwidth=1, + font = self.widget['font']) + self.label.pack() + tw.update_idletasks() + tw.lift() # work around bug in Tk 8.5.18+ (issue #24570) + + self.checkhideid = self.widget.bind(CHECKHIDE_VIRTUAL_EVENT_NAME, + self.checkhide_event) + for seq in CHECKHIDE_SEQUENCES: + self.widget.event_add(CHECKHIDE_VIRTUAL_EVENT_NAME, seq) + self.widget.after(CHECKHIDE_TIME, self.checkhide_event) + self.hideid = self.widget.bind(HIDE_VIRTUAL_EVENT_NAME, + self.hide_event) + for seq in HIDE_SEQUENCES: + self.widget.event_add(HIDE_VIRTUAL_EVENT_NAME, seq) + + def checkhide_event(self, event=None): + if not self.tipwindow: + # If the event was triggered by the same event that unbinded + # this function, the function will be called nevertheless, + # so do nothing in this case. + return + curline, curcol = map(int, self.widget.index("insert").split('.')) + if curline < self.parenline or \ + (curline == self.parenline and curcol <= self.parencol) or \ + self.widget.compare("insert", ">", MARK_RIGHT): + self.hidetip() + else: + self.position_window() + if self.checkhide_after_id is not None: + self.widget.after_cancel(self.checkhide_after_id) + self.checkhide_after_id = \ + self.widget.after(CHECKHIDE_TIME, self.checkhide_event) + + def hide_event(self, event): + if not self.tipwindow: + # See the explanation in checkhide_event. + return + self.hidetip() + + def hidetip(self): + if not self.tipwindow: + return + + for seq in CHECKHIDE_SEQUENCES: + self.widget.event_delete(CHECKHIDE_VIRTUAL_EVENT_NAME, seq) + self.widget.unbind(CHECKHIDE_VIRTUAL_EVENT_NAME, self.checkhideid) + self.checkhideid = None + for seq in HIDE_SEQUENCES: + self.widget.event_delete(HIDE_VIRTUAL_EVENT_NAME, seq) + self.widget.unbind(HIDE_VIRTUAL_EVENT_NAME, self.hideid) + self.hideid = None + + self.label.destroy() + self.label = None + self.tipwindow.destroy() + self.tipwindow = None + + self.widget.mark_unset(MARK_RIGHT) + self.parenline = self.parencol = self.lastline = None + + def is_active(self): + return bool(self.tipwindow) + + +def _calltip_window(parent): # htest # + from Tkinter import Toplevel, Text, LEFT, BOTH + + top = Toplevel(parent) + top.title("Test calltips") + top.geometry("200x100+%d+%d" % (parent.winfo_rootx() + 200, + parent.winfo_rooty() + 150)) + text = Text(top) + text.pack(side=LEFT, fill=BOTH, expand=1) + text.insert("insert", "string.split") + top.update() + calltip = CallTip(text) + + def calltip_show(event): + calltip.showtip("(s=Hello world)", "insert", "end") + def calltip_hide(event): + calltip.hidetip() + text.event_add("<<calltip-show>>", "(") + text.event_add("<<calltip-hide>>", ")") + text.bind("<<calltip-show>>", calltip_show) + text.bind("<<calltip-hide>>", calltip_hide) + text.focus_set() + +if __name__=='__main__': + from idlelib.idle_test.htest import run + run(_calltip_window) diff --git a/contrib/tools/python/src/Lib/idlelib/CallTips.py b/contrib/tools/python/src/Lib/idlelib/CallTips.py new file mode 100644 index 00000000000..3db26362c24 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/CallTips.py @@ -0,0 +1,219 @@ +"""CallTips.py - An IDLE Extension to Jog Your Memory + +Call Tips are floating windows which display function, class, and method +parameter and docstring information when you type an opening parenthesis, and +which disappear when you type a closing parenthesis. + +""" +import __main__ +import re +import sys +import textwrap +import types + +from idlelib import CallTipWindow +from idlelib.HyperParser import HyperParser + + +class CallTips: + + menudefs = [ + ('edit', [ + ("Show call tip", "<<force-open-calltip>>"), + ]) + ] + + def __init__(self, editwin=None): + if editwin is None: # subprocess and test + self.editwin = None + return + self.editwin = editwin + self.text = editwin.text + self.calltip = None + self._make_calltip_window = self._make_tk_calltip_window + + def close(self): + self._make_calltip_window = None + + def _make_tk_calltip_window(self): + # See __init__ for usage + return CallTipWindow.CallTip(self.text) + + def _remove_calltip_window(self, event=None): + if self.calltip: + self.calltip.hidetip() + self.calltip = None + + def force_open_calltip_event(self, event): + """Happens when the user really wants to open a CallTip, even if a + function call is needed. + """ + self.open_calltip(True) + + def try_open_calltip_event(self, event): + """Happens when it would be nice to open a CallTip, but not really + necessary, for example after an opening bracket, so function calls + won't be made. + """ + self.open_calltip(False) + + def refresh_calltip_event(self, event): + """If there is already a calltip window, check if it is still needed, + and if so, reload it. + """ + if self.calltip and self.calltip.is_active(): + self.open_calltip(False) + + def open_calltip(self, evalfuncs): + self._remove_calltip_window() + + hp = HyperParser(self.editwin, "insert") + sur_paren = hp.get_surrounding_brackets('(') + if not sur_paren: + return + hp.set_index(sur_paren[0]) + expression = hp.get_expression() + if not expression or (not evalfuncs and expression.find('(') != -1): + return + arg_text = self.fetch_tip(expression) + if not arg_text: + return + self.calltip = self._make_calltip_window() + self.calltip.showtip(arg_text, sur_paren[0], sur_paren[1]) + + def fetch_tip(self, expression): + """Return the argument list and docstring of a function or class + + If there is a Python subprocess, get the calltip there. Otherwise, + either fetch_tip() is running in the subprocess itself or it was called + in an IDLE EditorWindow before any script had been run. + + The subprocess environment is that of the most recently run script. If + two unrelated modules are being edited some calltips in the current + module may be inoperative if the module was not the last to run. + + To find methods, fetch_tip must be fed a fully qualified name. + + """ + try: + rpcclt = self.editwin.flist.pyshell.interp.rpcclt + except AttributeError: + rpcclt = None + if rpcclt: + return rpcclt.remotecall("exec", "get_the_calltip", + (expression,), {}) + else: + entity = self.get_entity(expression) + return get_arg_text(entity) + + def get_entity(self, expression): + """Return the object corresponding to expression evaluated + in a namespace spanning sys.modules and __main.dict__. + """ + if expression: + namespace = sys.modules.copy() + namespace.update(__main__.__dict__) + try: + return eval(expression, namespace) + except BaseException: + # An uncaught exception closes idle, and eval can raise any + # exception, especially if user classes are involved. + return None + +def _find_constructor(class_ob): + # Given a class object, return a function object used for the + # constructor (ie, __init__() ) or None if we can't find one. + try: + return class_ob.__init__.im_func + except AttributeError: + for base in class_ob.__bases__: + rc = _find_constructor(base) + if rc is not None: return rc + return None + +# The following are used in get_arg_text +_MAX_COLS = 85 +_MAX_LINES = 5 # enough for bytes +_INDENT = ' '*4 # for wrapped signatures + +def get_arg_text(ob): + '''Return a string describing the signature of a callable object, or ''. + + For Python-coded functions and methods, the first line is introspected. + Delete 'self' parameter for classes (.__init__) and bound methods. + The next lines are the first lines of the doc string up to the first + empty line or _MAX_LINES. For builtins, this typically includes + the arguments in addition to the return value. + ''' + argspec = "" + try: + ob_call = ob.__call__ + except BaseException: + if type(ob) is types.ClassType: # old-style + ob_call = ob + else: + return argspec + + arg_offset = 0 + if type(ob) in (types.ClassType, types.TypeType): + # Look for the first __init__ in the class chain with .im_func. + # Slot wrappers (builtins, classes defined in funcs) do not. + fob = _find_constructor(ob) + if fob is None: + fob = lambda: None + else: + arg_offset = 1 + elif type(ob) == types.MethodType: + # bit of a hack for methods - turn it into a function + # and drop the "self" param for bound methods + fob = ob.im_func + if ob.im_self is not None: + arg_offset = 1 + elif type(ob_call) == types.MethodType: + # a callable class instance + fob = ob_call.im_func + arg_offset = 1 + else: + fob = ob + # Try to build one for Python defined functions + if type(fob) in [types.FunctionType, types.LambdaType]: + argcount = fob.func_code.co_argcount + real_args = fob.func_code.co_varnames[arg_offset:argcount] + defaults = fob.func_defaults or [] + defaults = list(map(lambda name: "=%s" % repr(name), defaults)) + defaults = [""] * (len(real_args) - len(defaults)) + defaults + items = map(lambda arg, dflt: arg + dflt, real_args, defaults) + for flag, pre, name in ((0x4, '*', 'args'), (0x8, '**', 'kwargs')): + if fob.func_code.co_flags & flag: + pre_name = pre + name + if name not in real_args: + items.append(pre_name) + else: + i = 1 + while ((name+'%s') % i) in real_args: + i += 1 + items.append((pre_name+'%s') % i) + argspec = ", ".join(items) + argspec = "(%s)" % re.sub("(?<!\d)\.\d+", "<tuple>", argspec) + + lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT) + if len(argspec) > _MAX_COLS else [argspec] if argspec else []) + + if isinstance(ob_call, types.MethodType): + doc = ob_call.__doc__ + else: + doc = getattr(ob, "__doc__", "") + if doc: + for line in doc.split('\n', _MAX_LINES)[:_MAX_LINES]: + line = line.strip() + if not line: + break + if len(line) > _MAX_COLS: + line = line[: _MAX_COLS - 3] + '...' + lines.append(line) + argspec = '\n'.join(lines) + return argspec + +if __name__ == '__main__': + from unittest import main + main('idlelib.idle_test.test_calltips', verbosity=2) diff --git a/contrib/tools/python/src/Lib/idlelib/ClassBrowser.py b/contrib/tools/python/src/Lib/idlelib/ClassBrowser.py new file mode 100644 index 00000000000..d09c52fe4d7 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/ClassBrowser.py @@ -0,0 +1,236 @@ +"""Class browser. + +XXX TO DO: + +- reparse when source changed (maybe just a button would be OK?) + (or recheck on window popup) +- add popup menu with more options (e.g. doc strings, base classes, imports) +- show function argument list? (have to do pattern matching on source) +- should the classes and methods lists also be in the module's menu bar? +- add base classes to class browser tree +""" + +import os +import sys +import pyclbr + +from idlelib import PyShell +from idlelib.WindowList import ListedToplevel +from idlelib.TreeWidget import TreeNode, TreeItem, ScrolledCanvas +from idlelib.configHandler import idleConf + +file_open = None # Method...Item and Class...Item use this. +# Normally PyShell.flist.open, but there is no PyShell.flist for htest. + +class ClassBrowser: + + def __init__(self, flist, name, path, _htest=False): + # XXX This API should change, if the file doesn't end in ".py" + # XXX the code here is bogus! + """ + _htest - bool, change box when location running htest. + """ + global file_open + if not _htest: + file_open = PyShell.flist.open + self.name = name + self.file = os.path.join(path[0], self.name + ".py") + self._htest = _htest + self.init(flist) + + def close(self, event=None): + self.top.destroy() + self.node.destroy() + + def init(self, flist): + self.flist = flist + # reset pyclbr + pyclbr._modules.clear() + # create top + self.top = top = ListedToplevel(flist.root) + top.protocol("WM_DELETE_WINDOW", self.close) + top.bind("<Escape>", self.close) + if self._htest: # place dialog below parent if running htest + top.geometry("+%d+%d" % + (flist.root.winfo_rootx(), flist.root.winfo_rooty() + 200)) + self.settitle() + top.focus_set() + # create scrolled canvas + theme = idleConf.CurrentTheme() + background = idleConf.GetHighlight(theme, 'normal')['background'] + sc = ScrolledCanvas(top, bg=background, highlightthickness=0, takefocus=1) + sc.frame.pack(expand=1, fill="both") + item = self.rootnode() + self.node = node = TreeNode(sc.canvas, None, item) + node.update() + node.expand() + + def settitle(self): + self.top.wm_title("Class Browser - " + self.name) + self.top.wm_iconname("Class Browser") + + def rootnode(self): + return ModuleBrowserTreeItem(self.file) + +class ModuleBrowserTreeItem(TreeItem): + + def __init__(self, file): + self.file = file + + def GetText(self): + return os.path.basename(self.file) + + def GetIconName(self): + return "python" + + def GetSubList(self): + sublist = [] + for name in self.listclasses(): + item = ClassBrowserTreeItem(name, self.classes, self.file) + sublist.append(item) + return sublist + + def OnDoubleClick(self): + if os.path.normcase(self.file[-3:]) != ".py": + return + if not os.path.exists(self.file): + return + PyShell.flist.open(self.file) + + def IsExpandable(self): + return os.path.normcase(self.file[-3:]) == ".py" + + def listclasses(self): + dir, file = os.path.split(self.file) + name, ext = os.path.splitext(file) + if os.path.normcase(ext) != ".py": + return [] + try: + dict = pyclbr.readmodule_ex(name, [dir] + sys.path) + except ImportError: + return [] + items = [] + self.classes = {} + for key, cl in dict.items(): + if cl.module == name: + s = key + if hasattr(cl, 'super') and cl.super: + supers = [] + for sup in cl.super: + if type(sup) is type(''): + sname = sup + else: + sname = sup.name + if sup.module != cl.module: + sname = "%s.%s" % (sup.module, sname) + supers.append(sname) + s = s + "(%s)" % ", ".join(supers) + items.append((cl.lineno, s)) + self.classes[s] = cl + items.sort() + list = [] + for item, s in items: + list.append(s) + return list + +class ClassBrowserTreeItem(TreeItem): + + def __init__(self, name, classes, file): + self.name = name + self.classes = classes + self.file = file + try: + self.cl = self.classes[self.name] + except (IndexError, KeyError): + self.cl = None + self.isfunction = isinstance(self.cl, pyclbr.Function) + + def GetText(self): + if self.isfunction: + return "def " + self.name + "(...)" + else: + return "class " + self.name + + def GetIconName(self): + if self.isfunction: + return "python" + else: + return "folder" + + def IsExpandable(self): + if self.cl: + try: + return not not self.cl.methods + except AttributeError: + return False + + def GetSubList(self): + if not self.cl: + return [] + sublist = [] + for name in self.listmethods(): + item = MethodBrowserTreeItem(name, self.cl, self.file) + sublist.append(item) + return sublist + + def OnDoubleClick(self): + if not os.path.exists(self.file): + return + edit = file_open(self.file) + if hasattr(self.cl, 'lineno'): + lineno = self.cl.lineno + edit.gotoline(lineno) + + def listmethods(self): + if not self.cl: + return [] + items = [] + for name, lineno in self.cl.methods.items(): + items.append((lineno, name)) + items.sort() + list = [] + for item, name in items: + list.append(name) + return list + +class MethodBrowserTreeItem(TreeItem): + + def __init__(self, name, cl, file): + self.name = name + self.cl = cl + self.file = file + + def GetText(self): + return "def " + self.name + "(...)" + + def GetIconName(self): + return "python" # XXX + + def IsExpandable(self): + return 0 + + def OnDoubleClick(self): + if not os.path.exists(self.file): + return + edit = file_open(self.file) + edit.gotoline(self.cl.methods[self.name]) + +def _class_browser(parent): #Wrapper for htest + try: + file = __file__ + except NameError: + file = sys.argv[0] + if sys.argv[1:]: + file = sys.argv[1] + else: + file = sys.argv[0] + dir, file = os.path.split(file) + name = os.path.splitext(file)[0] + flist = PyShell.PyShellFileList(parent) + global file_open + file_open = flist.open + ClassBrowser(flist, name, [dir], _htest=True) + +if __name__ == "__main__": + from idlelib.idle_test.htest import run + run(_class_browser) diff --git a/contrib/tools/python/src/Lib/idlelib/CodeContext.py b/contrib/tools/python/src/Lib/idlelib/CodeContext.py new file mode 100644 index 00000000000..bb0cc9c8920 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/CodeContext.py @@ -0,0 +1,176 @@ +"""CodeContext - Extension to display the block context above the edit window + +Once code has scrolled off the top of a window, it can be difficult to +determine which block you are in. This extension implements a pane at the top +of each IDLE edit window which provides block structure hints. These hints are +the lines which contain the block opening keywords, e.g. 'if', for the +enclosing block. The number of hint lines is determined by the numlines +variable in the CodeContext section of config-extensions.def. Lines which do +not open blocks are not shown in the context hints pane. + +""" +import Tkinter +from Tkconstants import TOP, LEFT, X, W, SUNKEN +import re +from sys import maxint as INFINITY +from idlelib.configHandler import idleConf + +BLOCKOPENERS = {"class", "def", "elif", "else", "except", "finally", "for", + "if", "try", "while", "with"} +UPDATEINTERVAL = 100 # millisec +FONTUPDATEINTERVAL = 1000 # millisec + +getspacesfirstword =\ + lambda s, c=re.compile(r"^(\s*)(\w*)"): c.match(s).groups() + +class CodeContext: + menudefs = [('options', [('!Code Conte_xt', '<<toggle-code-context>>')])] + context_depth = idleConf.GetOption("extensions", "CodeContext", + "numlines", type="int", default=3) + bgcolor = idleConf.GetOption("extensions", "CodeContext", + "bgcolor", type="str", default="LightGray") + fgcolor = idleConf.GetOption("extensions", "CodeContext", + "fgcolor", type="str", default="Black") + def __init__(self, editwin): + self.editwin = editwin + self.text = editwin.text + self.textfont = self.text["font"] + self.label = None + # self.info is a list of (line number, indent level, line text, block + # keyword) tuples providing the block structure associated with + # self.topvisible (the linenumber of the line displayed at the top of + # the edit window). self.info[0] is initialized as a 'dummy' line which + # starts the toplevel 'block' of the module. + self.info = [(0, -1, "", False)] + self.topvisible = 1 + visible = idleConf.GetOption("extensions", "CodeContext", + "visible", type="bool", default=False) + if visible: + self.toggle_code_context_event() + self.editwin.setvar('<<toggle-code-context>>', True) + # Start two update cycles, one for context lines, one for font changes. + self.text.after(UPDATEINTERVAL, self.timer_event) + self.text.after(FONTUPDATEINTERVAL, self.font_timer_event) + + def toggle_code_context_event(self, event=None): + if not self.label: + # Calculate the border width and horizontal padding required to + # align the context with the text in the main Text widget. + # + # All values are passed through int(str(<value>)), since some + # values may be pixel objects, which can't simply be added to ints. + widgets = self.editwin.text, self.editwin.text_frame + # Calculate the required vertical padding + padx = 0 + for widget in widgets: + padx += int(str( widget.pack_info()['padx'] )) + padx += int(str( widget.cget('padx') )) + # Calculate the required border width + border = 0 + for widget in widgets: + border += int(str( widget.cget('border') )) + self.label = Tkinter.Label(self.editwin.top, + text="\n" * (self.context_depth - 1), + anchor=W, justify=LEFT, + font=self.textfont, + bg=self.bgcolor, fg=self.fgcolor, + width=1, #don't request more than we get + padx=padx, border=border, + relief=SUNKEN) + # Pack the label widget before and above the text_frame widget, + # thus ensuring that it will appear directly above text_frame + self.label.pack(side=TOP, fill=X, expand=False, + before=self.editwin.text_frame) + else: + self.label.destroy() + self.label = None + idleConf.SetOption("extensions", "CodeContext", "visible", + str(self.label is not None)) + idleConf.SaveUserCfgFiles() + + def get_line_info(self, linenum): + """Get the line indent value, text, and any block start keyword + + If the line does not start a block, the keyword value is False. + The indentation of empty lines (or comment lines) is INFINITY. + + """ + text = self.text.get("%d.0" % linenum, "%d.end" % linenum) + spaces, firstword = getspacesfirstword(text) + opener = firstword in BLOCKOPENERS and firstword + if len(text) == len(spaces) or text[len(spaces)] == '#': + indent = INFINITY + else: + indent = len(spaces) + return indent, text, opener + + def get_context(self, new_topvisible, stopline=1, stopindent=0): + """Get context lines, starting at new_topvisible and working backwards. + + Stop when stopline or stopindent is reached. Return a tuple of context + data and the indent level at the top of the region inspected. + + """ + assert stopline > 0 + lines = [] + # The indentation level we are currently in: + lastindent = INFINITY + # For a line to be interesting, it must begin with a block opening + # keyword, and have less indentation than lastindent. + for linenum in xrange(new_topvisible, stopline-1, -1): + indent, text, opener = self.get_line_info(linenum) + if indent < lastindent: + lastindent = indent + if opener in ("else", "elif"): + # We also show the if statement + lastindent += 1 + if opener and linenum < new_topvisible and indent >= stopindent: + lines.append((linenum, indent, text, opener)) + if lastindent <= stopindent: + break + lines.reverse() + return lines, lastindent + + def update_code_context(self): + """Update context information and lines visible in the context pane. + + """ + new_topvisible = int(self.text.index("@0,0").split('.')[0]) + if self.topvisible == new_topvisible: # haven't scrolled + return + if self.topvisible < new_topvisible: # scroll down + lines, lastindent = self.get_context(new_topvisible, + self.topvisible) + # retain only context info applicable to the region + # between topvisible and new_topvisible: + while self.info[-1][1] >= lastindent: + del self.info[-1] + elif self.topvisible > new_topvisible: # scroll up + stopindent = self.info[-1][1] + 1 + # retain only context info associated + # with lines above new_topvisible: + while self.info[-1][0] >= new_topvisible: + stopindent = self.info[-1][1] + del self.info[-1] + lines, lastindent = self.get_context(new_topvisible, + self.info[-1][0]+1, + stopindent) + self.info.extend(lines) + self.topvisible = new_topvisible + # empty lines in context pane: + context_strings = [""] * max(0, self.context_depth - len(self.info)) + # followed by the context hint lines: + context_strings += [x[2] for x in self.info[-self.context_depth:]] + self.label["text"] = '\n'.join(context_strings) + + def timer_event(self): + if self.label: + self.update_code_context() + self.text.after(UPDATEINTERVAL, self.timer_event) + + def font_timer_event(self): + newtextfont = self.text["font"] + if self.label and newtextfont != self.textfont: + self.textfont = newtextfont + self.label["font"] = self.textfont + self.text.after(FONTUPDATEINTERVAL, self.font_timer_event) diff --git a/contrib/tools/python/src/Lib/idlelib/ColorDelegator.py b/contrib/tools/python/src/Lib/idlelib/ColorDelegator.py new file mode 100644 index 00000000000..fec2670e792 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/ColorDelegator.py @@ -0,0 +1,258 @@ +import time +import re +import keyword +import __builtin__ +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(__builtin__) + if not name.startswith('_')] + # We don't know whether "print" is a function or a keyword, + # so we always treat is as a keyword (the most common case). + builtinlist.remove('print') + # self.file = file("file") : + # 1st 'file' colorized normal, 2nd as builtin, 3rd as string + builtin = r"([^.'\"\\#]\b|^)" + any("BUILTIN", builtinlist) + r"\b" + comment = any("COMMENT", [r"#[^\n]*"]) + stringprefix = r"(\br|u|ur|R|U|UR|Ur|uR|b|B|br|Br|bR|BR)?" + sqstring = stringprefix + r"'[^'\\\n]*(\\.[^'\\\n]*)*'?" + dqstring = stringprefix + r'"[^"\\\n]*(\\.[^"\\\n]*)*"?' + sq3string = stringprefix + r"'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?" + 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("<<toggle-auto-coloring>>") + Delegator.setdelegate(self, delegate) + if delegate is not None: + self.config_colors() + self.bind("<<toggle-auto-coloring>>", 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.clock() + self.recolorize_main() + t1 = time.clock() + if DEBUG: print "%.3f seconds" % (t1-t0) + finally: + self.colorizing = False + if self.allow_colorizing and self.tag_nextrange("TODO", "1.0"): + if DEBUG: print "reschedule colorizing" + 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.keys(): + 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.keys(): + 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/contrib/tools/python/src/Lib/idlelib/Debugger.py b/contrib/tools/python/src/Lib/idlelib/Debugger.py new file mode 100644 index 00000000000..c5170659124 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/Debugger.py @@ -0,0 +1,529 @@ +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() + # <running program with traces> + # callback to debugger's interaction() + # nested event loop + # run() for second command + # + # This kind of nesting of event loops causes all kinds of problems + # (see e.g. issue #24455) especially when dealing with running as a + # subprocess, where there's all kinds of extra stuff happening in + # there - insert a traceback.print_stack() to check it out. + # + # By this point, we've already called restart_subprocess() in + # ScriptBinding. However, we also need to unwind the stack back to + # that outer event loop. To accomplish this, we: + # - return immediately from the nested run() + # - abort_loop ensures the nested event loop will terminate + # - the debugger's interaction routine completes normally + # - the restart_subprocess() will have taken care of stopping + # the running program, which will also let the outer run complete + # + # That leaves us back at the outer main event loop, at which point our + # after event can fire, and we'll come back to this routine with a + # clean stack. + if self.nesting_level > 0: + self.abort_loop() + self.root.after(100, lambda: self.run(*args)) + return + try: + self.interacting = 1 + return self.idb.run(*args) + 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("<Escape>", 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" + pyshell_edit_windows = self.pyshell.flist.inversedict.keys() + for editwin in pyshell_edit_windows: + 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) + import string + sourceline = string.strip(sourceline) + 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 repr + self.repr = repr.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 subframe.children.values(): + c.destroy() + self.dict = None + if not dict: + l = Label(subframe, text="None") + l.grid(row=0, column=0) + else: + names = dict.keys() + names.sort() + 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 <Configure> 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/contrib/tools/python/src/Lib/idlelib/Delegator.py b/contrib/tools/python/src/Lib/idlelib/Delegator.py new file mode 100644 index 00000000000..c4765163f80 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/Delegator.py @@ -0,0 +1,25 @@ +class Delegator: + + # The cache is only used to be able to change delegates! + + def __init__(self, delegate=None): + self.delegate = delegate + self.__cache = set() + + 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): + for key in self.__cache: + try: + delattr(self, key) + except AttributeError: + pass + self.__cache.clear() + + def setdelegate(self, delegate): + self.resetcache() + self.delegate = delegate diff --git a/contrib/tools/python/src/Lib/idlelib/EditorWindow.py b/contrib/tools/python/src/Lib/idlelib/EditorWindow.py new file mode 100644 index 00000000000..8a337192ad1 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/EditorWindow.py @@ -0,0 +1,1704 @@ +import sys +import os +import platform +import re +import imp +from Tkinter import * +import tkSimpleDialog +import tkMessageBox +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) + if micro: + release += '%s' % (micro,) + if level == 'candidate': + release += 'rc%s' % (serial,) + elif level != 'final': + release += '%s%s' % (level[0], serial) + return release + +def _find_module(fullname, path=None): + """Version of imp.find_module() that handles hierarchical module names""" + + file = None + for tgt in fullname.split('.'): + if file is not None: + file.close() # close intermediate files + (file, filename, descr) = imp.find_module(tgt, path) + if descr[2] == imp.PY_SOURCE: + break # find but not load the source file + module = imp.load_module(tgt, file, filename, descr) + try: + path = module.__path__ + except AttributeError: + raise ImportError, 'No source for module ' + module.__name__ + if descr[2] != imp.PY_SOURCE: + # If all of the above fails and didn't raise an exception,fallback + # to a straight import which can find __init__.py in a package. + m = __import__(fullname) + try: + filename = m.__file__ + except AttributeError: + pass + else: + file = None + base, ext = os.path.splitext(filename) + if ext == '.pyc': + ext = '.py' + filename = base + ext + descr = filename, None, imp.PY_SOURCE + return file, filename, descr + + +class HelpDialog(object): + + def __init__(self): + self.parent = None # parent of help window + self.dlg = None # the help window iteself + + def display(self, parent, near=None): + """ Display the help dialog. + + parent - parent widget for the help window + + near - a Toplevel widget (e.g. EditorWindow or PyShell) + to use as a reference for placing the help window + """ + import warnings as w + w.warn("EditorWindow.HelpDialog is no longer used by Idle.\n" + "It will be removed in 3.6 or later.\n" + "It has been replaced by private help.HelpWindow\n", + DeprecationWarning, stacklevel=2) + if self.dlg is None: + self.show_dialog(parent) + if near: + self.nearwindow(near) + + def show_dialog(self, parent): + self.parent = parent + fn=os.path.join(os.path.abspath(os.path.dirname(__file__)),'help.txt') + self.dlg = dlg = textView.view_file(parent,'Help',fn, modal=False) + dlg.bind('<Destroy>', self.destroy, '+') + + def nearwindow(self, near): + # Place the help dialog near the window specified by parent. + # Note - this may not reposition the window in Metacity + # if "/apps/metacity/general/disable_workarounds" is enabled + dlg = self.dlg + geom = (near.winfo_rootx() + 10, near.winfo_rooty() + 10) + dlg.withdraw() + dlg.geometry("=+%d+%d" % geom) + dlg.deiconify() + dlg.lift() + + def destroy(self, ev=None): + self.dlg = None + self.parent = None + +helpDialog = HelpDialog() # singleton instance, no longer used + + +class EditorWindow(object): + from idlelib.Percolator import Percolator + from idlelib.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.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.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.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("<<close-window>>", self.close_event) + if macosxSupport.isAquaTk(): + # Command-W on editorwindows doesn't work without this. + text.bind('<<close-window>>', self.close_event) + # Some OS X systems have only one mouse button, so use + # control-click for popup context menus there. For two + # buttons, AquaTk defines <2> as the right button, not <3>. + text.bind("<Control-Button-1>",self.right_menu_event) + text.bind("<2>", self.right_menu_event) + else: + # Elsewhere, use right-click for popup menus. + text.bind("<3>",self.right_menu_event) + text.bind("<<cut>>", self.cut) + text.bind("<<copy>>", self.copy) + text.bind("<<paste>>", self.paste) + text.bind("<<center-insert>>", self.center_insert_event) + text.bind("<<help>>", self.help_dialog) + text.bind("<<python-docs>>", self.python_docs) + text.bind("<<about-idle>>", self.about_dialog) + text.bind("<<open-config-dialog>>", self.config_dialog) + text.bind("<<open-module>>", self.open_module) + text.bind("<<do-nothing>>", lambda event: "break") + text.bind("<<select-all>>", self.select_all) + text.bind("<<remove-selection>>", self.remove_selection) + text.bind("<<find>>", self.find_event) + text.bind("<<find-again>>", self.find_again_event) + text.bind("<<find-in-files>>", self.find_in_files_event) + text.bind("<<find-selection>>", self.find_selection_event) + text.bind("<<replace>>", self.replace_event) + text.bind("<<goto-line>>", self.goto_line_event) + text.bind("<<smart-backspace>>",self.smart_backspace_event) + text.bind("<<newline-and-indent>>",self.newline_and_indent_event) + text.bind("<<smart-indent>>",self.smart_indent_event) + text.bind("<<indent-region>>",self.indent_region_event) + text.bind("<<dedent-region>>",self.dedent_region_event) + text.bind("<<comment-region>>",self.comment_region_event) + text.bind("<<uncomment-region>>",self.uncomment_region_event) + text.bind("<<tabify-region>>",self.tabify_region_event) + text.bind("<<untabify-region>>",self.untabify_region_event) + text.bind("<<toggle-tabs>>",self.toggle_tabs_event) + text.bind("<<change-indentwidth>>",self.change_indentwidth_event) + text.bind("<Left>", self.move_at_edge_if_selection(0)) + text.bind("<Right>", self.move_at_edge_if_selection(1)) + text.bind("<<del-word-left>>", self.del_word_left) + text.bind("<<del-word-right>>", self.del_word_right) + text.bind("<<beginning-of-line>>", self.home_callback) + + if flist: + flist.inversedict[self] = key + if key: + flist.dict[key] = self + text.bind("<<open-new-window>>", self.new_callback) + text.bind("<<close-all-windows>>", self.flist.close_all_callback) + text.bind("<<open-class-browser>>", self.open_class_browser) + text.bind("<<open-path-browser>>", self.open_path_browser) + + 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) + + # Create the recent files submenu + self.recent_files_menu = Menu(self.menubar, tearoff=0) + self.menudict['file'].insert_cascade(3, label='Recent Files', + underline=0, + menu=self.recent_files_menu) + self.update_recent_files_list() + + self.color = None # initialized below in self.ResetColorizer + if filename: + if os.path.exists(filename) and not os.path.isdir(filename): + io.loadfile(filename) + else: + io.set_filename(filename) + self.ResetColorizer() + self.saved_change_hook() + + self.set_indentation_params(self.ispythonsource(filename)) + + 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): + """convert filename to unicode in order to display it in Tk""" + if isinstance(filename, unicode) or not filename: + return filename + else: + try: + return filename.decode(self.filesystemencoding) + except UnicodeDecodeError: + # XXX + try: + return filename.decode(self.encoding) + except UnicodeDecodeError: + # byte-to-byte conversion + return filename.decode('iso8859-1') + + def new_callback(self, event): + dirname, basename = self.io.defaultfilename() + self.flist.new(dirname) + return "break" + + def home_callback(self, event): + if (event.state & 4) != 0 and event.keysym == "Home": + # state&4==Control. If <Control-Home>, use the Tk binding. + return + 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 xrange(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"): + self.text.mark_set("my_anchor", "insert") # there was no previous selection + else: + if self.text.compare(self.text.index("sel.first"), "<", self.text.index("insert")): + 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("<<set-line-and-column>>", self.set_line_and_column) + self.text.event_add("<<set-line-and-column>>", + "<KeyRelease>", "<ButtonRelease>") + 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.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", "<<virtual-event>>", "statefuncname"), ... + ("Close", "<<close-window>>", 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 WindowsError 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("<<Cut>>") + 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("<<Copy>>") + return "break" + + def paste(self,event): + self.text.event_generate("<<Paste>>") + 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('<Meta-Delete>') + return "break" + + def del_word_right(self, event): + self.text.event_generate('<Meta-d>') + 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 or in FileList? + 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: + (f, file_path, (suffix, mode, mtype)) = _find_module(name) + except (NameError, ImportError) as msg: + tkMessageBox.showerror("Import error", str(msg), parent=self.text) + return + if mtype != imp.PY_SOURCE: + tkMessageBox.showerror("Unsupported type", + "%s is not a source module" % name, parent=self.text) + return + if f: + f.close() + if self.flist: + self.flist.open(file_path) + else: + self.io.loadfile(file_path) + return file_path + + def open_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 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 + try: + f = open(filename) + line = f.readline() + f.close() + except IOError: + return False + return line.startswith('#!') and line.find('python') >= 0 + + def close_hook(self): + if self.flist: + 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']) + + 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.keys(): + 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 WindowsError 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') 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') as rf_file: + rf_file.writelines(rf_list) + except IOError as err: + if not getattr(self.root, "recentfilelist_error_displayed", False): + self.root.recentfilelist_error_displayed = True + tkMessageBox.showwarning(title='IDLE Warning', + message="Cannot update File menu Recent Files list. " + "Your operating system says:\n%s\n" + "Select OK and IDLE will continue without updating." + % str(err), + parent=self.text) + # for each edit window instance, construct the recent files menu + for instance in self.top.instance_dict.keys(): + menu = instance.recent_files_menu + menu.delete(0, END) # clear, and rebuild: + for i, file_name in enumerate(rf_list): + file_name = file_name.rstrip() # zap \n + # make unicode string to display non-ASCII chars correctly + ufile_name = self._filename_to_unicode(file_name) + callback = instance.__recent_file_callback(file_name) + menu.add_command(label=ulchars[i] + " " + 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) + tuple = (map(int, m.groups())) + return tuple + + 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 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) + import traceback + traceback.print_exc() + + def get_standard_extension_names(self): + return idleConf.GetExtensions(editor_only=True) + + def load_extension(self, name): + try: + mod = __import__(name, globals(), locals(), []) + except ImportError: + print "\nFailed to import extension: ", name + return + 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.keys(): + 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_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_tabwidth(self, newtabwidth): + text = self.text + if self.get_tabwidth() != newtabwidth: + pixels = text.tk.call("font", "measure", text["font"], + "-displayof", text.master, + "n" * newtabwidth) + text.configure(tabs=pixels) + + # If ispythonsource and guess are true, guess a good value for + # indentwidth based on file content (if possible), and if + # indentwidth != tabwidth set usetabs false. + # In any case, adjust the Text widget's view of what a tab + # character means. + + def set_indentation_params(self, ispythonsource, guess=True): + if guess and ispythonsource: + i = self.guess_indent() + if 2 <= i <= 8: + self.indentwidth = i + if self.indentwidth != self.tabwidth: + self.usetabs = False + self.set_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: + _tokenize.tokenize(self.readline, self.tokeneater) + except (_tokenize.TokenError, SyntaxError): + # since we cut off the tokenizer early, we can trigger + # spurious errors + pass + finally: + _tokenize.tabsize = save_tabsize + 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 { + "<<open-module>>", + "<<goto-line>>", + "<<change-indentwidth>>"}): + 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) # [email protected] + 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("<<close-all-windows>>", edit.close_event) + # Does not stop error, neither does following + # edit.text.bind("<<close-window>>", edit.close_event) + + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(_editor_window) diff --git a/contrib/tools/python/src/Lib/idlelib/FileList.py b/contrib/tools/python/src/Lib/idlelib/FileList.py new file mode 100644 index 00000000000..46979e33e35 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/FileList.py @@ -0,0 +1,126 @@ +import os +from Tkinter import * +import 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: + return self.EditorWindow(self, filename, key) + + 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 self.inversedict.keys(): + 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 os.error: + pass + else: + filename = os.path.join(pwd, filename) + return os.path.normpath(filename) + + +def _test(): + from idlelib.EditorWindow import fixwordbreaks + from idlelib.run import fix_scaling + import sys + root = Tk() + fix_scaling(root) + fixwordbreaks(root) + root.withdraw() + flist = FileList(root) + 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/contrib/tools/python/src/Lib/idlelib/FormatParagraph.py b/contrib/tools/python/src/Lib/idlelib/FormatParagraph.py new file mode 100644 index 00000000000..7a9d185042e --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/FormatParagraph.py @@ -0,0 +1,195 @@ +"""Extension to format a paragraph or selection to a max width. + +Does basic, standard text formatting, and also understands Python +comment blocks. Thus, for editing Python source code, this +extension is really only suitable for reformatting these comment +blocks or triple-quoted strings. + +Known problems with comment reformatting: +* If there is a selection marked, and the first line of the + selection is not complete, the block will probably not be detected + as comments, and will have the normal "text formatting" rules + applied. +* If a comment block has leading whitespace that mixes tabs and + spaces, they will not be considered part of the same block. +* Fancy comments, like this bulleted list, aren't handled :-) +""" + +import re +from idlelib.configHandler import idleConf + +class FormatParagraph: + + menudefs = [ + ('format', [ # /s/edit/format [email protected] + ('Format Paragraph', '<<format-paragraph>>'), + ]) + ] + + def __init__(self, editwin): + self.editwin = editwin + + def close(self): + self.editwin = None + + def format_paragraph_event(self, event, limit=None): + """Formats paragraph to a max width specified in idleConf. + + If text is selected, format_paragraph_event will start breaking lines + at the max width, starting from the beginning selection. + + If no text is selected, format_paragraph_event uses the current + cursor location to determine the paragraph (lines of text surrounded + by blank lines) and formats it. + + The length limit parameter is for testing with a known value. + """ + if limit is None: + # The default length limit is that defined by pep8 + limit = idleConf.GetOption( + 'extensions', 'FormatParagraph', 'max-width', + type='int', default=72) + text = self.editwin.text + first, last = self.editwin.get_selection_indices() + if first and last: + data = text.get(first, last) + comment_header = get_comment_header(data) + else: + first, last, comment_header, data = \ + find_paragraph(text, text.index("insert")) + if comment_header: + newdata = reformat_comment(data, limit, comment_header) + else: + newdata = reformat_paragraph(data, limit) + text.tag_remove("sel", "1.0", "end") + + if newdata != data: + text.mark_set("insert", first) + text.undo_block_start() + text.delete(first, last) + text.insert(first, newdata) + text.undo_block_stop() + else: + text.mark_set("insert", last) + text.see("insert") + return "break" + +def find_paragraph(text, mark): + """Returns the start/stop indices enclosing the paragraph that mark is in. + + Also returns the comment format string, if any, and paragraph of text + between the start/stop indices. + """ + lineno, col = map(int, mark.split(".")) + line = text.get("%d.0" % lineno, "%d.end" % lineno) + + # Look for start of next paragraph if the index passed in is a blank line + while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line): + lineno = lineno + 1 + line = text.get("%d.0" % lineno, "%d.end" % lineno) + first_lineno = lineno + comment_header = get_comment_header(line) + comment_header_len = len(comment_header) + + # Once start line found, search for end of paragraph (a blank line) + while get_comment_header(line)==comment_header and \ + not is_all_white(line[comment_header_len:]): + lineno = lineno + 1 + line = text.get("%d.0" % lineno, "%d.end" % lineno) + last = "%d.0" % lineno + + # Search back to beginning of paragraph (first blank line before) + lineno = first_lineno - 1 + line = text.get("%d.0" % lineno, "%d.end" % lineno) + while lineno > 0 and \ + get_comment_header(line)==comment_header and \ + not is_all_white(line[comment_header_len:]): + lineno = lineno - 1 + line = text.get("%d.0" % lineno, "%d.end" % lineno) + first = "%d.0" % (lineno+1) + + return first, last, comment_header, text.get(first, last) + +# This should perhaps be replaced with textwrap.wrap +def reformat_paragraph(data, limit): + """Return data reformatted to specified width (limit).""" + lines = data.split("\n") + i = 0 + n = len(lines) + while i < n and is_all_white(lines[i]): + i = i+1 + if i >= n: + return data + indent1 = get_indent(lines[i]) + if i+1 < n and not is_all_white(lines[i+1]): + indent2 = get_indent(lines[i+1]) + else: + indent2 = indent1 + new = lines[:i] + partial = indent1 + while i < n and not is_all_white(lines[i]): + # XXX Should take double space after period (etc.) into account + words = re.split("(\s+)", lines[i]) + for j in range(0, len(words), 2): + word = words[j] + if not word: + continue # Can happen when line ends in whitespace + if len((partial + word).expandtabs()) > limit and \ + partial != indent1: + new.append(partial.rstrip()) + partial = indent2 + partial = partial + word + " " + if j+1 < len(words) and words[j+1] != " ": + partial = partial + " " + i = i+1 + new.append(partial.rstrip()) + # XXX Should reformat remaining paragraphs as well + new.extend(lines[i:]) + return "\n".join(new) + +def reformat_comment(data, limit, comment_header): + """Return data reformatted to specified width with comment header.""" + + # Remove header from the comment lines + lc = len(comment_header) + data = "\n".join(line[lc:] for line in data.split("\n")) + # Reformat to maxformatwidth chars or a 20 char width, + # whichever is greater. + format_width = max(limit - len(comment_header), 20) + newdata = reformat_paragraph(data, format_width) + # re-split and re-insert the comment header. + newdata = newdata.split("\n") + # If the block ends in a \n, we dont want the comment prefix + # inserted after it. (Im not sure it makes sense to reformat a + # comment block that is not made of complete lines, but whatever!) + # Can't think of a clean solution, so we hack away + block_suffix = "" + if not newdata[-1]: + block_suffix = "\n" + newdata = newdata[:-1] + return '\n'.join(comment_header+line for line in newdata) + block_suffix + +def is_all_white(line): + """Return True if line is empty or all whitespace.""" + + return re.match(r"^\s*$", line) is not None + +def get_indent(line): + """Return the initial space or tab indent of line.""" + return re.match(r"^([ \t]*)", line).group() + +def get_comment_header(line): + """Return string with leading whitespace and '#' from line or ''. + + A null return indicates that the line is not a comment line. A non- + null return, such as ' #', will be used to find the other lines of + a comment block with the same indent. + """ + m = re.match(r"^([ \t]*#*)", line) + if m is None: return "" + return m.group(1) + +if __name__ == "__main__": + import unittest + unittest.main('idlelib.idle_test.test_formatparagraph', + verbosity=2, exit=False) diff --git a/contrib/tools/python/src/Lib/idlelib/GrepDialog.py b/contrib/tools/python/src/Lib/idlelib/GrepDialog.py new file mode 100644 index 00000000000..d86d50d360c --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/GrepDialog.py @@ -0,0 +1,159 @@ +from __future__ import print_function +import os +import fnmatch +import re # for htest +import sys +from Tkinter import StringVar, BooleanVar, Checkbutton # for GrepDialog +from Tkinter import Tk, Text, Button, SEL, END # for htest +from idlelib import SearchEngine +from idlelib.SearchDialogBase import SearchDialogBase +# Importing OutputWindow fails due to import loop +# EditorWindow -> GrepDialop -> OutputWindow -> EditorWindow + +def grep(text, io=None, flist=None): + root = text._root() + engine = SearchEngine.get(root) + if not hasattr(engine, "_grepdialog"): + engine._grepdialog = GrepDialog(root, engine, flist) + dialog = engine._grepdialog + searchphrase = text.get("sel.first", "sel.last") + dialog.open(text, searchphrase, io) + +class GrepDialog(SearchDialogBase): + + title = "Find in Files Dialog" + icon = "Grep" + needwrapbutton = 0 + + def __init__(self, root, engine, flist): + SearchDialogBase.__init__(self, root, engine) + self.flist = flist + self.globvar = StringVar(root) + self.recvar = BooleanVar(root) + + def open(self, text, searchphrase, io=None): + SearchDialogBase.open(self, text, searchphrase) + if io: + path = io.filename or "" + else: + path = "" + dir, base = os.path.split(path) + head, tail = os.path.splitext(base) + if not tail: + tail = ".py" + self.globvar.set(os.path.join(dir, "*" + tail)) + + def create_entries(self): + SearchDialogBase.create_entries(self) + self.globent = self.make_entry("In files:", self.globvar)[0] + + def create_other_buttons(self): + f = self.make_frame()[0] + + btn = Checkbutton(f, anchor="w", + variable=self.recvar, + text="Recurse down subdirectories") + btn.pack(side="top", fill="both") + btn.select() + + def create_command_buttons(self): + SearchDialogBase.create_command_buttons(self) + self.make_button("Search Files", self.default_command, 1) + + def default_command(self, event=None): + prog = self.engine.getprog() + if not prog: + return + path = self.globvar.get() + if not path: + self.top.bell() + return + from idlelib.OutputWindow import OutputWindow # leave here! + save = sys.stdout + try: + sys.stdout = OutputWindow(self.flist) + self.grep_it(prog, path) + finally: + sys.stdout = save + + def grep_it(self, prog, path): + dir, base = os.path.split(path) + list = self.findfiles(dir, base, self.recvar.get()) + list.sort() + self.close() + pat = self.engine.getpat() + print("Searching %r in %s ..." % (pat, path)) + hits = 0 + try: + for fn in list: + try: + with open(fn) as f: + for lineno, line in enumerate(f, 1): + if line[-1:] == '\n': + line = line[:-1] + if prog.search(line): + sys.stdout.write("%s: %s: %s\n" % + (fn, lineno, line)) + hits += 1 + except IOError as msg: + print(msg) + print(("Hits found: %s\n" + "(Hint: right-click to open locations.)" + % hits) if hits else "No hits.") + except AttributeError: + # Tk window has been closed, OutputWindow.text = None, + # so in OW.write, OW.text.insert fails. + pass + + def findfiles(self, dir, base, rec): + try: + names = os.listdir(dir or os.curdir) + except os.error as msg: + print(msg) + return [] + list = [] + subdirs = [] + for name in names: + fn = os.path.join(dir, name) + if os.path.isdir(fn): + subdirs.append(fn) + else: + if fnmatch.fnmatch(name, base): + list.append(fn) + if rec: + for subdir in subdirs: + list.extend(self.findfiles(subdir, base, rec)) + return list + + def close(self, event=None): + if self.top: + self.top.grab_release() + self.top.withdraw() + + +def _grep_dialog(parent): # htest # + from idlelib.PyShell import PyShellFileList + root = Tk() + root.title("Test GrepDialog") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) + + flist = PyShellFileList(root) + text = Text(root, height=5) + text.pack() + + def show_grep_dialog(): + text.tag_add(SEL, "1.0", END) + grep(text, flist=flist) + text.tag_remove(SEL, "1.0", END) + + button = Button(root, text="Show GrepDialog", command=show_grep_dialog) + button.pack() + root.mainloop() + +if __name__ == "__main__": + import unittest + unittest.main('idlelib.idle_test.test_grep', verbosity=2, exit=False) + + from idlelib.idle_test.htest import run + run(_grep_dialog) diff --git a/contrib/tools/python/src/Lib/idlelib/HyperParser.py b/contrib/tools/python/src/Lib/idlelib/HyperParser.py new file mode 100644 index 00000000000..6e45b161a46 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/HyperParser.py @@ -0,0 +1,255 @@ +"""Provide advanced parsing abilities for ParenMatch and other extensions. + +HyperParser uses PyParser. PyParser mostly gives information on the +proper indentation of code. HyperParser gives additional information on +the structure of code. +""" + +import string +import keyword +from idlelib import PyParse + +class HyperParser: + + def __init__(self, editwin, index): + "To initialize, analyze the surroundings of the given index." + + self.editwin = editwin + self.text = text = editwin.text + + parser = PyParse.Parser(editwin.indentwidth, editwin.tabwidth) + + 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 + + # Ascii chars that may be in a white space + _whitespace_chars = " \t\n\\" + # Ascii chars that may be in an identifier + _id_chars = string.ascii_letters + string.digits + "_" + # Ascii chars that may be the first char of an identifier + _id_first_chars = string.ascii_letters + "_" + + # Given a string and pos, return the number of chars in the + # identifier which ends at pos, or 0 if there is no such one. Saved + # words are not identifiers. + def _eat_identifier(self, str, limit, pos): + i = pos + while i > limit and str[i-1] in self._id_chars: + i -= 1 + if (i < pos and (str[i] not in self._id_first_chars or + keyword.iskeyword(str[i:pos]))): + i = pos + return pos - i + + 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/contrib/tools/python/src/Lib/idlelib/IOBinding.py b/contrib/tools/python/src/Lib/idlelib/IOBinding.py new file mode 100644 index 00000000000..2aba46e0df4 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/IOBinding.py @@ -0,0 +1,610 @@ +# changes by [email protected] +# - IOBinding.open() replaces the current window with the opened file, +# if the current window is both unmodified and unnamed +# - IOBinding.loadfile() interprets Windows, UNIX, and Macintosh +# end-of-line conventions, instead of relying on the standard library, +# which will only understand the local convention. + +import codecs +from codecs import BOM_UTF8 +import os +import pipes +import re +import sys +import tempfile + +from Tkinter import * +import tkFileDialog +import tkMessageBox +from SimpleDialog import SimpleDialog + +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() + +encoding = "ascii" +if sys.platform == 'win32': + # On Windows, we could use "mbcs". However, to give the user + # a portable encoding name, we need to find the code page + try: + encoding = locale.getdefaultlocale()[1] + codecs.lookup(encoding) + except LookupError: + pass +else: + try: + # Different things can fail here: the locale module may not be + # loaded, it may not offer nl_langinfo, or CODESET, or the + # resulting codeset may be unknown to Python. We ignore all + # these problems, falling back to ASCII + encoding = locale.nl_langinfo(locale.CODESET) + if encoding is None or encoding is '': + # situation occurs on Mac OS X + encoding = 'ascii' + codecs.lookup(encoding) + except (NameError, AttributeError, LookupError): + # Try getdefaultlocale well: it parses environment variables, + # which may give a clue. Unfortunately, getdefaultlocale has + # bugs that can cause ValueError. + try: + encoding = locale.getdefaultlocale()[1] + if encoding is None or encoding is '': + # situation occurs on Mac OS X + encoding = 'ascii' + codecs.lookup(encoding) + except (ValueError, LookupError): + pass + +encoding = encoding.lower() + +coding_re = re.compile(r'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)') +blank_re = re.compile(r'^[ \t\f]*(?:[#\r\n]|$)') + +class EncodingMessage(SimpleDialog): + "Inform user that an encoding declaration is needed." + def __init__(self, master, enc): + self.should_edit = False + + self.root = top = Toplevel(master) + top.bind("<Return>", self.return_event) + top.bind("<Escape>", self.do_ok) + top.protocol("WM_DELETE_WINDOW", self.wm_delete_window) + top.wm_title("I/O Warning") + top.wm_iconname("I/O Warning") + self.top = top + + l1 = Label(top, + text="Non-ASCII found, yet no encoding declared. Add a line like") + l1.pack(side=TOP, anchor=W) + l2 = Entry(top, font="courier") + l2.insert(0, "# -*- coding: %s -*-" % enc) + # For some reason, the text is not selectable anymore if the + # widget is disabled. + # l2['state'] = DISABLED + l2.pack(side=TOP, anchor = W, fill=X) + l3 = Label(top, text="to your file\n" + "See Language Reference, 2.1.4 Encoding declarations.\n" + "Choose OK to save this file as %s\n" + "Edit your general options to silence this warning" % enc) + l3.pack(side=TOP, anchor = W) + + buttons = Frame(top) + buttons.pack(side=TOP, fill=X) + # Both return and cancel mean the same thing: do nothing + self.default = self.cancel = 0 + b1 = Button(buttons, text="Ok", default="active", + command=self.do_ok) + b1.pack(side=LEFT, fill=BOTH, expand=1) + b2 = Button(buttons, text="Edit my file", + command=self.do_edit) + b2.pack(side=LEFT, fill=BOTH, expand=1) + + self._set_transient(master) + + def do_ok(self): + self.done(0) + + def do_edit(self): + self.done(1) + +def coding_spec(str): + """Return the encoding declaration according to PEP 263. + + Raise LookupError if the encoding is declared but unknown. + """ + # Only consider the first two lines + lst = str.split("\n", 2)[:2] + for line in lst: + match = coding_re.match(line) + if match is not None: + break + if not blank_re.match(line): + return None + else: + return None + name = match.group(1) + # Check whether the encoding is known + import codecs + try: + codecs.lookup(name) + except LookupError: + # The standard encoding error does not indicate the encoding + raise LookupError, "Unknown encoding "+name + return name + +class IOBinding: + + def __init__(self, editwin): + self.editwin = editwin + self.text = editwin.text + self.__id_open = self.text.bind("<<open-window-from-file>>", self.open) + self.__id_save = self.text.bind("<<save-window>>", self.save) + self.__id_saveas = self.text.bind("<<save-window-as-file>>", + self.save_as) + self.__id_savecopy = self.text.bind("<<save-copy-of-window-as-file>>", + self.save_a_copy) + self.fileencoding = None + self.__id_print = self.text.bind("<<print-window>>", self.print_window) + + def close(self): + # Undo command bindings + self.text.unbind("<<open-window-from-file>>", self.__id_open) + self.text.unbind("<<save-window>>", self.__id_save) + self.text.unbind("<<save-window-as-file>>",self.__id_saveas) + self.text.unbind("<<save-copy-of-window-as-file>>", self.__id_savecopy) + self.text.unbind("<<print-window>>", 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: + chars = f.read() + except IOError as msg: + tkMessageBox.showerror("I/O Error", str(msg), parent=self.text) + return False + + chars = self.decode(chars) + # We now convert all end-of-lines to '\n's + firsteol = self.eol_re.search(chars) + if firsteol: + self.eol_convention = firsteol.group(0) + if isinstance(self.eol_convention, unicode): + # Make sure it is an ASCII string + self.eol_convention = self.eol_convention.encode("ascii") + chars = self.eol_re.sub(r"\n", chars) + + self.text.delete("1.0", "end") + self.set_filename(None) + self.text.insert("1.0", chars) + self.reset_undo() + self.set_filename(filename) + self.text.mark_set("insert", "1.0") + self.text.yview("insert") + self.updaterecentfileslist(filename) + return True + + def decode(self, chars): + """Create a Unicode string + + If that fails, let Tcl try its best + """ + # Check presence of a UTF-8 signature first + if chars.startswith(BOM_UTF8): + try: + chars = chars[3:].decode("utf-8") + except UnicodeError: + # has UTF-8 signature, but fails to decode... + return chars + else: + # Indicates that this file originally had a BOM + self.fileencoding = BOM_UTF8 + return chars + # Next look for coding specification + try: + enc = coding_spec(chars) + 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 + if enc: + try: + return unicode(chars, enc) + except UnicodeError: + pass + # If it is ASCII, we need not to record anything + try: + return unicode(chars, 'ascii') + except UnicodeError: + pass + # Finally, try the locale's encoding. This is deprecated; + # the user should declare a non-ASCII encoding + try: + chars = unicode(chars, encoding) + self.fileencoding = encoding + except UnicodeError: + pass + return chars + + 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() + chars = self.encode(self.text.get("1.0", "end-1c")) + if self.eol_convention != "\n": + chars = chars.replace("\n", self.eol_convention) + try: + with open(filename, "wb") as f: + f.write(chars) + return True + except IOError as msg: + tkMessageBox.showerror("I/O Error", str(msg), + parent=self.text) + return False + + def encode(self, chars): + if isinstance(chars, str): + # This is either plain ASCII, or Tk was returning mixed-encoding + # text to us. Don't try to guess further. + return chars + # 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 + # If there is an encoding declared, try this first. + try: + enc = coding_spec(chars) + failed = None + except LookupError as msg: + failed = msg + enc = None + if enc: + try: + return chars.encode(enc) + except UnicodeError: + failed = "Invalid encoding '%s'" % enc + if failed: + tkMessageBox.showerror( + "I/O Error", + "%s. Saving as UTF-8" % failed, + parent = self.text) + # If there was a UTF-8 signature, use that. This should not fail + if self.fileencoding == BOM_UTF8 or failed: + return BOM_UTF8 + chars.encode("utf-8") + # Try the original file encoding next, if any + if self.fileencoding: + try: + return chars.encode(self.fileencoding) + except UnicodeError: + tkMessageBox.showerror( + "I/O Error", + "Cannot save this as '%s' anymore. Saving as UTF-8" \ + % self.fileencoding, + parent = self.text) + return BOM_UTF8 + chars.encode("utf-8") + # Nothing was declared, and we had not determined an encoding + # on loading. Recommend an encoding line. + config_encoding = idleConf.GetOption("main","EditorWindow", + "encoding") + if config_encoding == 'utf-8': + # User has requested that we save files as UTF-8 + return BOM_UTF8 + chars.encode("utf-8") + ask_user = True + try: + chars = chars.encode(encoding) + enc = encoding + if config_encoding == 'locale': + ask_user = False + except UnicodeError: + chars = BOM_UTF8 + chars.encode("utf-8") + enc = "utf-8" + if not ask_user: + return chars + dialog = EncodingMessage(self.editwin.top, enc) + dialog.go() + if dialog.num == 1: + # User asked us to edit the file + encline = "# -*- coding: %s -*-\n" % enc + firstline = self.text.get("1.0", "2.0") + if firstline.startswith("#!"): + # Insert encoding after #! line + self.text.insert("2.0", encline) + else: + self.text.insert("1.0", encline) + return self.encode(self.text.get("1.0", "end-1c")) + return chars + + def fixlastline(self): + c = self.text.get("end-2c") + if c != '\n': + self.text.insert("end-1c", "\n") + + def print_window(self, event): + confirm = tkMessageBox.askokcancel( + 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 % pipes.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) + if isinstance(filename, unicode): + filename = filename.encode(filesystemencoding) + 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 os.error: + 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) + if isinstance(filename, unicode): + filename = filename.encode(filesystemencoding) + return filename + + def updaterecentfileslist(self,filename): + "Update recent file list on all editor windows" + 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("<Control-o>", self.open) + self.text.bind('<Control-p>', self.printer) + self.text.bind("<Control-s>", self.save) + self.text.bind("<Alt-s>", self.saveas) + self.text.bind('<Control-c>', self.savecopy) + def get_saved(self): return 0 + def set_saved(self, flag): pass + def reset_undo(self): pass + def update_recent_files_list(self, filename): pass + def open(self, event): + self.text.event_generate("<<open-window-from-file>>") + def printer(self, event): + self.text.event_generate("<<print-window>>") + def save(self, event): + self.text.event_generate("<<save-window>>") + def saveas(self, event): + self.text.event_generate("<<save-window-as-file>>") + def savecopy(self, event): + self.text.event_generate("<<save-copy-of-window-as-file>>") + + 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/contrib/tools/python/src/Lib/idlelib/IdleHistory.py b/contrib/tools/python/src/Lib/idlelib/IdleHistory.py new file mode 100644 index 00000000000..078af290532 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/IdleHistory.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 <<history-next>> event (default Alt-N). + history_prev - Bound to <<history-prev>> 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("<<history-previous>>", self.history_prev) + text.bind("<<history-next>>", 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/contrib/tools/python/src/Lib/idlelib/MultiCall.py b/contrib/tools/python/src/Lib/idlelib/MultiCall.py new file mode 100644 index 00000000000..a157d7ad284 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/MultiCall.py @@ -0,0 +1,430 @@ +""" +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 string +import re +import Tkinter + +# the event type constants, which define the meaning of mc_type +MC_KEYPRESS=0; MC_KEYRELEASE=1; MC_BUTTONPRESS=2; MC_BUTTONRELEASE=3; +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]]) + +# 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: + self.widget.unbind(self.widgetinst, self.sequence, self.handlerid) + +# An int in range(1 << len(_modifiers)) represents a combination of modifiers +# (if the least significant bit is on, _modifiers[0] is on, and so on). +# _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: + self.widget.unbind(self.widgetinst, seq, id) + +# define the list of event types to be handled by MultiEvent. the order is +# compatible with the definition of event type constants. +_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 = string.split(sequence[1:-1], '-') + + modifiers = 0 + while words and words[0] in _modifier_names: + modifiers |= 1 << _modifier_names[words[0]] + del words[0] + + if words and words[0] in _type_names: + type = _type_names[words[0]] + del words[0] + else: + return None + + if _binder_classes[type] is _SimpleBinder: + if modifiers or words: + return None + 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) called." % (sequence, func, add) + 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) was called"%(repr(virtual),repr(sequences)) + 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 >> sys.stderr, "Seq. %s was added by Tkinter."%seq + 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 >> sys.stderr, "Seq. %s was deleted by Tkinter."%seq + 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: + self.__binders[triplet[1]].unbind(triplet, func) + + + _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("<<handler%d>>"%n[0], handler) + text.event_add("<<handler%d>>"%n[0], seq) + n[0] += 1 + bindseq("<Key>") + bindseq("<Control-Key>") + bindseq("<Alt-Key-a>") + bindseq("<Control-Key-a>") + bindseq("<Alt-Control-Key-a>") + bindseq("<Key-b>") + bindseq("<Control-Button-1>") + bindseq("<Button-2>") + bindseq("<Alt-Button-1>") + bindseq("<FocusOut>") + bindseq("<Enter>") + bindseq("<Leave>") + root.mainloop() + +if __name__ == "__main__": + from idlelib.idle_test.htest import run + run(_multi_call) diff --git a/contrib/tools/python/src/Lib/idlelib/MultiStatusBar.py b/contrib/tools/python/src/Lib/idlelib/MultiStatusBar.py new file mode 100644 index 00000000000..e3d59ee8c10 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/MultiStatusBar.py @@ -0,0 +1,47 @@ +from Tkinter import * + +class MultiStatusBar(Frame): + + def __init__(self, master=None, **kw): + if master is None: + master = Tk() + Frame.__init__(self, master, **kw) + self.labels = {} + + def set_label(self, name, text='', side=LEFT, width=0): + if name not in self.labels: + label = Label(self, borderwidth=0, anchor=W) + label.pack(side=side, pady=0, padx=4) + self.labels[name] = label + else: + label = self.labels[name] + if width != 0: + label.config(width=width) + label.config(text=text) + +def _multistatus_bar(parent): + root = Tk() + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d" %(x, y + 150)) + root.title("Test multistatus bar") + frame = Frame(root) + text = Text(frame) + text.pack() + msb = MultiStatusBar(frame) + msb.set_label("one", "hello") + msb.set_label("two", "world") + msb.pack(side=BOTTOM, fill=X) + + def change(): + msb.set_label("one", "foo") + msb.set_label("two", "bar") + + button = Button(root, text="Update status", command=change) + button.pack(side=BOTTOM) + frame.pack() + frame.mainloop() + root.mainloop() + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(_multistatus_bar) diff --git a/contrib/tools/python/src/Lib/idlelib/ObjectBrowser.py b/contrib/tools/python/src/Lib/idlelib/ObjectBrowser.py new file mode 100644 index 00000000000..e69365c144f --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/ObjectBrowser.py @@ -0,0 +1,156 @@ +# 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 repr 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 InstanceTreeItem(ObjectTreeItem): + def IsExpandable(self): + return True + def GetSubList(self): + sublist = ObjectTreeItem.GetSubList(self) + sublist.insert(0, + make_objecttreeitem("__class__ =", self.object.__class__)) + return sublist + +class ClassTreeItem(ObjectTreeItem): + def IsExpandable(self): + return True + 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 = self.object.keys() + try: + keys.sort() + except: + pass + return keys + +from types import * + +dispatch = { + IntType: AtomicObjectTreeItem, + LongType: AtomicObjectTreeItem, + FloatType: AtomicObjectTreeItem, + StringType: AtomicObjectTreeItem, + TupleType: SequenceTreeItem, + ListType: SequenceTreeItem, + DictType: DictTreeItem, + InstanceType: InstanceTreeItem, + ClassType: ClassTreeItem, +} + +def make_objecttreeitem(labeltext, object, setfunction=None): + 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/contrib/tools/python/src/Lib/idlelib/OutputWindow.py b/contrib/tools/python/src/Lib/idlelib/OutputWindow.py new file mode 100644 index 00000000000..63dc7376bcb --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/OutputWindow.py @@ -0,0 +1,149 @@ +from Tkinter import * +from idlelib.EditorWindow import EditorWindow +import re +import tkMessageBox +from idlelib import IOBinding + +class OutputWindow(EditorWindow): + + """An editor window that can serve as an output file. + + Also the future base class for the Python shell window. + This class has no input facilities. + """ + + def __init__(self, *args): + EditorWindow.__init__(self, *args) + self.text.bind("<<goto-file-line>>", self.goto_file_line) + + # Customize EditorWindow + + def ispythonsource(self, filename): + # No colorization needed + return 0 + + def short_title(self): + return "Output" + + def maybesave(self): + # Override base class method -- don't ask any questions + if self.get_saved(): + return "yes" + else: + return "no" + + # Act as output file + + def write(self, s, tags=(), mark="insert"): + # Tk assumes that byte strings are Latin-1; + # we assume that they are in the locale's encoding + if isinstance(s, str): + try: + s = unicode(s, IOBinding.encoding) + except UnicodeError: + # some other encoding; let Tcl deal with it + pass + self.text.insert(mark, s, tags) + self.text.see(mark) + self.text.update() + + def writelines(self, lines): + for line in lines: + self.write(line) + + def flush(self): + pass + + # Our own right-button menu + + rmenu_specs = [ + ("Cut", "<<cut>>", "rmenu_check_cut"), + ("Copy", "<<copy>>", "rmenu_check_copy"), + ("Paste", "<<paste>>", "rmenu_check_paste"), + (None, None, None), + ("Go to file/line", "<<goto-file-line>>", None), + ] + + file_line_pats = [ + # order of patterns matters + r'file "([^"]*)", line (\d+)', + r'([^\s]+)\((\d+)\)', + r'^(\s*\S.*?):\s*(\d+):', # Win filename, maybe starting with spaces + r'([^\s]+):\s*(\d+):', # filename or path, ltrim + r'^\s*(\S.*?):\s*(\d+):', # Win abs path with embedded spaces, ltrim + ] + + file_line_progs = None + + def goto_file_line(self, event=None): + if self.file_line_progs is None: + l = [] + for pat in self.file_line_pats: + l.append(re.compile(pat, re.IGNORECASE)) + self.file_line_progs = l + # x, y = self.event.x, self.event.y + # self.text.mark_set("insert", "@%d,%d" % (x, y)) + line = self.text.get("insert linestart", "insert lineend") + result = self._file_line_helper(line) + if not result: + # Try the previous line. This is handy e.g. in tracebacks, + # where you tend to right-click on the displayed source line + line = self.text.get("insert -1line linestart", + "insert -1line lineend") + result = self._file_line_helper(line) + if not result: + tkMessageBox.showerror( + "No special line", + "The line you point at doesn't look like " + "a valid file name followed by a line number.", + parent=self.text) + return + filename, lineno = result + edit = self.flist.open(filename) + edit.gotoline(lineno) + + def _file_line_helper(self, line): + for prog in self.file_line_progs: + match = prog.search(line) + if match: + filename, lineno = match.group(1, 2) + try: + f = open(filename, "r") + f.close() + break + except IOError: + continue + else: + return None + try: + return filename, int(lineno) + except TypeError: + return None + +# These classes are currently not used but might come in handy + +class OnDemandOutputWindow: + + tagdefs = { + # XXX Should use IdlePrefs.ColorPrefs + "stdout": {"foreground": "blue"}, + "stderr": {"foreground": "#007700"}, + } + + def __init__(self, flist): + self.flist = flist + self.owin = None + + def write(self, s, tags, mark): + if not self.owin: + self.setup() + self.owin.write(s, tags, mark) + + def setup(self): + self.owin = owin = OutputWindow(self.flist) + text = owin.text + for tag, cnf in self.tagdefs.items(): + if cnf: + text.tag_configure(tag, **cnf) + text.tag_raise('sel') + self.write = self.owin.write diff --git a/contrib/tools/python/src/Lib/idlelib/ParenMatch.py b/contrib/tools/python/src/Lib/idlelib/ParenMatch.py new file mode 100644 index 00000000000..47e10f3517b --- /dev/null +++ b/contrib/tools/python/src/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 # milliseconds + +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", "<<flash-paren>>"), + ]) + ] + STYLE = idleConf.GetOption('extensions','ParenMatch','style', + default='expression') + FLASH_DELAY = idleConf.GetOption('extensions','ParenMatch','flash-delay', + type='int',default=500) + HILITE_CONFIG = idleConf.GetHighlight(idleConf.CurrentTheme(),'hilite') + BELL = idleConf.GetOption('extensions','ParenMatch','bell', + type='bool',default=1) + + RESTORE_VIRTUAL_EVENT_NAME = "<<parenmatch-check-restore>>" + # We want the restore event be called before the usual return and + # backspace events. + RESTORE_SEQUENCES = ("<KeyPress>", "<ButtonPress>", + "<Key-Return>", "<Key-BackSpace>") + + 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/contrib/tools/python/src/Lib/idlelib/PathBrowser.py b/contrib/tools/python/src/Lib/idlelib/PathBrowser.py new file mode 100644 index 00000000000..ae26714fd59 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/PathBrowser.py @@ -0,0 +1,105 @@ +import os +import sys +import imp + +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 os.error: + 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): + 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 = imp.get_suffixes() + sorted = [] + for suff, mode, flag 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/contrib/tools/python/src/Lib/idlelib/Percolator.py b/contrib/tools/python/src/Lib/idlelib/Percolator.py new file mode 100644 index 00000000000..e0e8cade0dd --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/Percolator.py @@ -0,0 +1,103 @@ +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): + 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) + root = tk.Tk() + root.title("Test Percolator") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) + text = tk.Text(root) + p = Percolator(text) + t1 = Tracer("t1") + t2 = Tracer("t2") + + def toggle1(): + if var1.get() == 0: + var1.set(1) + p.insertfilter(t1) + elif var1.get() == 1: + var1.set(0) + p.removefilter(t1) + + def toggle2(): + if var2.get() == 0: + var2.set(1) + p.insertfilter(t2) + elif var2.get() == 1: + var2.set(0) + p.removefilter(t2) + + text.pack() + var1 = tk.IntVar() + cb1 = tk.Checkbutton(root, text="Tracer1", command=toggle1, variable=var1) + cb1.pack() + var2 = tk.IntVar() + cb2 = tk.Checkbutton(root, text="Tracer2", command=toggle2, variable=var2) + cb2.pack() + +if __name__ == "__main__": + from idlelib.idle_test.htest import run + run(_percolator) diff --git a/contrib/tools/python/src/Lib/idlelib/PyParse.py b/contrib/tools/python/src/Lib/idlelib/PyParse.py new file mode 100644 index 00000000000..1a9db6743ce --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/PyParse.py @@ -0,0 +1,594 @@ +import re +import sys + +# 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 + +# Build translation table to map uninteresting chars to "x", open +# brackets to "(", and close brackets to ")". + +_tran = ['x'] * 256 +for ch in "({[": + _tran[ord(ch)] = '(' +for ch in ")}]": + _tran[ord(ch)] = ')' +for ch in "\"'\\\n#": + _tran[ord(ch)] = ch +_tran = ''.join(_tran) +del ch + +try: + UnicodeType = type(unicode("")) +except NameError: + UnicodeType = None + +class Parser: + + def __init__(self, indentwidth, tabwidth): + self.indentwidth = indentwidth + self.tabwidth = tabwidth + + def set_str(self, str): + assert len(str) == 0 or str[-1] == '\n' + if type(str) is UnicodeType: + # The parse functions have no idea what to do with Unicode, so + # replace all Unicode characters with "x". This is "safe" + # so long as the only characters germane to parsing the structure + # of Python are 7-bit ASCII. It's *necessary* because Unicode + # strings don't have a .translate() method that supports + # deletechars. + uniphooey = str + str = [] + push = str.append + for raw in map(ord, uniphooey): + push(raw < 127 and chr(raw) or "x") + str = "".join(str) + self.str = str + self.study_level = 0 + + # Return index of a good place to begin parsing, as close to the + # end of the string as possible. This will be the start of some + # popular stmt like "if" or "def". Return None if none found: + # the caller should pass more prior context then, if possible, or + # if not (the entire program text up until the point of interest + # has already been tried) pass 0 to set_lo. + # + # This will be reliable iff given a reliable is_char_in_string + # function, meaning that when it says "no", it's absolutely + # guaranteed that the char is not in a string. + + def find_good_parse_start(self, is_char_in_string=None, + _synchre=_synchre): + 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:] + + # As quickly as humanly possible <wink>, find the line numbers (0- + # based) of the non-continuation lines. + # Creates self.{goodlines, continuation}. + + def _study1(self): + if self.study_level >= 1: + return + self.study_level = 1 + + # 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(_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/contrib/tools/python/src/Lib/idlelib/PyShell.py b/contrib/tools/python/src/Lib/idlelib/PyShell.py new file mode 100644 index 00000000000..2ea7e6b939a --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/PyShell.py @@ -0,0 +1,1643 @@ +#! /usr/bin/env python +from __future__ import print_function + +import os +import os.path +import sys +import string +import getopt +import re +import socket +import time +import threading +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 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 +from idlelib import IOBinding + +IDENTCHARS = string.ascii_letters + string.digits + "_" +HOST = '127.0.0.1' # python execution server on localhost loopback +PORT = 0 # someday pass in host, port for remote debug capability + +try: + from signal import SIGTERM +except ImportError: + SIGTERM = 15 + +# Override warnings module to write to warning_stream. Initialize to send IDLE +# internal warnings to the console. ScriptBinding.check_syntax() will +# temporarily redirect the stream to the shell window to display warnings when +# checking user's code. +warning_stream = sys.__stderr__ # None, at least on Windows, if no console. +import warnings + +def idle_formatwarning(message, category, filename, lineno, line=None): + """Format warnings the IDLE way.""" + + s = "\nWarning (from warnings module):\n" + s += ' File \"%s\", line %s\n' % (filename, lineno) + if line is None: + line = linecache.getline(filename, lineno) + line = line.strip() + if line: + s += " %s\n" % line + s += "%s: %s\n" % (category.__name__, message) + return s + +def idle_showwarning( + message, category, filename, lineno, file=None, line=None): + """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, IOError): + 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 <pyshell#...> entries + + Rather than repeating the linecache code, patch it to save the + <pyshell#...> 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("<<set-breakpoint-here>>", self.set_breakpoint_here) + self.text.bind("<<clear-breakpoint-here>>", self.clear_breakpoint_here) + self.text.bind("<<open-python-shell>>", 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", "<<cut>>", "rmenu_check_cut"), + ("Copy", "<<copy>>", "rmenu_check_copy"), + ("Paste", "<<paste>>", "rmenu_check_paste"), + ("Set Breakpoint", "<<set-breakpoint-here>>", None), + ("Clear Breakpoint", "<<clear-breakpoint-here>>", None) + ] + + def color_breakpoint_text(self, color=True): + "Turn colorizing of breakpoint text on or off" + if self.io is None: + # possible due to update in restore_file_breaks + return + if color: + theme = idleConf.CurrentTheme() + cfg = idleConf.GetHighlight(theme, "break") + else: + cfg = {'foreground': '', 'background': ''} + self.text.tag_config('BREAK', cfg) + + def set_breakpoint(self, lineno): + text = self.text + filename = self.io.filename + text.tag_add("BREAK", "%d.0" % lineno, "%d.0" % (lineno+1)) + try: + 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 old_file: + lines = old_file.readlines() + except IOError: + 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 IOError 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): + lines = open(self.breakpointPath,"r").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 + rpcpid = None + + def spawn_subprocess(self): + if self.subprocess_arglist is None: + self.subprocess_arglist = self.build_subprocess_arglist() + args = self.subprocess_arglist + self.rpcpid = os.spawnv(os.P_NOWAIT, sys.executable, args) + + def build_subprocess_arglist(self): + assert (self.port!=0), ( + "Socket should have been assigned a port number.") + w = ['-W' + s for s in sys.warnoptions] + if 1/2 > 0: # account for new division + w.append('-Qnew') + # Maybe IDLE is installed and is being accessed via sys.path, + # or maybe it's not installed and the idle.py script is being + # run from the IDLE source directory. + del_exitf = idleConf.GetOption('main', 'General', 'delete-exitfunc', + default=False, type='bool') + if __name__ == 'idlelib.PyShell': + command = "__import__('idlelib.run').run.main(%r)" % (del_exitf,) + else: + command = "__import__('run').main(%r)" % (del_exitf,) + if sys.platform[:3] == 'win' and ' ' in sys.executable: + # handle embedded space in path by quoting the argument + decorated_exec = '"%s"' % sys.executable + else: + decorated_exec = sys.executable + return [decorated_exec] + w + ["-c", command, str(self.port)] + + def start_subprocess(self): + addr = (HOST, self.port) + # 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 socket.error: + 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.unix_terminate() + 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.close() + except AttributeError: # no socket + pass + self.unix_terminate() + self.tkconsole.executing = False + self.rpcclt = None + + def unix_terminate(self): + "UNIX: make sure subprocess is terminated and collect status" + if hasattr(os, 'kill'): + try: + os.kill(self.rpcpid, SIGTERM) + except OSError: + # process already terminated: + return + else: + try: + os.waitpid(self.rpcpid, 0) + except OSError: + return + + def transfer_path(self, with_cwd=False): + if with_cwd: # Issue 13506 + 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, IOError, 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("<<toggle-jit-stack-viewer>>"): + 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: + source = open(filename, "r").read() + try: + code = compile(source, filename, "exec", dont_inherit=True) + except (OverflowError, SyntaxError): + self.tkconsole.resetoutput() + print('*** Error in script or command!\n' + 'Traceback (most recent call last):', + 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) + if isinstance(source, unicode) and IOBinding.encoding != 'utf-8': + try: + source = '# -*- coding: %s -*-\n%s' % ( + IOBinding.encoding, + source.encode(IOBinding.encoding)) + except UnicodeError: + self.tkconsole.resetoutput() + self.write("Unsupported characters in input\n") + return + try: + # InteractiveInterpreter.runsource() calls its runcode() method, + # which is overridden (see below) + return InteractiveInterpreter.runsource(self, source, filename) + finally: + if self.save_warnings_filters is not None: + warnings.filters[:] = self.save_warnings_filters + self.save_warnings_filters = None + + def stuffsource(self, source): + "Stuff source in the filename cache" + filename = "<pyshell#%d>" % 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): + """Extend base class method: Add Colorizing + + Color the offending position instead of printing it and pointing at it + with a caret. + + """ + text = self.tkconsole.text + stuff = self.unpackerror() + if stuff: + msg, lineno, offset, line = stuff + if lineno == 1: + pos = "iomark + %d chars" % (offset-1) + else: + pos = "iomark linestart + %d lines + %d chars" % \ + (lineno-1, offset-1) + text.tag_add("ERROR", pos) + text.see(pos) + char = text.get(pos) + if char and char in IDENTCHARS: + text.tag_add("ERROR", pos + " wordstart", pos) + self.tkconsole.resetoutput() + self.write("SyntaxError: %s\n" % str(msg)) + else: + self.tkconsole.resetoutput() + InteractiveInterpreter.showsyntaxerror(self, filename) + self.tkconsole.showprompt() + + def unpackerror(self): + type, value, tb = sys.exc_info() + ok = type is SyntaxError + if ok: + try: + msg, (dummy_filename, lineno, offset, line) = value + if not offset: + offset = 0 + except: + ok = 0 + if ok: + return msg, lineno, offset, line + else: + return None + + def showtraceback(self): + "Extend base class method to reset output properly" + self.tkconsole.resetoutput() + self.checklinecache() + InteractiveInterpreter.showtraceback(self) + if self.tkconsole.getvar("<<toggle-jit-stack-viewer>>"): + self.tkconsole.open_stack_viewer() + + def checklinecache(self): + c = linecache.cache + for key in 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 in 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 in 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" + 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("<<newline-and-indent>>", self.enter_callback) + text.bind("<<plain-newline-and-indent>>", self.linefeed_callback) + text.bind("<<interrupt-execution>>", self.cancel_callback) + text.bind("<<end-of-file>>", self.eof_callback) + text.bind("<<open-stack-viewer>>", self.open_stack_viewer) + text.bind("<<toggle-debugger>>", self.toggle_debugger) + text.bind("<<toggle-jit-stack-viewer>>", self.toggle_jit_stack_viewer) + if use_subprocess: + text.bind("<<view-restart>>", self.view_restart_mark) + text.bind("<<restart-shell>>", 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 + # + 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("<<toggle-debugger>>", 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 "help", "copyright", "credits" or "license()" for more information.' + + def begin(self): + self.resetoutput() + if use_subprocess: + nosub = '' + client = self.interp.start_subprocess() + if not client: + self.close() + return False + else: + nosub = "==== No Subprocess ====" + self.write("Python %s on %s\n%s\n%s" % + (sys.version, sys.platform, self.COPYRIGHT, nosub)) + self.text.focus_force() + self.showprompt() + import Tkinter + Tkinter._default_root = None # 03Jan04 KBK What's this? + 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" + if isinstance(line, unicode): + from idlelib import IOBinding + try: + line = line.encode(IOBinding.encoding) + except UnicodeError: + pass + self.resetoutput() + if self.canceled: + self.canceled = 0 + 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() in raw_input() + 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() + sys.stdout.softspace = 0 + + def write(self, s, tags=()): + try: + self.text.mark_gravity("iomark", "right") + OutputWindow.write(self, s, tags, "iomark") + self.text.mark_gravity("iomark", "left") + except: + pass + if self.canceled: + self.canceled = 0 + if not use_subprocess: + raise KeyboardInterrupt + + 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(PyShell, self).rmenu_check_cut() + + def rmenu_check_paste(self): + if self.text.compare('insert', '<', 'iomark'): + return 'disabled' + return super(PyShell, self).rmenu_check_paste() + +class PseudoFile(io.TextIOBase): + + def __init__(self, shell, tags, encoding=None): + self.shell = shell + self.tags = tags + self.softspace = 0 + self._encoding = encoding + + @property + def encoding(self): + return self._encoding + + @property + def name(self): + return '<%s>' % self.tags + + def isatty(self): + return True + + +class PseudoOutputFile(PseudoFile): + + def writable(self): + return True + + def write(self, s): + if self.closed: + raise ValueError("write to closed file") + if type(s) not in (unicode, str, bytearray): + # See issue #19481 + if isinstance(s, unicode): + s = unicode.__getitem__(s, slice(None)) + elif isinstance(s, str): + s = str.__str__(s) + elif isinstance(s, bytearray): + s = bytearray.__str__(s) + else: + raise TypeError('must be string, not ' + type(s).__name__) + return self.shell.write(s, self.tags) + + +class PseudoInputFile(PseudoFile): + + def __init__(self, shell, tags, encoding=None): + PseudoFile.__init__(self, shell, tags, encoding) + self._line_buffer = '' + + def readable(self): + return True + + def read(self, size=-1): + if self.closed: + raise ValueError("read from closed file") + if size is None: + size = -1 + elif not isinstance(size, (int, long)): + raise TypeError('must be int, not ' + type(size).__name__) + result = self._line_buffer + self._line_buffer = '' + if size < 0: + while True: + line = self.shell.readline() + if not line: break + result += line + else: + while len(result) < size: + line = self.shell.readline() + if not line: break + result += line + self._line_buffer = result[size:] + result = result[:size] + return result + + def readline(self, size=-1): + if self.closed: + raise ValueError("read from closed file") + if size is None: + size = -1 + elif not isinstance(size, (int, long)): + raise TypeError('must be int, not ' + type(size).__name__) + line = self._line_buffer or self.shell.readline() + if size < 0: + size = len(line) + eol = line.find('\n', 0, size) + if eol >= 0: + size = eol + 1 + self._line_buffer = line[size:] + return line[:size] + + def close(self): + self.shell.close() + + +def fix_x11_paste(root): + "Make paste replace selection on x11. See issue #5124." + if root._windowingsystem == 'x11': + for cls in 'Text', 'Entry', 'Spinbox': + root.bind_class( + cls, + '<<Paste>>', + 'catch {%W delete sel.first sel.last}\n' + + root.bind_class(cls, '<<Paste>>')) + + +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 (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': + use_subprocess = False + if o == '-r': + script = a + if os.path.isfile(script): + pass + else: + print("No script file: ", script, file=sys.stderr) + 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 dir not in sys.path: + sys.path.insert(0, dir) + else: + dir = os.getcwd() + if not dir in sys.path: + sys.path.insert(0, dir) + # check the IDLE settings configuration (but command line overrides) + edit_start = idleConf.GetOption('main', 'General', + '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") + root.withdraw() + from idlelib.run import fix_scaling + fix_scaling(root) + + # 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 and sys.platform != 'darwin': + ext = '.png' if TkVersion >= 8.6 else '.gif' + iconfiles = [os.path.join(icondir, 'idle_%d%s' % (size, ext)) + for size in (16, 32, 48)] + icons = [PhotoImage(file=iconfile) for iconfile in iconfiles] + root.tk.call('wm', 'iconphoto', str(root), "-default", *icons) + + fixwordbreaks(root) + fix_x11_paste(root) + flist = PyShellFileList(root) + macosxSupport.setupApp(root, flist) + + if macosxSupport.isAquaTk(): + # There are some screwed up <2> class bindings for text + # widgets defined in Tk which we need to do away with. + # See issue #24801. + root.unbind_class('Text', '<B2>') + root.unbind_class('Text', '<B2-Motion>') + root.unbind_class('Text', '<<PasteSelection>>') + + if enable_edit: + if not (cmd or script): + for filename in args[:]: + 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/contrib/tools/python/src/Lib/idlelib/RemoteDebugger.py b/contrib/tools/python/src/Lib/idlelib/RemoteDebugger.py new file mode 100644 index 00000000000..8c71a217461 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/RemoteDebugger.py @@ -0,0 +1,379 @@ +"""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): + ##print >>sys.__stderr__, "get_stack(%r, %r)" % (fid, tbid) + frame = frametable[fid] + if tbid is None: + tb = None + else: + tb = tracebacktable[tbid] + stack, i = self.idb.get_stack(frame, tb) + ##print >>sys.__stderr__, "get_stack() ->", stack + stack = [(wrap_frame(frame2), k) for frame2, k in stack] + ##print >>sys.__stderr__, "get_stack() ->", stack + return stack, i + + def run(self, cmd): + 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): + dict = dicttable[did] + return dict.keys() + + def dict_item(self, did, key): + dict = dicttable[did] + value = dict[key] + value = repr(value) + 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,), {}) + + def __getitem__(self, key): + return self._conn.remotecall(self._oid, "dict_item", + (self._did, key), {}) + + def __getattr__(self, name): + ##print >>sys.__stderr__, "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/contrib/tools/python/src/Lib/idlelib/RemoteObjectBrowser.py b/contrib/tools/python/src/Lib/idlelib/RemoteObjectBrowser.py new file mode 100644 index 00000000000..43e2c68f30c --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/RemoteObjectBrowser.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): + list = self.__item._GetSubList() + return map(remote_object_tree_item, 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): + list = self.sockio.remotecall(self.oid, "_GetSubList", (), {}) + return [StubObjectTreeItem(self.sockio, oid) for oid in list] diff --git a/contrib/tools/python/src/Lib/idlelib/ReplaceDialog.py b/contrib/tools/python/src/Lib/idlelib/ReplaceDialog.py new file mode 100644 index 00000000000..66a871a947e --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/ReplaceDialog.py @@ -0,0 +1,220 @@ +from Tkinter import * + +from idlelib import SearchEngine +from idlelib.SearchDialogBase import SearchDialogBase +import re + + +def replace(text): + 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): + 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): + 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): + 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): + 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): + 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): + root = Tk() + root.title("Test ReplaceDialog") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) + + # mock undo delegator methods + def undo_block_start(): + pass + + def undo_block_stop(): + pass + + text = Text(root) + text.undo_block_start = undo_block_start + text.undo_block_stop = undo_block_stop + text.pack() + text.insert("insert","This is a sample string.\n"*10) + + def show_replace(): + text.tag_add(SEL, "1.0", END) + replace(text) + text.tag_remove(SEL, "1.0", END) + + button = Button(root, text="Replace", command=show_replace) + button.pack() + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(_replace_dialog) diff --git a/contrib/tools/python/src/Lib/idlelib/RstripExtension.py b/contrib/tools/python/src/Lib/idlelib/RstripExtension.py new file mode 100644 index 00000000000..2ce3c7eafe5 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/RstripExtension.py @@ -0,0 +1,33 @@ +'Provides "Strip trailing whitespace" under the "Format" menu.' + +class RstripExtension: + + menudefs = [ + ('format', [None, ('Strip trailing whitespace', '<<do-rstrip>>'), ] ), ] + + def __init__(self, editwin): + self.editwin = editwin + self.editwin.text.bind("<<do-rstrip>>", self.do_rstrip) + + def do_rstrip(self, event=None): + + text = self.editwin.text + undo = self.editwin.undo + + undo.undo_block_start() + + end_line = int(float(text.index('end'))) + for cur in range(1, end_line): + txt = text.get('%i.0' % cur, '%i.end' % cur) + raw = len(txt) + cut = len(txt.rstrip()) + # Since text.delete() marks file as changed, even if not, + # only call it when needed to actually delete something. + if cut < raw: + text.delete('%i.%i' % (cur, cut), '%i.end' % cur) + + undo.undo_block_stop() + +if __name__ == "__main__": + import unittest + unittest.main('idlelib.idle_test.test_rstrip', verbosity=2, exit=False) diff --git a/contrib/tools/python/src/Lib/idlelib/ScriptBinding.py b/contrib/tools/python/src/Lib/idlelib/ScriptBinding.py new file mode 100644 index 00000000000..0309a8a8e20 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/ScriptBinding.py @@ -0,0 +1,222 @@ +"""Extension to execute code outside the Python shell window. + +This adds the following commands: + +- Check module does a full syntax check of the current module. + It also runs the tabnanny to catch any inconsistent tabs. + +- Run module executes the module's code in the __main__ namespace. The window + must have been saved previously. The module is added to sys.modules, and is + also added to the __main__ namespace. + +XXX GvR Redesign this interface (yet again) as follows: + +- Present a dialog box for ``Run Module'' + +- Allow specify command line arguments in the dialog box + +""" + +import os +import re +import string +import tabnanny +import tokenize +import tkMessageBox +from idlelib import PyShell + +from idlelib.configHandler import idleConf +from idlelib import macosxSupport + +IDENTCHARS = string.ascii_letters + string.digits + "_" + +indent_message = """Error: Inconsistent indentation detected! + +1) Your indentation is outright incorrect (easy to fix), OR + +2) Your indentation mixes tabs and spaces. + +To fix case 2, change all tabs to spaces by using Edit->Select All followed \ +by Format->Untabify Region and specify the number of columns used by each tab. +""" + +class ScriptBinding: + + menudefs = [ + ('run', [None, + ('Check Module', '<<check-module>>'), + ('Run Module', '<<run-module>>'), ]), ] + + def __init__(self, editwin): + self.editwin = editwin + # Provide instance variables referenced by Debugger + # XXX This should be done differently + self.flist = self.editwin.flist + self.root = self.editwin.root + + if macosxSupport.isCocoaTk(): + self.editwin.text_frame.bind('<<run-module-event-2>>', self._run_module_event) + + def check_module_event(self, event): + filename = self.getfilename() + if not filename: + return 'break' + if not self.checksyntax(filename): + return 'break' + if not self.tabnanny(filename): + return 'break' + + def tabnanny(self, filename): + f = open(filename, 'r') + try: + tabnanny.process_tokens(tokenize.generate_tokens(f.readline)) + except tokenize.TokenError as msg: + msgtxt, (lineno, start) = msg.args + self.editwin.gotoline(lineno) + self.errorbox("Tabnanny Tokenizing Error", + "Token Error: %s" % msgtxt) + return False + except tabnanny.NannyNag as nag: + # The error messages from tabnanny are too confusing... + self.editwin.gotoline(nag.get_lineno()) + self.errorbox("Tab/space error", indent_message) + return False + return True + + def checksyntax(self, filename): + self.shell = shell = self.flist.open_shell() + saved_stream = shell.get_warning_stream() + shell.set_warning_stream(shell.stderr) + with open(filename, 'r') as f: + source = f.read() + if '\r' in source: + source = re.sub(r"\r\n", "\n", source) + source = re.sub(r"\r", "\n", source) + if source and source[-1] != '\n': + source = source + '\n' + text = self.editwin.text + text.tag_remove("ERROR", "1.0", "end") + try: + try: + # If successful, return the compiled code + return compile(source, filename, "exec") + except (SyntaxError, OverflowError, ValueError) as err: + try: + msg, (errorfilename, lineno, offset, line) = err + if not errorfilename: + err.args = msg, (filename, lineno, offset, line) + err.filename = filename + self.colorize_syntax_error(msg, lineno, offset) + except: + msg = "*** " + str(err) + self.errorbox("Syntax error", + "There's an error in your program:\n" + msg) + return False + finally: + shell.set_warning_stream(saved_stream) + + def colorize_syntax_error(self, msg, lineno, offset): + text = self.editwin.text + pos = "0.0 + %d lines + %d chars" % (lineno-1, offset-1) + text.tag_add("ERROR", pos) + char = text.get(pos) + if char and char in IDENTCHARS: + text.tag_add("ERROR", pos + " wordstart", pos) + if '\n' == text.get(pos): # error at line end + text.mark_set("insert", pos) + else: + text.mark_set("insert", pos + "+1c") + text.see(pos) + + def run_module_event(self, event): + """Run the module after setting up the environment. + + First check the syntax. If OK, make sure the shell is active and + then transfer the arguments, set the run environment's working + directory to the directory of the module being executed and also + add that directory to its sys.path if not already included. + + """ + filename = self.getfilename() + if not filename: + return 'break' + code = self.checksyntax(filename) + if not code: + return 'break' + if not self.tabnanny(filename): + return 'break' + interp = self.shell.interp + if PyShell.use_subprocess: + interp.restart_subprocess(with_cwd=False, filename=code.co_filename) + dirname = os.path.dirname(filename) + # XXX Too often this discards arguments the user just set... + interp.runcommand("""if 1: + __file__ = {filename!r} + import sys as _sys + from os.path import basename as _basename + if (not _sys.argv or + _basename(_sys.argv[0]) != _basename(__file__)): + _sys.argv = [__file__] + import os as _os + _os.chdir({dirname!r}) + del _sys, _basename, _os + \n""".format(filename=filename, dirname=dirname)) + interp.prepend_syspath(filename) + # XXX KBK 03Jul04 When run w/o subprocess, runtime warnings still + # go to __stderr__. With subprocess, they go to the shell. + # Need to change streams in PyShell.ModifiedInterpreter. + interp.runcode(code) + return 'break' + + if macosxSupport.isCocoaTk(): + # Tk-Cocoa in MacOSX is broken until at least + # Tk 8.5.9, and without this rather + # crude workaround IDLE would hang when a user + # tries to run a module using the keyboard shortcut + # (the menu item works fine). + _run_module_event = run_module_event + + def run_module_event(self, event): + self.editwin.text_frame.after(200, + lambda: self.editwin.text_frame.event_generate('<<run-module-event-2>>')) + return 'break' + + def getfilename(self): + """Get source filename. If not saved, offer to save (or create) file + + The debugger requires a source file. Make sure there is one, and that + the current version of the source buffer has been saved. If the user + declines to save or cancels the Save As dialog, return None. + + If the user has configured IDLE for Autosave, the file will be + silently saved if it already exists and is dirty. + + """ + filename = self.editwin.io.filename + if not self.editwin.get_saved(): + autosave = idleConf.GetOption('main', 'General', + 'autosave', type='bool') + if autosave and filename: + self.editwin.io.save(None) + else: + confirm = self.ask_save_dialog() + self.editwin.text.focus_set() + if confirm: + self.editwin.io.save(None) + filename = self.editwin.io.filename + else: + filename = None + return filename + + def ask_save_dialog(self): + msg = "Source Must Be Saved\n" + 5*' ' + "OK to Save?" + confirm = tkMessageBox.askokcancel(title="Save Before Run or Check", + message=msg, + default=tkMessageBox.OK, + parent=self.editwin.text) + return confirm + + def errorbox(self, title, message): + # XXX This should really be a function of EditorWindow... + tkMessageBox.showerror(title, message, parent=self.editwin.text) + self.editwin.text.focus_set() diff --git a/contrib/tools/python/src/Lib/idlelib/ScrolledList.py b/contrib/tools/python/src/Lib/idlelib/ScrolledList.py new file mode 100644 index 00000000000..fd9f0ff5e26 --- /dev/null +++ b/contrib/tools/python/src/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("<ButtonRelease-1>", self.click_event) + listbox.bind("<Double-ButtonRelease-1>", self.double_click_event) + if macosxSupport.isAquaTk(): + listbox.bind("<ButtonPress-2>", self.popup_event) + listbox.bind("<Control-Button-1>", self.popup_event) + else: + listbox.bind("<ButtonPress-3>", self.popup_event) + listbox.bind("<Key-Up>", self.up_event) + listbox.bind("<Key-Down>", self.down_event) + # Mark as empty + 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/contrib/tools/python/src/Lib/idlelib/SearchDialog.py b/contrib/tools/python/src/Lib/idlelib/SearchDialog.py new file mode 100644 index 00000000000..043168af002 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/SearchDialog.py @@ -0,0 +1,89 @@ +from Tkinter import * + +from idlelib import SearchEngine +from idlelib.SearchDialogBase import SearchDialogBase + +def _setup(text): + root = text._root() + engine = SearchEngine.get(root) + if not hasattr(engine, "_searchdialog"): + engine._searchdialog = SearchDialog(root, engine) + return engine._searchdialog + +def find(text): + pat = text.get("sel.first", "sel.last") + return _setup(text).open(text,pat) + +def find_again(text): + return _setup(text).find_again(text) + +def find_selection(text): + return _setup(text).find_selection(text) + +class SearchDialog(SearchDialogBase): + + def create_widgets(self): + SearchDialogBase.create_widgets(self) + self.make_button("Find Next", self.default_command, 1) + + def default_command(self, event=None): + if not self.engine.getprog(): + return + self.find_again(self.text) + + def find_again(self, text): + if not self.engine.getpat(): + self.open(text) + return False + if not self.engine.getprog(): + return False + res = self.engine.search_text(text) + if res: + line, m = res + i, j = m.span() + first = "%d.%d" % (line, i) + last = "%d.%d" % (line, j) + try: + selfirst = text.index("sel.first") + sellast = text.index("sel.last") + if selfirst == first and sellast == last: + text.bell() + return False + except TclError: + pass + text.tag_remove("sel", "1.0", "end") + text.tag_add("sel", first, last) + text.mark_set("insert", self.engine.isback() and first or last) + text.see("insert") + return True + else: + text.bell() + return False + + def find_selection(self, text): + pat = text.get("sel.first", "sel.last") + if pat: + self.engine.setcookedpat(pat) + return self.find_again(text) + +def _search_dialog(parent): + root = Tk() + root.title("Test SearchDialog") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) + text = Text(root) + text.pack() + text.insert("insert","This is a sample string.\n"*10) + + def show_find(): + text.tag_add(SEL, "1.0", END) + s = _setup(text) + s.open(text) + text.tag_remove(SEL, "1.0", END) + + button = Button(root, text="Search", command=show_find) + button.pack() + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(_search_dialog) diff --git a/contrib/tools/python/src/Lib/idlelib/SearchDialogBase.py b/contrib/tools/python/src/Lib/idlelib/SearchDialogBase.py new file mode 100644 index 00000000000..651e7f4a3fc --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/SearchDialogBase.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("<Return>", self.default_command) + top.bind("<Escape>", 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/contrib/tools/python/src/Lib/idlelib/SearchEngine.py b/contrib/tools/python/src/Lib/idlelib/SearchEngine.py new file mode 100644 index 00000000000..ad431300e5e --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/SearchEngine.py @@ -0,0 +1,233 @@ +'''Define SearchEngine for search dialogs.''' +import re +from Tkinter import StringVar, BooleanVar, TclError +import 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/contrib/tools/python/src/Lib/idlelib/StackViewer.py b/contrib/tools/python/src/Lib/idlelib/StackViewer.py new file mode 100644 index 00000000000..555a08c2c00 --- /dev/null +++ b/contrib/tools/python/src/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("<locals>", frame.f_locals, self.flist) + sublist.append(item) + item = VariablesTreeItem("<globals>", 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 self.object.keys() + +def _stack_viewer(parent): # htest # + 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/contrib/tools/python/src/Lib/idlelib/ToolTip.py b/contrib/tools/python/src/Lib/idlelib/ToolTip.py new file mode 100644 index 00000000000..11136c44295 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/ToolTip.py @@ -0,0 +1,97 @@ +# general purpose 'tooltip' routines - currently unused in idlefork +# (although the 'calltips' extension is partly based on this code) +# may be useful for some purposes in (or almost in ;) the current project scope +# Ideas gleaned from PySol + +from Tkinter import * + +class ToolTipBase: + + def __init__(self, button): + self.button = button + self.tipwindow = None + self.id = None + self.x = self.y = 0 + self._id1 = self.button.bind("<Enter>", self.enter) + self._id2 = self.button.bind("<Leave>", self.leave) + self._id3 = self.button.bind("<ButtonPress>", self.leave) + + def enter(self, event=None): + self.schedule() + + def leave(self, event=None): + self.unschedule() + self.hidetip() + + def schedule(self): + self.unschedule() + self.id = self.button.after(1500, self.showtip) + + def unschedule(self): + id = self.id + self.id = None + if id: + self.button.after_cancel(id) + + def showtip(self): + if self.tipwindow: + return + # The tip window must be completely outside the button; + # otherwise when the mouse enters the tip window we get + # a leave event and it disappears, and then we get an enter + # event and it reappears, and so on forever :-( + x = self.button.winfo_rootx() + 20 + y = self.button.winfo_rooty() + self.button.winfo_height() + 1 + self.tipwindow = tw = Toplevel(self.button) + tw.wm_overrideredirect(1) + tw.wm_geometry("+%d+%d" % (x, y)) + self.showcontents() + + def showcontents(self, text="Your text here"): + # Override this in derived class + label = Label(self.tipwindow, text=text, justify=LEFT, + background="#ffffe0", relief=SOLID, borderwidth=1) + label.pack() + + def hidetip(self): + tw = self.tipwindow + self.tipwindow = None + if tw: + tw.destroy() + +class ToolTip(ToolTipBase): + def __init__(self, button, text): + ToolTipBase.__init__(self, button) + self.text = text + def showcontents(self): + ToolTipBase.showcontents(self, self.text) + +class ListboxToolTip(ToolTipBase): + def __init__(self, button, items): + ToolTipBase.__init__(self, button) + self.items = items + def showcontents(self): + listbox = Listbox(self.tipwindow, background="#ffffe0") + listbox.pack() + for item in self.items: + listbox.insert(END, item) + +def _tooltip(parent): + root = Tk() + root.title("Test tooltip") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) + label = Label(root, text="Place your mouse over buttons") + label.pack() + button1 = Button(root, text="Button 1") + button2 = Button(root, text="Button 2") + button1.pack() + button2.pack() + ToolTip(button1, "This is tooltip text for button1.") + ListboxToolTip(button2, ["This is","multiple line", + "tooltip text","for button2"]) + root.mainloop() + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(_tooltip) diff --git a/contrib/tools/python/src/Lib/idlelib/TreeWidget.py b/contrib/tools/python/src/Lib/idlelib/TreeWidget.py new file mode 100644 index 00000000000..9d9d4d9529f --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/TreeWidget.py @@ -0,0 +1,467 @@ +# 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 * +import imp + +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, "<Double-1>", 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, "<Double-1>", 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, "<Double-1>", self.flip) + x0, y0, x1, y1 = self.canvas.bbox(id) + textx = max(x1, 200) + 10 + text = self.item.GetText() or "<no text>" + 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("<Double-1>", 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("<Return>", self.edit_finish) + self.entry.bind("<Escape>", 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 os.error: + 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 os.error: + 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("<Key-Prior>", self.page_up) + self.canvas.bind("<Key-Next>", self.page_down) + self.canvas.bind("<Key-Up>", self.unit_up) + self.canvas.bind("<Key-Down>", self.unit_down) + #if isinstance(master, Toplevel) or isinstance(master, Tk): + self.canvas.bind("<Alt-Key-2>", 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/contrib/tools/python/src/Lib/idlelib/UndoDelegator.py b/contrib/tools/python/src/Lib/idlelib/UndoDelegator.py new file mode 100644 index 00000000000..cdeacea32f9 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/UndoDelegator.py @@ -0,0 +1,365 @@ +import string +from Tkinter import * + +from idlelib.Delegator import Delegator + +#$ event <<redo>> +#$ win <Control-y> +#$ unix <Alt-z> + +#$ event <<undo>> +#$ win <Control-z> +#$ unix <Control-z> + +#$ event <<dump-undo-state>> +#$ win <Control-backslash> +#$ unix <Control-backslash> + + +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("<<undo>>") + self.unbind("<<redo>>") + self.unbind("<<dump-undo-state>>") + Delegator.setdelegate(self, delegate) + if delegate is not None: + self.bind("<<undo>>", self.undo_event) + self.bind("<<redo>>", self.redo_event) + self.bind("<<dump-undo-state>>", self.dump_event) + + def dump_event(self, event): + from pprint import pprint + pprint(self.undolist[:self.pointer]) + print "pointer:", self.pointer, + print "saved:", self.saved, + print "can_merge:", self.can_merge, + print "get_saved():", self.get_saved() + pprint(self.undolist[self.pointer:]) + return "break" + + 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): + from idlelib.Percolator import Percolator + root = Tk() + root.title("Test UndoDelegator") + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 150)) + + text = Text(root) + text.config(height=10) + text.pack() + text.focus_set() + p = Percolator(text) + d = UndoDelegator() + p.insertfilter(d) + + undo = Button(root, text="Undo", command=lambda:d.undo_event(None)) + undo.pack(side='left') + redo = Button(root, text="Redo", command=lambda:d.redo_event(None)) + redo.pack(side='left') + dump = Button(root, text="Dump", command=lambda:d.dump_event(None)) + dump.pack(side='left') + + root.mainloop() + +if __name__ == "__main__": + from idlelib.idle_test.htest import run + run(_undo_delegator) diff --git a/contrib/tools/python/src/Lib/idlelib/WidgetRedirector.py b/contrib/tools/python/src/Lib/idlelib/WidgetRedirector.py new file mode 100644 index 00000000000..54431f73825 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/WidgetRedirector.py @@ -0,0 +1,175 @@ +from __future__ import print_function +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 <Key> could be made via tkinter, what we really want + to do is to hook the Tk 'insert' operation itself. For one thing, we want + a text.insert call in idle code to have the same effect as a key press. + + When a widget is instantiated, a Tcl command is created whose name is the + same as the pathname widget._w. This command is used to invoke the various + widget operations, e.g. insert (for a Text widget). We are going to hook + this command and provide a facility ('register') to intercept the widget + operation. 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 "WidgetRedirector(%s<%s>)" % (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 "OriginalCommand(%r, %r)" % (self.redir, self.operation) + + def __call__(self, *args): + return self.tk_call(self.orig_and_operation + args) + + +def _widget_redirector(parent): # htest # + from Tkinter import 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/contrib/tools/python/src/Lib/idlelib/WindowList.py b/contrib/tools/python/src/Lib/idlelib/WindowList.py new file mode 100644 index 00000000000..658502b20b0 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/WindowList.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.keys(): + window = self.dict[key] + try: + title = window.get_title() + except TclError: + continue + list.append((title, window)) + list.sort() + for title, 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: + print "warning: callback failed in WindowList", \ + sys.exc_type, ":", sys.exc_value + +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/contrib/tools/python/src/Lib/idlelib/ZoomHeight.py b/contrib/tools/python/src/Lib/idlelib/ZoomHeight.py new file mode 100644 index 00000000000..a5d679e4991 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/ZoomHeight.py @@ -0,0 +1,51 @@ +# Sample extension: zoom a window to maximum height + +import re +import sys + +from idlelib import macosxSupport + +class ZoomHeight: + + menudefs = [ + ('windows', [ + ('_Zoom Height', '<<zoom-height>>'), + ]) + ] + + def __init__(self, editwin): + self.editwin = editwin + + def zoom_height_event(self, event): + top = self.editwin.top + zoom_height(top) + +def zoom_height(top): + geom = top.wm_geometry() + m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom) + if not m: + top.bell() + return + width, height, x, y = map(int, m.groups()) + newheight = top.winfo_screenheight() + if sys.platform == 'win32': + newy = 0 + newheight = newheight - 72 + + elif macosxSupport.isAquaTk(): + # The '88' below is a magic number that avoids placing the bottom + # of the window below the panel on my machine. I don't know how + # to calculate the correct value for this with tkinter. + newy = 22 + newheight = newheight - newy - 88 + + else: + #newy = 24 + newy = 0 + #newheight = newheight - 96 + newheight = newheight - 88 + if height >= newheight: + newgeom = "" + else: + newgeom = "%dx%d+%d+%d" % (width, newheight, x, newy) + top.wm_geometry(newgeom) diff --git a/contrib/tools/python/src/Lib/idlelib/__init__.py b/contrib/tools/python/src/Lib/idlelib/__init__.py new file mode 100644 index 00000000000..32b7eacd1ea --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/__init__.py @@ -0,0 +1,8 @@ +"""The idlelib package implements the Idle application. + +Idle includes an interactive shell and editor. +Use the files named idle.* to start Idle. + +The other files are private implementations. Their details are subject +to change. See PEP 434 for more. Import them at your own risk. +""" diff --git a/contrib/tools/python/src/Lib/idlelib/aboutDialog.py b/contrib/tools/python/src/Lib/idlelib/aboutDialog.py new file mode 100644 index 00000000000..87d6c3cb9c0 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/aboutDialog.py @@ -0,0 +1,151 @@ +"""About Dialog for IDLE + +""" +import os +from sys import version +from Tkinter import * +from idlelib import textView + +class AboutDialog(Toplevel): + """Modal about dialog for idle + + """ + def __init__(self, parent, title, _htest=False): + """ + _htest - bool, change box location when running htest + """ + Toplevel.__init__(self, parent) + self.configure(borderwidth=5) + # place dialog below parent if running htest + self.geometry("+%d+%d" % ( + parent.winfo_rootx()+30, + parent.winfo_rooty()+(30 if not _htest else 100))) + self.bg = "#707070" + self.fg = "#ffffff" + self.CreateWidgets() + self.resizable(height=FALSE, width=FALSE) + self.title(title) + self.transient(parent) + self.grab_set() + self.protocol("WM_DELETE_WINDOW", self.Ok) + self.parent = parent + self.buttonOk.focus_set() + self.bind('<Return>',self.Ok) #dismiss dialog + self.bind('<Escape>',self.Ok) #dismiss dialog + self.wait_window() + + def CreateWidgets(self): + release = version[:version.index(' ')] + frameMain = Frame(self, borderwidth=2, relief=SUNKEN) + frameButtons = Frame(self) + frameButtons.pack(side=BOTTOM, fill=X) + frameMain.pack(side=TOP, expand=TRUE, fill=BOTH) + self.buttonOk = Button(frameButtons, text='Close', + command=self.Ok) + self.buttonOk.pack(padx=5, pady=5) + #self.picture = Image('photo', data=self.pictureData) + frameBg = Frame(frameMain, bg=self.bg) + frameBg.pack(expand=TRUE, fill=BOTH) + labelTitle = Label(frameBg, text='IDLE', fg=self.fg, bg=self.bg, + font=('courier', 24, 'bold')) + labelTitle.grid(row=0, column=0, sticky=W, padx=10, pady=10) + #labelPicture = Label(frameBg, text='[picture]') + #image=self.picture, bg=self.bg) + #labelPicture.grid(row=1, column=1, sticky=W, rowspan=2, + # padx=0, pady=3) + byline = "Python's Integrated DeveLopment Environment" + 5*'\n' + labelDesc = Label(frameBg, text=byline, justify=LEFT, + fg=self.fg, bg=self.bg) + labelDesc.grid(row=2, column=0, sticky=W, columnspan=3, padx=10, pady=5) + labelEmail = Label(frameBg, text='email: [email protected]', + justify=LEFT, fg=self.fg, bg=self.bg) + labelEmail.grid(row=6, column=0, columnspan=2, + sticky=W, padx=10, pady=0) + labelWWW = Label(frameBg, text='https://docs.python.org/' + + version[:3] + '/library/idle.html', + justify=LEFT, fg=self.fg, bg=self.bg) + labelWWW.grid(row=7, column=0, columnspan=2, sticky=W, padx=10, pady=0) + Frame(frameBg, borderwidth=1, relief=SUNKEN, + height=2, bg=self.bg).grid(row=8, column=0, sticky=EW, + columnspan=3, padx=5, pady=5) + labelPythonVer = Label(frameBg, text='Python version: ' + + release, fg=self.fg, bg=self.bg) + labelPythonVer.grid(row=9, column=0, sticky=W, padx=10, pady=0) + tkVer = self.tk.call('info', 'patchlevel') + labelTkVer = Label(frameBg, text='Tk version: '+ + tkVer, fg=self.fg, bg=self.bg) + labelTkVer.grid(row=9, column=1, sticky=W, padx=2, pady=0) + py_button_f = Frame(frameBg, bg=self.bg) + py_button_f.grid(row=10, column=0, columnspan=2, sticky=NSEW) + buttonLicense = Button(py_button_f, text='License', width=8, + highlightbackground=self.bg, + command=self.ShowLicense) + buttonLicense.pack(side=LEFT, padx=10, pady=10) + buttonCopyright = Button(py_button_f, text='Copyright', width=8, + highlightbackground=self.bg, + command=self.ShowCopyright) + buttonCopyright.pack(side=LEFT, padx=10, pady=10) + buttonCredits = Button(py_button_f, text='Credits', width=8, + highlightbackground=self.bg, + command=self.ShowPythonCredits) + buttonCredits.pack(side=LEFT, padx=10, pady=10) + Frame(frameBg, borderwidth=1, relief=SUNKEN, + height=2, bg=self.bg).grid(row=11, column=0, sticky=EW, + columnspan=3, padx=5, pady=5) + idle_v = Label(frameBg, text='IDLE version: ' + release, + fg=self.fg, bg=self.bg) + idle_v.grid(row=12, column=0, sticky=W, padx=10, pady=0) + idle_button_f = Frame(frameBg, bg=self.bg) + idle_button_f.grid(row=13, column=0, columnspan=3, sticky=NSEW) + idle_about_b = Button(idle_button_f, text='README', width=8, + highlightbackground=self.bg, + command=self.ShowIDLEAbout) + idle_about_b.pack(side=LEFT, padx=10, pady=10) + idle_news_b = Button(idle_button_f, text='NEWS', width=8, + highlightbackground=self.bg, + command=self.ShowIDLENEWS) + idle_news_b.pack(side=LEFT, padx=10, pady=10) + idle_credits_b = Button(idle_button_f, text='Credits', width=8, + highlightbackground=self.bg, + command=self.ShowIDLECredits) + idle_credits_b.pack(side=LEFT, padx=10, pady=10) + + # License, et all, are of type _sitebuiltins._Printer + def ShowLicense(self): + self.display_printer_text('About - License', license) + + def ShowCopyright(self): + self.display_printer_text('About - Copyright', copyright) + + def ShowPythonCredits(self): + self.display_printer_text('About - Python Credits', credits) + + # Encode CREDITS.txt to utf-8 for proper version of Loewis. + # Specify others as ascii until need utf-8, so catch errors. + def ShowIDLECredits(self): + self.display_file_text('About - Credits', 'CREDITS.txt', 'utf-8') + + def ShowIDLEAbout(self): + self.display_file_text('About - Readme', 'README.txt', 'ascii') + + def ShowIDLENEWS(self): + self.display_file_text('About - NEWS', 'NEWS.txt', 'utf-8') + + def display_printer_text(self, title, printer): + printer._Printer__setup() + text = '\n'.join(printer._Printer__lines) + textView.view_text(self, title, text) + + def display_file_text(self, title, filename, encoding=None): + fn = os.path.join(os.path.abspath(os.path.dirname(__file__)), filename) + textView.view_file(self, title, fn, encoding) + + def Ok(self, event=None): + self.grab_release() + self.destroy() + +if __name__ == '__main__': + import unittest + unittest.main('idlelib.idle_test.test_helpabout', verbosity=2, exit=False) + from idlelib.idle_test.htest import run + run(AboutDialog) diff --git a/contrib/tools/python/src/Lib/idlelib/configDialog.py b/contrib/tools/python/src/Lib/idlelib/configDialog.py new file mode 100644 index 00000000000..3c29af13763 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/configDialog.py @@ -0,0 +1,1457 @@ +"""IDLE Configuration Dialog: support user customization of IDLE by GUI + +Customize font faces, sizes, and colorization attributes. Set indentation +defaults. Customize keybindings. Colorization and keybindings can be +saved as user defined sets. Select startup options including shell/editor +and default window size. Define additional help sources. + +Note that tab width in IDLE is currently fixed at eight due to Tk issues. +Refer to comments in EditorWindow autoindent code for details. + +""" +from Tkinter import * +import tkMessageBox, tkColorChooser, tkFont + +from idlelib.configHandler import idleConf +from idlelib.dynOptionMenuWidget import DynOptionMenu +from idlelib.keybindingDialog import GetKeysDialog +from idlelib.configSectionNameDialog import GetCfgSectionNameDialog +from idlelib.configHelpSourceEdit import GetHelpSourceDialog +from idlelib.tabbedpages import TabbedPageSet +from idlelib.textView import view_text +from idlelib import macosxSupport + +class ConfigDialog(Toplevel): + + def __init__(self, parent, title='', _htest=False, _utest=False): + """ + _htest - bool, change box location when running htest + _utest - bool, don't wait_window when running unittest + """ + Toplevel.__init__(self, parent) + self.parent = parent + if _htest: + parent.instance_dict = {} + self.wm_withdraw() + + self.configure(borderwidth=5) + self.title(title or 'IDLE Preferences') + self.geometry( + "+%d+%d" % (parent.winfo_rootx() + 20, + parent.winfo_rooty() + (30 if not _htest else 150))) + #Theme Elements. Each theme element key is its display name. + #The first value of the tuple is the sample area tag name. + #The second value is the display name list sort index. + self.themeElements={ + 'Normal Text': ('normal', '00'), + 'Python Keywords': ('keyword', '01'), + 'Python Definitions': ('definition', '02'), + 'Python Builtins': ('builtin', '03'), + 'Python Comments': ('comment', '04'), + 'Python Strings': ('string', '05'), + 'Selected Text': ('hilite', '06'), + 'Found Text': ('hit', '07'), + 'Cursor': ('cursor', '08'), + 'Editor Breakpoint': ('break', '09'), + 'Shell Normal Text': ('console', '10'), + 'Shell Error Text': ('error', '11'), + 'Shell Stdout Text': ('stdout', '12'), + 'Shell Stderr Text': ('stderr', '13'), + } + self.ResetChangedItems() #load initial values in changed items dict + self.CreateWidgets() + self.resizable(height=FALSE, width=FALSE) + self.transient(parent) + self.grab_set() + self.protocol("WM_DELETE_WINDOW", self.Cancel) + self.tabPages.focus_set() + #key bindings for this dialog + #self.bind('<Escape>', self.Cancel) #dismiss dialog, no save + #self.bind('<Alt-a>', self.Apply) #apply changes, save + #self.bind('<F1>', self.Help) #context help + self.LoadConfigs() + self.AttachVarCallbacks() #avoid callbacks during LoadConfigs + + if not _utest: + self.wm_deiconify() + self.wait_window() + + def CreateWidgets(self): + self.tabPages = TabbedPageSet(self, + page_names=['Fonts/Tabs', 'Highlighting', 'Keys', 'General', + 'Extensions']) + self.tabPages.pack(side=TOP, expand=TRUE, fill=BOTH) + self.CreatePageFontTab() + self.CreatePageHighlight() + self.CreatePageKeys() + self.CreatePageGeneral() + self.CreatePageExtensions() + self.create_action_buttons().pack(side=BOTTOM) + + def create_action_buttons(self): + if macosxSupport.isAquaTk(): + # Changing the default padding on OSX results in unreadable + # text in the buttons + paddingArgs = {} + else: + paddingArgs = {'padx':6, 'pady':3} + outer = Frame(self, pady=2) + buttons = Frame(outer, pady=2) + for txt, cmd in ( + ('Ok', self.Ok), + ('Apply', self.Apply), + ('Cancel', self.Cancel), + ('Help', self.Help)): + Button(buttons, text=txt, command=cmd, takefocus=FALSE, + **paddingArgs).pack(side=LEFT, padx=5) + # add space above buttons + Frame(outer, height=2, borderwidth=0).pack(side=TOP) + buttons.pack(side=BOTTOM) + return outer + + def CreatePageFontTab(self): + parent = self.parent + self.fontSize = StringVar(parent) + self.fontBold = BooleanVar(parent) + self.fontName = StringVar(parent) + self.spaceNum = IntVar(parent) + self.editFont = tkFont.Font(parent, ('courier', 10, 'normal')) + + ##widget creation + #body frame + frame = self.tabPages.pages['Fonts/Tabs'].frame + #body section frames + frameFont = LabelFrame( + frame, borderwidth=2, relief=GROOVE, text=' Base Editor Font ') + frameIndent = LabelFrame( + frame, borderwidth=2, relief=GROOVE, text=' Indentation Width ') + #frameFont + frameFontName = Frame(frameFont) + frameFontParam = Frame(frameFont) + labelFontNameTitle = Label( + frameFontName, justify=LEFT, text='Font Face :') + self.listFontName = Listbox( + frameFontName, height=5, takefocus=FALSE, exportselection=FALSE) + self.listFontName.bind( + '<ButtonRelease-1>', self.OnListFontButtonRelease) + scrollFont = Scrollbar(frameFontName) + scrollFont.config(command=self.listFontName.yview) + self.listFontName.config(yscrollcommand=scrollFont.set) + labelFontSizeTitle = Label(frameFontParam, text='Size :') + self.optMenuFontSize = DynOptionMenu( + frameFontParam, self.fontSize, None, command=self.SetFontSample) + checkFontBold = Checkbutton( + frameFontParam, variable=self.fontBold, onvalue=1, + offvalue=0, text='Bold', command=self.SetFontSample) + frameFontSample = Frame(frameFont, relief=SOLID, borderwidth=1) + self.labelFontSample = Label( + frameFontSample, justify=LEFT, font=self.editFont, + text='AaBbCcDdEe\nFfGgHhIiJjK\n1234567890\n#:+=(){}[]') + #frameIndent + frameIndentSize = Frame(frameIndent) + labelSpaceNumTitle = Label( + frameIndentSize, justify=LEFT, + text='Python Standard: 4 Spaces!') + self.scaleSpaceNum = Scale( + frameIndentSize, variable=self.spaceNum, + orient='horizontal', tickinterval=2, from_=2, to=16) + + #widget packing + #body + frameFont.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) + frameIndent.pack(side=LEFT, padx=5, pady=5, fill=Y) + #frameFont + frameFontName.pack(side=TOP, padx=5, pady=5, fill=X) + frameFontParam.pack(side=TOP, padx=5, pady=5, fill=X) + labelFontNameTitle.pack(side=TOP, anchor=W) + self.listFontName.pack(side=LEFT, expand=TRUE, fill=X) + scrollFont.pack(side=LEFT, fill=Y) + labelFontSizeTitle.pack(side=LEFT, anchor=W) + self.optMenuFontSize.pack(side=LEFT, anchor=W) + checkFontBold.pack(side=LEFT, anchor=W, padx=20) + frameFontSample.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) + self.labelFontSample.pack(expand=TRUE, fill=BOTH) + #frameIndent + frameIndentSize.pack(side=TOP, fill=X) + labelSpaceNumTitle.pack(side=TOP, anchor=W, padx=5) + self.scaleSpaceNum.pack(side=TOP, padx=5, fill=X) + return frame + + def CreatePageHighlight(self): + parent = self.parent + self.builtinTheme = StringVar(parent) + self.customTheme = StringVar(parent) + self.fgHilite = BooleanVar(parent) + self.colour = StringVar(parent) + self.fontName = StringVar(parent) + self.themeIsBuiltin = BooleanVar(parent) + self.highlightTarget = StringVar(parent) + + ##widget creation + #body frame + frame = self.tabPages.pages['Highlighting'].frame + #body section frames + frameCustom = LabelFrame(frame, borderwidth=2, relief=GROOVE, + text=' Custom Highlighting ') + frameTheme = LabelFrame(frame, borderwidth=2, relief=GROOVE, + text=' Highlighting Theme ') + #frameCustom + self.textHighlightSample=Text( + frameCustom, relief=SOLID, borderwidth=1, + font=('courier', 12, ''), cursor='hand2', width=21, height=11, + takefocus=FALSE, highlightthickness=0, wrap=NONE) + text=self.textHighlightSample + text.bind('<Double-Button-1>', lambda e: 'break') + text.bind('<B1-Motion>', lambda e: 'break') + textAndTags=( + ('#you can click here', 'comment'), ('\n', 'normal'), + ('#to choose items', 'comment'), ('\n', 'normal'), + ('def', 'keyword'), (' ', 'normal'), + ('func', 'definition'), ('(param):\n ', 'normal'), + ('"""string"""', 'string'), ('\n var0 = ', 'normal'), + ("'string'", 'string'), ('\n var1 = ', 'normal'), + ("'selected'", 'hilite'), ('\n var2 = ', 'normal'), + ("'found'", 'hit'), ('\n var3 = ', 'normal'), + ('list', 'builtin'), ('(', 'normal'), + ('None', 'builtin'), (')\n', 'normal'), + (' breakpoint("line")', 'break'), ('\n\n', 'normal'), + (' error ', 'error'), (' ', 'normal'), + ('cursor |', 'cursor'), ('\n ', 'normal'), + ('shell', 'console'), (' ', 'normal'), + ('stdout', 'stdout'), (' ', 'normal'), + ('stderr', 'stderr'), ('\n', 'normal')) + for txTa in textAndTags: + text.insert(END, txTa[0], txTa[1]) + for element in self.themeElements: + def tem(event, elem=element): + event.widget.winfo_toplevel().highlightTarget.set(elem) + text.tag_bind( + self.themeElements[element][0], '<ButtonPress-1>', tem) + text.config(state=DISABLED) + self.frameColourSet = Frame(frameCustom, relief=SOLID, borderwidth=1) + frameFgBg = Frame(frameCustom) + buttonSetColour = Button( + self.frameColourSet, text='Choose Colour for :', + command=self.GetColour, highlightthickness=0) + self.optMenuHighlightTarget = DynOptionMenu( + self.frameColourSet, self.highlightTarget, None, + highlightthickness=0) #, command=self.SetHighlightTargetBinding + self.radioFg = Radiobutton( + frameFgBg, variable=self.fgHilite, value=1, + text='Foreground', command=self.SetColourSampleBinding) + self.radioBg=Radiobutton( + frameFgBg, variable=self.fgHilite, value=0, + text='Background', command=self.SetColourSampleBinding) + self.fgHilite.set(1) + buttonSaveCustomTheme = Button( + frameCustom, text='Save as New Custom Theme', + command=self.SaveAsNewTheme) + #frameTheme + labelTypeTitle = Label(frameTheme, text='Select : ') + self.radioThemeBuiltin = Radiobutton( + frameTheme, variable=self.themeIsBuiltin, value=1, + command=self.SetThemeType, text='a Built-in Theme') + self.radioThemeCustom = Radiobutton( + frameTheme, variable=self.themeIsBuiltin, value=0, + command=self.SetThemeType, text='a Custom Theme') + self.optMenuThemeBuiltin = DynOptionMenu( + frameTheme, self.builtinTheme, None, command=None) + self.optMenuThemeCustom=DynOptionMenu( + frameTheme, self.customTheme, None, command=None) + self.buttonDeleteCustomTheme=Button( + frameTheme, text='Delete Custom Theme', + command=self.DeleteCustomTheme) + self.new_custom_theme = Label(frameTheme, bd=2) + + ##widget packing + #body + frameCustom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) + frameTheme.pack(side=LEFT, padx=5, pady=5, fill=Y) + #frameCustom + self.frameColourSet.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X) + frameFgBg.pack(side=TOP, padx=5, pady=0) + self.textHighlightSample.pack( + side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) + buttonSetColour.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4) + self.optMenuHighlightTarget.pack( + side=TOP, expand=TRUE, fill=X, padx=8, pady=3) + self.radioFg.pack(side=LEFT, anchor=E) + self.radioBg.pack(side=RIGHT, anchor=W) + buttonSaveCustomTheme.pack(side=BOTTOM, fill=X, padx=5, pady=5) + #frameTheme + labelTypeTitle.pack(side=TOP, anchor=W, padx=5, pady=5) + self.radioThemeBuiltin.pack(side=TOP, anchor=W, padx=5) + self.radioThemeCustom.pack(side=TOP, anchor=W, padx=5, pady=2) + self.optMenuThemeBuiltin.pack(side=TOP, fill=X, padx=5, pady=5) + self.optMenuThemeCustom.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5) + self.buttonDeleteCustomTheme.pack(side=TOP, fill=X, padx=5, pady=5) + self.new_custom_theme.pack(side=TOP, fill=X, pady=5) + return frame + + def CreatePageKeys(self): + parent = self.parent + self.bindingTarget = StringVar(parent) + self.builtinKeys = StringVar(parent) + self.customKeys = StringVar(parent) + self.keysAreBuiltin = BooleanVar(parent) + self.keyBinding = StringVar(parent) + + ##widget creation + #body frame + frame = self.tabPages.pages['Keys'].frame + #body section frames + frameCustom = LabelFrame( + frame, borderwidth=2, relief=GROOVE, + text=' Custom Key Bindings ') + frameKeySets = LabelFrame( + frame, borderwidth=2, relief=GROOVE, text=' Key Set ') + #frameCustom + frameTarget = Frame(frameCustom) + labelTargetTitle = Label(frameTarget, text='Action - Key(s)') + scrollTargetY = Scrollbar(frameTarget) + scrollTargetX = Scrollbar(frameTarget, orient=HORIZONTAL) + self.listBindings = Listbox( + frameTarget, takefocus=FALSE, exportselection=FALSE) + self.listBindings.bind('<ButtonRelease-1>', self.KeyBindingSelected) + scrollTargetY.config(command=self.listBindings.yview) + scrollTargetX.config(command=self.listBindings.xview) + self.listBindings.config(yscrollcommand=scrollTargetY.set) + self.listBindings.config(xscrollcommand=scrollTargetX.set) + self.buttonNewKeys = Button( + frameCustom, text='Get New Keys for Selection', + command=self.GetNewKeys, state=DISABLED) + #frameKeySets + frames = [Frame(frameKeySets, padx=2, pady=2, borderwidth=0) + for i in range(2)] + self.radioKeysBuiltin = Radiobutton( + frames[0], variable=self.keysAreBuiltin, value=1, + command=self.SetKeysType, text='Use a Built-in Key Set') + self.radioKeysCustom = Radiobutton( + frames[0], variable=self.keysAreBuiltin, value=0, + command=self.SetKeysType, text='Use a Custom Key Set') + self.optMenuKeysBuiltin = DynOptionMenu( + frames[0], self.builtinKeys, None, command=None) + self.optMenuKeysCustom = DynOptionMenu( + frames[0], self.customKeys, None, command=None) + self.buttonDeleteCustomKeys = Button( + frames[1], text='Delete Custom Key Set', + command=self.DeleteCustomKeys) + buttonSaveCustomKeys = Button( + frames[1], text='Save as New Custom Key Set', + command=self.SaveAsNewKeySet) + + ##widget packing + #body + frameCustom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH) + frameKeySets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH) + #frameCustom + self.buttonNewKeys.pack(side=BOTTOM, fill=X, padx=5, pady=5) + frameTarget.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) + #frame target + frameTarget.columnconfigure(0, weight=1) + frameTarget.rowconfigure(1, weight=1) + labelTargetTitle.grid(row=0, column=0, columnspan=2, sticky=W) + self.listBindings.grid(row=1, column=0, sticky=NSEW) + scrollTargetY.grid(row=1, column=1, sticky=NS) + scrollTargetX.grid(row=2, column=0, sticky=EW) + #frameKeySets + self.radioKeysBuiltin.grid(row=0, column=0, sticky=W+NS) + self.radioKeysCustom.grid(row=1, column=0, sticky=W+NS) + self.optMenuKeysBuiltin.grid(row=0, column=1, sticky=NSEW) + self.optMenuKeysCustom.grid(row=1, column=1, sticky=NSEW) + self.buttonDeleteCustomKeys.pack(side=LEFT, fill=X, expand=True, padx=2) + buttonSaveCustomKeys.pack(side=LEFT, fill=X, expand=True, padx=2) + frames[0].pack(side=TOP, fill=BOTH, expand=True) + frames[1].pack(side=TOP, fill=X, expand=True, pady=2) + return frame + + def CreatePageGeneral(self): + parent = self.parent + self.winWidth = StringVar(parent) + self.winHeight = StringVar(parent) + self.startupEdit = IntVar(parent) + self.autoSave = IntVar(parent) + self.encoding = StringVar(parent) + self.userHelpBrowser = BooleanVar(parent) + self.helpBrowser = StringVar(parent) + + #widget creation + #body + frame = self.tabPages.pages['General'].frame + #body section frames + frameRun = LabelFrame(frame, borderwidth=2, relief=GROOVE, + text=' Startup Preferences ') + frameSave = LabelFrame(frame, borderwidth=2, relief=GROOVE, + text=' Autosave Preferences ') + frameWinSize = Frame(frame, borderwidth=2, relief=GROOVE) + frameEncoding = Frame(frame, borderwidth=2, relief=GROOVE) + frameHelp = LabelFrame(frame, borderwidth=2, relief=GROOVE, + text=' Additional Help Sources ') + #frameRun + labelRunChoiceTitle = Label(frameRun, text='At Startup') + radioStartupEdit = Radiobutton( + frameRun, variable=self.startupEdit, value=1, + command=self.SetKeysType, text="Open Edit Window") + radioStartupShell = Radiobutton( + frameRun, variable=self.startupEdit, value=0, + command=self.SetKeysType, text='Open Shell Window') + #frameSave + labelRunSaveTitle = Label(frameSave, text='At Start of Run (F5) ') + radioSaveAsk = Radiobutton( + frameSave, variable=self.autoSave, value=0, + command=self.SetKeysType, text="Prompt to Save") + radioSaveAuto = Radiobutton( + frameSave, variable=self.autoSave, value=1, + command=self.SetKeysType, text='No Prompt') + #frameWinSize + labelWinSizeTitle = Label( + frameWinSize, text='Initial Window Size (in characters)') + labelWinWidthTitle = Label(frameWinSize, text='Width') + entryWinWidth = Entry( + frameWinSize, textvariable=self.winWidth, width=3) + labelWinHeightTitle = Label(frameWinSize, text='Height') + entryWinHeight = Entry( + frameWinSize, textvariable=self.winHeight, width=3) + #frameEncoding + labelEncodingTitle = Label( + frameEncoding, text="Default Source Encoding") + radioEncLocale = Radiobutton( + frameEncoding, variable=self.encoding, + value="locale", text="Locale-defined") + radioEncUTF8 = Radiobutton( + frameEncoding, variable=self.encoding, + value="utf-8", text="UTF-8") + radioEncNone = Radiobutton( + frameEncoding, variable=self.encoding, + value="none", text="None") + #frameHelp + frameHelpList = Frame(frameHelp) + frameHelpListButtons = Frame(frameHelpList) + scrollHelpList = Scrollbar(frameHelpList) + self.listHelp = Listbox( + frameHelpList, height=5, takefocus=FALSE, + exportselection=FALSE) + scrollHelpList.config(command=self.listHelp.yview) + self.listHelp.config(yscrollcommand=scrollHelpList.set) + self.listHelp.bind('<ButtonRelease-1>', self.HelpSourceSelected) + self.buttonHelpListEdit = Button( + frameHelpListButtons, text='Edit', state=DISABLED, + width=8, command=self.HelpListItemEdit) + self.buttonHelpListAdd = Button( + frameHelpListButtons, text='Add', + width=8, command=self.HelpListItemAdd) + self.buttonHelpListRemove = Button( + frameHelpListButtons, text='Remove', state=DISABLED, + width=8, command=self.HelpListItemRemove) + + #widget packing + #body + frameRun.pack(side=TOP, padx=5, pady=5, fill=X) + frameSave.pack(side=TOP, padx=5, pady=5, fill=X) + frameWinSize.pack(side=TOP, padx=5, pady=5, fill=X) + frameEncoding.pack(side=TOP, padx=5, pady=5, fill=X) + frameHelp.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) + #frameRun + labelRunChoiceTitle.pack(side=LEFT, anchor=W, padx=5, pady=5) + radioStartupShell.pack(side=RIGHT, anchor=W, padx=5, pady=5) + radioStartupEdit.pack(side=RIGHT, anchor=W, padx=5, pady=5) + #frameSave + labelRunSaveTitle.pack(side=LEFT, anchor=W, padx=5, pady=5) + radioSaveAuto.pack(side=RIGHT, anchor=W, padx=5, pady=5) + radioSaveAsk.pack(side=RIGHT, anchor=W, padx=5, pady=5) + #frameWinSize + labelWinSizeTitle.pack(side=LEFT, anchor=W, padx=5, pady=5) + entryWinHeight.pack(side=RIGHT, anchor=E, padx=10, pady=5) + labelWinHeightTitle.pack(side=RIGHT, anchor=E, pady=5) + entryWinWidth.pack(side=RIGHT, anchor=E, padx=10, pady=5) + labelWinWidthTitle.pack(side=RIGHT, anchor=E, pady=5) + #frameEncoding + labelEncodingTitle.pack(side=LEFT, anchor=W, padx=5, pady=5) + radioEncNone.pack(side=RIGHT, anchor=E, pady=5) + radioEncUTF8.pack(side=RIGHT, anchor=E, pady=5) + radioEncLocale.pack(side=RIGHT, anchor=E, pady=5) + #frameHelp + frameHelpListButtons.pack(side=RIGHT, padx=5, pady=5, fill=Y) + frameHelpList.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) + scrollHelpList.pack(side=RIGHT, anchor=W, fill=Y) + self.listHelp.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH) + self.buttonHelpListEdit.pack(side=TOP, anchor=W, pady=5) + self.buttonHelpListAdd.pack(side=TOP, anchor=W) + self.buttonHelpListRemove.pack(side=TOP, anchor=W, pady=5) + return frame + + def AttachVarCallbacks(self): + self.fontSize.trace_variable('w', self.VarChanged_font) + self.fontName.trace_variable('w', self.VarChanged_font) + self.fontBold.trace_variable('w', self.VarChanged_font) + self.spaceNum.trace_variable('w', self.VarChanged_spaceNum) + self.colour.trace_variable('w', self.VarChanged_colour) + self.builtinTheme.trace_variable('w', self.VarChanged_builtinTheme) + self.customTheme.trace_variable('w', self.VarChanged_customTheme) + self.themeIsBuiltin.trace_variable('w', self.VarChanged_themeIsBuiltin) + self.highlightTarget.trace_variable('w', self.VarChanged_highlightTarget) + self.keyBinding.trace_variable('w', self.VarChanged_keyBinding) + self.builtinKeys.trace_variable('w', self.VarChanged_builtinKeys) + self.customKeys.trace_variable('w', self.VarChanged_customKeys) + self.keysAreBuiltin.trace_variable('w', self.VarChanged_keysAreBuiltin) + self.winWidth.trace_variable('w', self.VarChanged_winWidth) + self.winHeight.trace_variable('w', self.VarChanged_winHeight) + self.startupEdit.trace_variable('w', self.VarChanged_startupEdit) + self.autoSave.trace_variable('w', self.VarChanged_autoSave) + self.encoding.trace_variable('w', self.VarChanged_encoding) + + def remove_var_callbacks(self): + for var in ( + self.fontSize, self.fontName, self.fontBold, + self.spaceNum, self.colour, self.builtinTheme, + self.customTheme, self.themeIsBuiltin, self.highlightTarget, + self.keyBinding, self.builtinKeys, self.customKeys, + self.keysAreBuiltin, self.winWidth, self.winHeight, + self.startupEdit, self.autoSave, self.encoding,): + var.trace_vdelete('w', var.trace_vinfo()[0][1]) + + def VarChanged_font(self, *params): + '''When one font attribute changes, save them all, as they are + not independent from each other. In particular, when we are + overriding the default font, we need to write out everything. + ''' + value = self.fontName.get() + self.AddChangedItem('main', 'EditorWindow', 'font', value) + value = self.fontSize.get() + self.AddChangedItem('main', 'EditorWindow', 'font-size', value) + value = self.fontBold.get() + self.AddChangedItem('main', 'EditorWindow', 'font-bold', value) + + def VarChanged_spaceNum(self, *params): + value = self.spaceNum.get() + self.AddChangedItem('main', 'Indent', 'num-spaces', value) + + def VarChanged_colour(self, *params): + self.OnNewColourSet() + + def VarChanged_builtinTheme(self, *params): + value = self.builtinTheme.get() + if value == 'IDLE Dark': + if idleConf.GetOption('main', 'Theme', 'name') != 'IDLE New': + self.AddChangedItem('main', 'Theme', 'name', 'IDLE Classic') + self.AddChangedItem('main', 'Theme', 'name2', value) + self.new_custom_theme.config(text='New theme, see Help', + fg='#500000') + else: + self.AddChangedItem('main', 'Theme', 'name', value) + self.AddChangedItem('main', 'Theme', 'name2', '') + self.new_custom_theme.config(text='', fg='black') + self.PaintThemeSample() + + def VarChanged_customTheme(self, *params): + value = self.customTheme.get() + if value != '- no custom themes -': + self.AddChangedItem('main', 'Theme', 'name', value) + self.PaintThemeSample() + + def VarChanged_themeIsBuiltin(self, *params): + value = self.themeIsBuiltin.get() + self.AddChangedItem('main', 'Theme', 'default', value) + if value: + self.VarChanged_builtinTheme() + else: + self.VarChanged_customTheme() + + def VarChanged_highlightTarget(self, *params): + self.SetHighlightTarget() + + def VarChanged_keyBinding(self, *params): + value = self.keyBinding.get() + keySet = self.customKeys.get() + event = self.listBindings.get(ANCHOR).split()[0] + if idleConf.IsCoreBinding(event): + #this is a core keybinding + self.AddChangedItem('keys', keySet, event, value) + else: #this is an extension key binding + extName = idleConf.GetExtnNameForEvent(event) + extKeybindSection = extName + '_cfgBindings' + self.AddChangedItem('extensions', extKeybindSection, event, value) + + def VarChanged_builtinKeys(self, *params): + value = self.builtinKeys.get() + self.AddChangedItem('main', 'Keys', 'name', value) + self.LoadKeysList(value) + + def VarChanged_customKeys(self, *params): + value = self.customKeys.get() + if value != '- no custom keys -': + self.AddChangedItem('main', 'Keys', 'name', value) + self.LoadKeysList(value) + + def VarChanged_keysAreBuiltin(self, *params): + value = self.keysAreBuiltin.get() + self.AddChangedItem('main', 'Keys', 'default', value) + if value: + self.VarChanged_builtinKeys() + else: + self.VarChanged_customKeys() + + def VarChanged_winWidth(self, *params): + value = self.winWidth.get() + self.AddChangedItem('main', 'EditorWindow', 'width', value) + + def VarChanged_winHeight(self, *params): + value = self.winHeight.get() + self.AddChangedItem('main', 'EditorWindow', 'height', value) + + def VarChanged_startupEdit(self, *params): + value = self.startupEdit.get() + self.AddChangedItem('main', 'General', 'editor-on-startup', value) + + def VarChanged_autoSave(self, *params): + value = self.autoSave.get() + self.AddChangedItem('main', 'General', 'autosave', value) + + def VarChanged_encoding(self, *params): + value = self.encoding.get() + self.AddChangedItem('main', 'EditorWindow', 'encoding', value) + + def ResetChangedItems(self): + #When any config item is changed in this dialog, an entry + #should be made in the relevant section (config type) of this + #dictionary. The key should be the config file section name and the + #value a dictionary, whose key:value pairs are item=value pairs for + #that config file section. + self.changedItems = {'main':{}, 'highlight':{}, 'keys':{}, + 'extensions':{}} + + def AddChangedItem(self, typ, section, item, value): + value = str(value) #make sure we use a string + if section not in self.changedItems[typ]: + self.changedItems[typ][section] = {} + self.changedItems[typ][section][item] = value + + def GetDefaultItems(self): + dItems={'main':{}, 'highlight':{}, 'keys':{}, 'extensions':{}} + for configType in dItems: + sections = idleConf.GetSectionList('default', configType) + for section in sections: + dItems[configType][section] = {} + options = idleConf.defaultCfg[configType].GetOptionList(section) + for option in options: + dItems[configType][section][option] = ( + idleConf.defaultCfg[configType].Get(section, option)) + return dItems + + def SetThemeType(self): + if self.themeIsBuiltin.get(): + self.optMenuThemeBuiltin.config(state=NORMAL) + self.optMenuThemeCustom.config(state=DISABLED) + self.buttonDeleteCustomTheme.config(state=DISABLED) + else: + self.optMenuThemeBuiltin.config(state=DISABLED) + self.radioThemeCustom.config(state=NORMAL) + self.optMenuThemeCustom.config(state=NORMAL) + self.buttonDeleteCustomTheme.config(state=NORMAL) + + def SetKeysType(self): + if self.keysAreBuiltin.get(): + self.optMenuKeysBuiltin.config(state=NORMAL) + self.optMenuKeysCustom.config(state=DISABLED) + self.buttonDeleteCustomKeys.config(state=DISABLED) + else: + self.optMenuKeysBuiltin.config(state=DISABLED) + self.radioKeysCustom.config(state=NORMAL) + self.optMenuKeysCustom.config(state=NORMAL) + self.buttonDeleteCustomKeys.config(state=NORMAL) + + def GetNewKeys(self): + listIndex = self.listBindings.index(ANCHOR) + binding = self.listBindings.get(listIndex) + bindName = binding.split()[0] #first part, up to first space + if self.keysAreBuiltin.get(): + currentKeySetName = self.builtinKeys.get() + else: + currentKeySetName = self.customKeys.get() + currentBindings = idleConf.GetCurrentKeySet() + if currentKeySetName in self.changedItems['keys']: #unsaved changes + keySetChanges = self.changedItems['keys'][currentKeySetName] + for event in keySetChanges: + currentBindings[event] = keySetChanges[event].split() + currentKeySequences = currentBindings.values() + newKeys = GetKeysDialog(self, 'Get New Keys', bindName, + currentKeySequences).result + if newKeys: #new keys were specified + if self.keysAreBuiltin.get(): #current key set is a built-in + message = ('Your changes will be saved as a new Custom Key Set.' + ' Enter a name for your new Custom Key Set below.') + newKeySet = self.GetNewKeysName(message) + if not newKeySet: #user cancelled custom key set creation + self.listBindings.select_set(listIndex) + self.listBindings.select_anchor(listIndex) + return + else: #create new custom key set based on previously active key set + self.CreateNewKeySet(newKeySet) + self.listBindings.delete(listIndex) + self.listBindings.insert(listIndex, bindName+' - '+newKeys) + self.listBindings.select_set(listIndex) + self.listBindings.select_anchor(listIndex) + self.keyBinding.set(newKeys) + else: + self.listBindings.select_set(listIndex) + self.listBindings.select_anchor(listIndex) + + def GetNewKeysName(self, message): + usedNames = (idleConf.GetSectionList('user', 'keys') + + idleConf.GetSectionList('default', 'keys')) + newKeySet = GetCfgSectionNameDialog( + self, 'New Custom Key Set', message, usedNames).result + return newKeySet + + def SaveAsNewKeySet(self): + newKeysName = self.GetNewKeysName('New Key Set Name:') + if newKeysName: + self.CreateNewKeySet(newKeysName) + + def KeyBindingSelected(self, event): + self.buttonNewKeys.config(state=NORMAL) + + def CreateNewKeySet(self, newKeySetName): + #creates new custom key set based on the previously active key set, + #and makes the new key set active + if self.keysAreBuiltin.get(): + prevKeySetName = self.builtinKeys.get() + else: + prevKeySetName = self.customKeys.get() + prevKeys = idleConf.GetCoreKeys(prevKeySetName) + newKeys = {} + for event in prevKeys: #add key set to changed items + eventName = event[2:-2] #trim off the angle brackets + binding = ' '.join(prevKeys[event]) + newKeys[eventName] = binding + #handle any unsaved changes to prev key set + if prevKeySetName in self.changedItems['keys']: + keySetChanges = self.changedItems['keys'][prevKeySetName] + for event in keySetChanges: + newKeys[event] = keySetChanges[event] + #save the new theme + self.SaveNewKeySet(newKeySetName, newKeys) + #change gui over to the new key set + customKeyList = idleConf.GetSectionList('user', 'keys') + customKeyList.sort() + self.optMenuKeysCustom.SetMenu(customKeyList, newKeySetName) + self.keysAreBuiltin.set(0) + self.SetKeysType() + + def LoadKeysList(self, keySetName): + reselect = 0 + newKeySet = 0 + if self.listBindings.curselection(): + reselect = 1 + listIndex = self.listBindings.index(ANCHOR) + keySet = idleConf.GetKeySet(keySetName) + bindNames = keySet.keys() + bindNames.sort() + self.listBindings.delete(0, END) + for bindName in bindNames: + key = ' '.join(keySet[bindName]) #make key(s) into a string + bindName = bindName[2:-2] #trim off the angle brackets + if keySetName in self.changedItems['keys']: + #handle any unsaved changes to this key set + if bindName in self.changedItems['keys'][keySetName]: + key = self.changedItems['keys'][keySetName][bindName] + self.listBindings.insert(END, bindName+' - '+key) + if reselect: + self.listBindings.see(listIndex) + self.listBindings.select_set(listIndex) + self.listBindings.select_anchor(listIndex) + + def DeleteCustomKeys(self): + keySetName=self.customKeys.get() + delmsg = 'Are you sure you wish to delete the key set %r ?' + if not tkMessageBox.askyesno( + 'Delete Key Set', delmsg % keySetName, parent=self): + return + self.DeactivateCurrentConfig() + #remove key set from config + idleConf.userCfg['keys'].remove_section(keySetName) + if keySetName in self.changedItems['keys']: + del(self.changedItems['keys'][keySetName]) + #write changes + idleConf.userCfg['keys'].Save() + #reload user key set list + itemList = idleConf.GetSectionList('user', 'keys') + itemList.sort() + if not itemList: + self.radioKeysCustom.config(state=DISABLED) + self.optMenuKeysCustom.SetMenu(itemList, '- no custom keys -') + else: + self.optMenuKeysCustom.SetMenu(itemList, itemList[0]) + #revert to default key set + self.keysAreBuiltin.set(idleConf.defaultCfg['main'].Get('Keys', 'default')) + self.builtinKeys.set(idleConf.defaultCfg['main'].Get('Keys', 'name')) + #user can't back out of these changes, they must be applied now + self.SaveAllChangedConfigs() + self.ActivateConfigChanges() + self.SetKeysType() + + def DeleteCustomTheme(self): + themeName = self.customTheme.get() + delmsg = 'Are you sure you wish to delete the theme %r ?' + if not tkMessageBox.askyesno( + 'Delete Theme', delmsg % themeName, parent=self): + return + self.DeactivateCurrentConfig() + #remove theme from config + idleConf.userCfg['highlight'].remove_section(themeName) + if themeName in self.changedItems['highlight']: + del(self.changedItems['highlight'][themeName]) + #write changes + idleConf.userCfg['highlight'].Save() + #reload user theme list + itemList = idleConf.GetSectionList('user', 'highlight') + itemList.sort() + if not itemList: + self.radioThemeCustom.config(state=DISABLED) + self.optMenuThemeCustom.SetMenu(itemList, '- no custom themes -') + else: + self.optMenuThemeCustom.SetMenu(itemList, itemList[0]) + #revert to default theme + self.themeIsBuiltin.set(idleConf.defaultCfg['main'].Get('Theme', 'default')) + self.builtinTheme.set(idleConf.defaultCfg['main'].Get('Theme', 'name')) + #user can't back out of these changes, they must be applied now + self.SaveAllChangedConfigs() + self.ActivateConfigChanges() + self.SetThemeType() + + def GetColour(self): + target = self.highlightTarget.get() + prevColour = self.frameColourSet.cget('bg') + rgbTuplet, colourString = tkColorChooser.askcolor( + parent=self, title='Pick new colour for : '+target, + initialcolor=prevColour) + if colourString and (colourString != prevColour): + #user didn't cancel, and they chose a new colour + if self.themeIsBuiltin.get(): #current theme is a built-in + message = ('Your changes will be saved as a new Custom Theme. ' + 'Enter a name for your new Custom Theme below.') + newTheme = self.GetNewThemeName(message) + if not newTheme: #user cancelled custom theme creation + return + else: #create new custom theme based on previously active theme + self.CreateNewTheme(newTheme) + self.colour.set(colourString) + else: #current theme is user defined + self.colour.set(colourString) + + def OnNewColourSet(self): + newColour=self.colour.get() + self.frameColourSet.config(bg=newColour) #set sample + plane ='foreground' if self.fgHilite.get() else 'background' + sampleElement = self.themeElements[self.highlightTarget.get()][0] + self.textHighlightSample.tag_config(sampleElement, **{plane:newColour}) + theme = self.customTheme.get() + themeElement = sampleElement + '-' + plane + self.AddChangedItem('highlight', theme, themeElement, newColour) + + def GetNewThemeName(self, message): + usedNames = (idleConf.GetSectionList('user', 'highlight') + + idleConf.GetSectionList('default', 'highlight')) + newTheme = GetCfgSectionNameDialog( + self, 'New Custom Theme', message, usedNames).result + return newTheme + + def SaveAsNewTheme(self): + newThemeName = self.GetNewThemeName('New Theme Name:') + if newThemeName: + self.CreateNewTheme(newThemeName) + + def CreateNewTheme(self, newThemeName): + #creates new custom theme based on the previously active theme, + #and makes the new theme active + if self.themeIsBuiltin.get(): + themeType = 'default' + themeName = self.builtinTheme.get() + else: + themeType = 'user' + themeName = self.customTheme.get() + newTheme = idleConf.GetThemeDict(themeType, themeName) + #apply any of the old theme's unsaved changes to the new theme + if themeName in self.changedItems['highlight']: + themeChanges = self.changedItems['highlight'][themeName] + for element in themeChanges: + newTheme[element] = themeChanges[element] + #save the new theme + self.SaveNewTheme(newThemeName, newTheme) + #change gui over to the new theme + customThemeList = idleConf.GetSectionList('user', 'highlight') + customThemeList.sort() + self.optMenuThemeCustom.SetMenu(customThemeList, newThemeName) + self.themeIsBuiltin.set(0) + self.SetThemeType() + + def OnListFontButtonRelease(self, event): + font = self.listFontName.get(ANCHOR) + self.fontName.set(font.lower()) + self.SetFontSample() + + def SetFontSample(self, event=None): + fontName = self.fontName.get() + fontWeight = tkFont.BOLD if self.fontBold.get() else tkFont.NORMAL + newFont = (fontName, self.fontSize.get(), fontWeight) + self.labelFontSample.config(font=newFont) + self.textHighlightSample.configure(font=newFont) + + def SetHighlightTarget(self): + if self.highlightTarget.get() == 'Cursor': #bg not possible + self.radioFg.config(state=DISABLED) + self.radioBg.config(state=DISABLED) + self.fgHilite.set(1) + else: #both fg and bg can be set + self.radioFg.config(state=NORMAL) + self.radioBg.config(state=NORMAL) + self.fgHilite.set(1) + self.SetColourSample() + + def SetColourSampleBinding(self, *args): + self.SetColourSample() + + def SetColourSample(self): + #set the colour smaple area + tag = self.themeElements[self.highlightTarget.get()][0] + plane = 'foreground' if self.fgHilite.get() else 'background' + colour = self.textHighlightSample.tag_cget(tag, plane) + self.frameColourSet.config(bg=colour) + + def PaintThemeSample(self): + if self.themeIsBuiltin.get(): #a default theme + theme = self.builtinTheme.get() + else: #a user theme + theme = self.customTheme.get() + for elementTitle in self.themeElements: + element = self.themeElements[elementTitle][0] + colours = idleConf.GetHighlight(theme, element) + if element == 'cursor': #cursor sample needs special painting + colours['background'] = idleConf.GetHighlight( + theme, 'normal', fgBg='bg') + #handle any unsaved changes to this theme + if theme in self.changedItems['highlight']: + themeDict = self.changedItems['highlight'][theme] + if element + '-foreground' in themeDict: + colours['foreground'] = themeDict[element + '-foreground'] + if element + '-background' in themeDict: + colours['background'] = themeDict[element + '-background'] + self.textHighlightSample.tag_config(element, **colours) + self.SetColourSample() + + def HelpSourceSelected(self, event): + self.SetHelpListButtonStates() + + def SetHelpListButtonStates(self): + if self.listHelp.size() < 1: #no entries in list + self.buttonHelpListEdit.config(state=DISABLED) + self.buttonHelpListRemove.config(state=DISABLED) + else: #there are some entries + if self.listHelp.curselection(): #there currently is a selection + self.buttonHelpListEdit.config(state=NORMAL) + self.buttonHelpListRemove.config(state=NORMAL) + else: #there currently is not a selection + self.buttonHelpListEdit.config(state=DISABLED) + self.buttonHelpListRemove.config(state=DISABLED) + + def HelpListItemAdd(self): + helpSource = GetHelpSourceDialog(self, 'New Help Source').result + if helpSource: + self.userHelpList.append((helpSource[0], helpSource[1])) + self.listHelp.insert(END, helpSource[0]) + self.UpdateUserHelpChangedItems() + self.SetHelpListButtonStates() + + def HelpListItemEdit(self): + itemIndex = self.listHelp.index(ANCHOR) + helpSource = self.userHelpList[itemIndex] + newHelpSource = GetHelpSourceDialog( + self, 'Edit Help Source', menuItem=helpSource[0], + filePath=helpSource[1]).result + if (not newHelpSource) or (newHelpSource == helpSource): + return #no changes + self.userHelpList[itemIndex] = newHelpSource + self.listHelp.delete(itemIndex) + self.listHelp.insert(itemIndex, newHelpSource[0]) + self.UpdateUserHelpChangedItems() + self.SetHelpListButtonStates() + + def HelpListItemRemove(self): + itemIndex = self.listHelp.index(ANCHOR) + del(self.userHelpList[itemIndex]) + self.listHelp.delete(itemIndex) + self.UpdateUserHelpChangedItems() + self.SetHelpListButtonStates() + + def UpdateUserHelpChangedItems(self): + "Clear and rebuild the HelpFiles section in self.changedItems" + self.changedItems['main']['HelpFiles'] = {} + for num in range(1, len(self.userHelpList) + 1): + self.AddChangedItem( + 'main', 'HelpFiles', str(num), + ';'.join(self.userHelpList[num-1][:2])) + + def LoadFontCfg(self): + ##base editor font selection list + fonts = list(tkFont.families(self)) + fonts.sort() + for font in fonts: + self.listFontName.insert(END, font) + configuredFont = idleConf.GetFont(self, 'main', 'EditorWindow') + fontName = configuredFont[0].lower() + fontSize = configuredFont[1] + fontBold = configuredFont[2]=='bold' + self.fontName.set(fontName) + lc_fonts = [s.lower() for s in fonts] + try: + currentFontIndex = lc_fonts.index(fontName) + self.listFontName.see(currentFontIndex) + self.listFontName.select_set(currentFontIndex) + self.listFontName.select_anchor(currentFontIndex) + except ValueError: + pass + ##font size dropdown + self.optMenuFontSize.SetMenu(('7', '8', '9', '10', '11', '12', '13', + '14', '16', '18', '20', '22', + '25', '29', '34', '40'), fontSize ) + ##fontWeight + self.fontBold.set(fontBold) + ##font sample + self.SetFontSample() + + def LoadTabCfg(self): + ##indent sizes + spaceNum = idleConf.GetOption( + 'main', 'Indent', 'num-spaces', default=4, type='int') + self.spaceNum.set(spaceNum) + + def LoadThemeCfg(self): + ##current theme type radiobutton + self.themeIsBuiltin.set(idleConf.GetOption( + 'main', 'Theme', 'default', type='bool', default=1)) + ##currently set theme + currentOption = idleConf.CurrentTheme() + ##load available theme option menus + if self.themeIsBuiltin.get(): #default theme selected + itemList = idleConf.GetSectionList('default', 'highlight') + itemList.sort() + self.optMenuThemeBuiltin.SetMenu(itemList, currentOption) + itemList = idleConf.GetSectionList('user', 'highlight') + itemList.sort() + if not itemList: + self.radioThemeCustom.config(state=DISABLED) + self.customTheme.set('- no custom themes -') + else: + self.optMenuThemeCustom.SetMenu(itemList, itemList[0]) + else: #user theme selected + itemList = idleConf.GetSectionList('user', 'highlight') + itemList.sort() + self.optMenuThemeCustom.SetMenu(itemList, currentOption) + itemList = idleConf.GetSectionList('default', 'highlight') + itemList.sort() + self.optMenuThemeBuiltin.SetMenu(itemList, itemList[0]) + self.SetThemeType() + ##load theme element option menu + themeNames = self.themeElements.keys() + themeNames.sort(key=lambda x: self.themeElements[x][1]) + self.optMenuHighlightTarget.SetMenu(themeNames, themeNames[0]) + self.PaintThemeSample() + self.SetHighlightTarget() + + def LoadKeyCfg(self): + ##current keys type radiobutton + self.keysAreBuiltin.set(idleConf.GetOption( + 'main', 'Keys', 'default', type='bool', default=1)) + ##currently set keys + currentOption = idleConf.CurrentKeys() + ##load available keyset option menus + if self.keysAreBuiltin.get(): #default theme selected + itemList = idleConf.GetSectionList('default', 'keys') + itemList.sort() + self.optMenuKeysBuiltin.SetMenu(itemList, currentOption) + itemList = idleConf.GetSectionList('user', 'keys') + itemList.sort() + if not itemList: + self.radioKeysCustom.config(state=DISABLED) + self.customKeys.set('- no custom keys -') + else: + self.optMenuKeysCustom.SetMenu(itemList, itemList[0]) + else: #user key set selected + itemList = idleConf.GetSectionList('user', 'keys') + itemList.sort() + self.optMenuKeysCustom.SetMenu(itemList, currentOption) + itemList = idleConf.GetSectionList('default', 'keys') + itemList.sort() + self.optMenuKeysBuiltin.SetMenu(itemList, itemList[0]) + self.SetKeysType() + ##load keyset element list + keySetName = idleConf.CurrentKeys() + self.LoadKeysList(keySetName) + + def LoadGeneralCfg(self): + #startup state + self.startupEdit.set(idleConf.GetOption( + 'main', 'General', 'editor-on-startup', default=1, type='bool')) + #autosave state + self.autoSave.set(idleConf.GetOption( + 'main', 'General', 'autosave', default=0, type='bool')) + #initial window size + self.winWidth.set(idleConf.GetOption( + 'main', 'EditorWindow', 'width', type='int')) + self.winHeight.set(idleConf.GetOption( + 'main', 'EditorWindow', 'height', type='int')) + # default source encoding + self.encoding.set(idleConf.GetOption( + 'main', 'EditorWindow', 'encoding', default='none')) + # additional help sources + self.userHelpList = idleConf.GetAllExtraHelpSourcesList() + for helpItem in self.userHelpList: + self.listHelp.insert(END, helpItem[0]) + self.SetHelpListButtonStates() + + def LoadConfigs(self): + """ + load configuration from default and user config files and populate + the widgets on the config dialog pages. + """ + ### fonts / tabs page + self.LoadFontCfg() + self.LoadTabCfg() + ### highlighting page + self.LoadThemeCfg() + ### keys page + self.LoadKeyCfg() + ### general page + self.LoadGeneralCfg() + # note: extension page handled separately + + def SaveNewKeySet(self, keySetName, keySet): + """ + save a newly created core key set. + keySetName - string, the name of the new key set + keySet - dictionary containing the new key set + """ + if not idleConf.userCfg['keys'].has_section(keySetName): + idleConf.userCfg['keys'].add_section(keySetName) + for event in keySet: + value = keySet[event] + idleConf.userCfg['keys'].SetOption(keySetName, event, value) + + def SaveNewTheme(self, themeName, theme): + """ + save a newly created theme. + themeName - string, the name of the new theme + theme - dictionary containing the new theme + """ + if not idleConf.userCfg['highlight'].has_section(themeName): + idleConf.userCfg['highlight'].add_section(themeName) + for element in theme: + value = theme[element] + idleConf.userCfg['highlight'].SetOption(themeName, element, value) + + def SetUserValue(self, configType, section, item, value): + if idleConf.defaultCfg[configType].has_option(section, item): + if idleConf.defaultCfg[configType].Get(section, item) == value: + #the setting equals a default setting, remove it from user cfg + return idleConf.userCfg[configType].RemoveOption(section, item) + #if we got here set the option + return idleConf.userCfg[configType].SetOption(section, item, value) + + def SaveAllChangedConfigs(self): + "Save configuration changes to the user config file." + idleConf.userCfg['main'].Save() + for configType in self.changedItems: + cfgTypeHasChanges = False + for section in self.changedItems[configType]: + if section == 'HelpFiles': + #this section gets completely replaced + idleConf.userCfg['main'].remove_section('HelpFiles') + cfgTypeHasChanges = True + for item in self.changedItems[configType][section]: + value = self.changedItems[configType][section][item] + if self.SetUserValue(configType, section, item, value): + cfgTypeHasChanges = True + if cfgTypeHasChanges: + idleConf.userCfg[configType].Save() + for configType in ['keys', 'highlight']: + # save these even if unchanged! + idleConf.userCfg[configType].Save() + self.ResetChangedItems() #clear the changed items dict + self.save_all_changed_extensions() # uses a different mechanism + + def DeactivateCurrentConfig(self): + #Before a config is saved, some cleanup of current + #config must be done - remove the previous keybindings + winInstances = self.parent.instance_dict + for instance in winInstances: + instance.RemoveKeybindings() + + def ActivateConfigChanges(self): + "Dynamically apply configuration changes" + winInstances = self.parent.instance_dict.keys() + for instance in winInstances: + instance.ResetColorizer() + instance.ResetFont() + instance.set_notabs_indentwidth() + instance.ApplyKeybindings() + instance.reset_help_menu_entries() + + def Cancel(self): + self.grab_release() + self.destroy() + + def Ok(self): + self.Apply() + self.grab_release() + self.destroy() + + def Apply(self): + self.DeactivateCurrentConfig() + self.SaveAllChangedConfigs() + self.ActivateConfigChanges() + + def Help(self): + page = self.tabPages._current_page + view_text(self, title='Help for IDLE preferences', + text=help_common+help_pages.get(page, '')) + + def CreatePageExtensions(self): + """Part of the config dialog used for configuring IDLE extensions. + + This code is generic - it works for any and all IDLE extensions. + + IDLE extensions save their configuration options using idleConf. + This code reads the current configuration using idleConf, supplies a + GUI interface to change the configuration values, and saves the + changes using idleConf. + + Not all changes take effect immediately - some may require restarting IDLE. + This depends on each extension's implementation. + + All values are treated as text, and it is up to the user to supply + reasonable values. The only exception to this are the 'enable*' options, + which are boolean, and can be toggled with a True/False button. + """ + parent = self.parent + frame = self.tabPages.pages['Extensions'].frame + self.ext_defaultCfg = idleConf.defaultCfg['extensions'] + self.ext_userCfg = idleConf.userCfg['extensions'] + self.is_int = self.register(is_int) + self.load_extensions() + # create widgets - a listbox shows all available extensions, with the + # controls for the extension selected in the listbox to the right + self.extension_names = StringVar(self) + frame.rowconfigure(0, weight=1) + frame.columnconfigure(2, weight=1) + self.extension_list = Listbox(frame, listvariable=self.extension_names, + selectmode='browse') + self.extension_list.bind('<<ListboxSelect>>', self.extension_selected) + scroll = Scrollbar(frame, command=self.extension_list.yview) + self.extension_list.yscrollcommand=scroll.set + self.details_frame = LabelFrame(frame, width=250, height=250) + self.extension_list.grid(column=0, row=0, sticky='nws') + scroll.grid(column=1, row=0, sticky='ns') + self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0]) + frame.configure(padx=10, pady=10) + self.config_frame = {} + self.current_extension = None + + self.outerframe = self # TEMPORARY + self.tabbed_page_set = self.extension_list # TEMPORARY + + # create the frame holding controls for each extension + ext_names = '' + for ext_name in sorted(self.extensions): + self.create_extension_frame(ext_name) + ext_names = ext_names + '{' + ext_name + '} ' + self.extension_names.set(ext_names) + self.extension_list.selection_set(0) + self.extension_selected(None) + + def load_extensions(self): + "Fill self.extensions with data from the default and user configs." + self.extensions = {} + for ext_name in idleConf.GetExtensions(active_only=False): + self.extensions[ext_name] = [] + + for ext_name in self.extensions: + opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name)) + + # bring 'enable' options to the beginning of the list + enables = [opt_name for opt_name in opt_list + if opt_name.startswith('enable')] + for opt_name in enables: + opt_list.remove(opt_name) + opt_list = enables + opt_list + + for opt_name in opt_list: + def_str = self.ext_defaultCfg.Get( + ext_name, opt_name, raw=True) + try: + def_obj = {'True':True, 'False':False}[def_str] + opt_type = 'bool' + except KeyError: + try: + def_obj = int(def_str) + opt_type = 'int' + except ValueError: + def_obj = def_str + opt_type = None + try: + value = self.ext_userCfg.Get( + ext_name, opt_name, type=opt_type, raw=True, + default=def_obj) + except ValueError: # Need this until .Get fixed + value = def_obj # bad values overwritten by entry + var = StringVar(self) + var.set(str(value)) + + self.extensions[ext_name].append({'name': opt_name, + 'type': opt_type, + 'default': def_str, + 'value': value, + 'var': var, + }) + + def extension_selected(self, event): + newsel = self.extension_list.curselection() + if newsel: + newsel = self.extension_list.get(newsel) + if newsel is None or newsel != self.current_extension: + if self.current_extension: + self.details_frame.config(text='') + self.config_frame[self.current_extension].grid_forget() + self.current_extension = None + if newsel: + self.details_frame.config(text=newsel) + self.config_frame[newsel].grid(column=0, row=0, sticky='nsew') + self.current_extension = newsel + + def create_extension_frame(self, ext_name): + """Create a frame holding the widgets to configure one extension""" + f = VerticalScrolledFrame(self.details_frame, height=250, width=250) + self.config_frame[ext_name] = f + entry_area = f.interior + # create an entry for each configuration option + for row, opt in enumerate(self.extensions[ext_name]): + # create a row with a label and entry/checkbutton + label = Label(entry_area, text=opt['name']) + label.grid(row=row, column=0, sticky=NW) + var = opt['var'] + if opt['type'] == 'bool': + Checkbutton(entry_area, textvariable=var, variable=var, + onvalue='True', offvalue='False', + indicatoron=FALSE, selectcolor='', width=8 + ).grid(row=row, column=1, sticky=W, padx=7) + elif opt['type'] == 'int': + Entry(entry_area, textvariable=var, validate='key', + validatecommand=(self.is_int, '%P') + ).grid(row=row, column=1, sticky=NSEW, padx=7) + + else: + Entry(entry_area, textvariable=var + ).grid(row=row, column=1, sticky=NSEW, padx=7) + return + + def set_extension_value(self, section, opt): + name = opt['name'] + default = opt['default'] + value = opt['var'].get().strip() or default + opt['var'].set(value) + # if self.defaultCfg.has_section(section): + # Currently, always true; if not, indent to return + if (value == default): + return self.ext_userCfg.RemoveOption(section, name) + # set the option + return self.ext_userCfg.SetOption(section, name, value) + + def save_all_changed_extensions(self): + """Save configuration changes to the user config file.""" + has_changes = False + for ext_name in self.extensions: + options = self.extensions[ext_name] + for opt in options: + if self.set_extension_value(ext_name, opt): + has_changes = True + if has_changes: + self.ext_userCfg.Save() + + +help_common = '''\ +When you click either the Apply or Ok buttons, settings in this +dialog that are different from IDLE's default are saved in +a .idlerc directory in your home directory. Except as noted, +these changes apply to all versions of IDLE installed on this +machine. Some do not take affect until IDLE is restarted. +[Cancel] only cancels changes made since the last save. +''' +help_pages = { + 'Highlighting':''' +Highlighting: +The IDLE Dark color theme is new in October 2015. It can only +be used with older IDLE releases if it is saved as a custom +theme, with a different name. +''' +} + + +def is_int(s): + "Return 's is blank or represents an int'" + if not s: + return True + try: + int(s) + return True + except ValueError: + return False + + +class VerticalScrolledFrame(Frame): + """A pure Tkinter vertically scrollable frame. + + * Use the 'interior' attribute to place widgets inside the scrollable frame + * Construct and pack/place/grid normally + * This frame only allows vertical scrolling + """ + def __init__(self, parent, *args, **kw): + Frame.__init__(self, parent, *args, **kw) + + # create a canvas object and a vertical scrollbar for scrolling it + vscrollbar = Scrollbar(self, orient=VERTICAL) + vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE) + canvas = Canvas(self, bd=0, highlightthickness=0, + yscrollcommand=vscrollbar.set, width=240) + canvas.pack(side=LEFT, fill=BOTH, expand=TRUE) + vscrollbar.config(command=canvas.yview) + + # reset the view + canvas.xview_moveto(0) + canvas.yview_moveto(0) + + # create a frame inside the canvas which will be scrolled with it + self.interior = interior = Frame(canvas) + interior_id = canvas.create_window(0, 0, window=interior, anchor=NW) + + # track changes to the canvas and frame width and sync them, + # also updating the scrollbar + def _configure_interior(event): + # update the scrollbars to match the size of the inner frame + size = (interior.winfo_reqwidth(), interior.winfo_reqheight()) + canvas.config(scrollregion="0 0 %s %s" % size) + interior.bind('<Configure>', _configure_interior) + + def _configure_canvas(event): + if interior.winfo_reqwidth() != canvas.winfo_width(): + # update the inner frame's width to fill the canvas + canvas.itemconfigure(interior_id, width=canvas.winfo_width()) + canvas.bind('<Configure>', _configure_canvas) + + return + + +if __name__ == '__main__': + import unittest + unittest.main('idlelib.idle_test.test_configdialog', + verbosity=2, exit=False) + from idlelib.idle_test.htest import run + run(ConfigDialog) diff --git a/contrib/tools/python/src/Lib/idlelib/configHandler.py b/contrib/tools/python/src/Lib/idlelib/configHandler.py new file mode 100644 index 00000000000..ca885ed2fe5 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/configHandler.py @@ -0,0 +1,772 @@ +"""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 + +from __future__ import print_function +import os +import sys + +from ConfigParser import ConfigParser +from Tkinter import TkVersion +from tkFont import Font, nametofont + +class InvalidConfigType(Exception): pass +class InvalidConfigSet(Exception): pass +class InvalidFgBg(Exception): pass +class InvalidTheme(Exception): pass + +class IdleConfParser(ConfigParser): + """ + 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) + + 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 IOError: + os.unlink(fname) + cfgFile = open(fname, 'w') + with cfgFile: + self.write(cfgFile) + else: + self.RemoveFile() + +class IdleConf: + """Hold config parsers for all idle config files in singleton instance. + + 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 IOError: + 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, IOError): + warn = ('\n Warning: unable to create user config directory\n' + + userDir + '\n Check path and permissions.\n Exiting!\n') + print(warn, file=sys.stderr) + raise SystemExit + # TODO continue without userDIr instead of exit + return userDir + + 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 IOError: + 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 IOError: + 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 IOError: + pass + theme[element] = cfgParser.Get( + themeName, element, default=theme[element]) + return theme + + def CurrentTheme(self): + """Return the name of the currently active text color theme. + + 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 '<<event>>'. + """ + 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('<Alt-', '<Option-') for x in v ] + if v != v2: + result[k] = v2 + + return result + + def GetKeySet(self, keySetName): + """Return event-key dict for keySetName core plus active extensions. + + If a binding defined in an extension is already in use, the + extension binding is disabled by being set to '' + """ + keySet = self.GetCoreKeys(keySetName) + activeExtns = self.GetExtensions(active_only=1) + for extn in activeExtns: + extKeys = self.__GetRawExtensionKeys(extn) + if extKeys: #the extension defines keybindings + for event in extKeys: + if extKeys[event] in keySet.values(): + #the binding is already in use + extKeys[event] = '' #disable this binding + keySet[event] = extKeys[event] #add binding + return keySet + + def IsCoreBinding(self, virtualEvent): + """Return True if the virtual event is one of the core idle key events. + + virtualEvent - string, name of the virtual event to test for, + without the enclosing '<< >>' + """ + return ('<<'+virtualEvent+'>>') in self.GetCoreKeys() + +# TODO make keyBindins a file or class attribute used for test above +# and copied in function below + + def GetCoreKeys(self, keySetName=None): + """Return dict of core virtual-key keybindings for keySetName. + + The default keySetName None corresponds to the keyBindings base + dict. If keySetName is not None, bindings from the config + file(s) are loaded _over_ these defaults, so if there is a + problem getting any core binding there will be an 'ultimate last + resort fallback' to the CUA-ish bindings defined here. + """ + keyBindings={ + '<<copy>>': ['<Control-c>', '<Control-C>'], + '<<cut>>': ['<Control-x>', '<Control-X>'], + '<<paste>>': ['<Control-v>', '<Control-V>'], + '<<beginning-of-line>>': ['<Control-a>', '<Home>'], + '<<center-insert>>': ['<Control-l>'], + '<<close-all-windows>>': ['<Control-q>'], + '<<close-window>>': ['<Alt-F4>'], + '<<do-nothing>>': ['<Control-x>'], + '<<end-of-file>>': ['<Control-d>'], + '<<python-docs>>': ['<F1>'], + '<<python-context-help>>': ['<Shift-F1>'], + '<<history-next>>': ['<Alt-n>'], + '<<history-previous>>': ['<Alt-p>'], + '<<interrupt-execution>>': ['<Control-c>'], + '<<view-restart>>': ['<F6>'], + '<<restart-shell>>': ['<Control-F6>'], + '<<open-class-browser>>': ['<Alt-c>'], + '<<open-module>>': ['<Alt-m>'], + '<<open-new-window>>': ['<Control-n>'], + '<<open-window-from-file>>': ['<Control-o>'], + '<<plain-newline-and-indent>>': ['<Control-j>'], + '<<print-window>>': ['<Control-p>'], + '<<redo>>': ['<Control-y>'], + '<<remove-selection>>': ['<Escape>'], + '<<save-copy-of-window-as-file>>': ['<Alt-Shift-S>'], + '<<save-window-as-file>>': ['<Alt-s>'], + '<<save-window>>': ['<Control-s>'], + '<<select-all>>': ['<Alt-a>'], + '<<toggle-auto-coloring>>': ['<Control-slash>'], + '<<undo>>': ['<Control-z>'], + '<<find-again>>': ['<Control-g>', '<F3>'], + '<<find-in-files>>': ['<Alt-F3>'], + '<<find-selection>>': ['<Control-F3>'], + '<<find>>': ['<Control-f>'], + '<<replace>>': ['<Control-h>'], + '<<goto-line>>': ['<Alt-g>'], + '<<smart-backspace>>': ['<Key-BackSpace>'], + '<<newline-and-indent>>': ['<Key-Return>', '<Key-KP_Enter>'], + '<<smart-indent>>': ['<Key-Tab>'], + '<<indent-region>>': ['<Control-Key-bracketright>'], + '<<dedent-region>>': ['<Control-Key-bracketleft>'], + '<<comment-region>>': ['<Alt-Key-3>'], + '<<uncomment-region>>': ['<Alt-Key-4>'], + '<<tabify-region>>': ['<Alt-Key-5>'], + '<<untabify-region>>': ['<Alt-Key-6>'], + '<<toggle-tabs>>': ['<Alt-Key-t>'], + '<<change-indentwidth>>': ['<Alt-Key-u>'], + '<<del-word-left>>': ['<Control-Key-BackSpace>'], + '<<del-word-right>>': ['<Control-Key-Delete>'] + } + 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 IOError: + 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: int(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 +# +if __name__ == '__main__': + from zlib import crc32 + line, crc = 0, 0 + + def sprint(obj): + global line, crc + txt = str(obj) + line += 1 + crc = crc32(txt.encode(encoding='utf-8'), crc) + print(txt) + #print('***', line, crc, '***') # uncomment for diagnosis + + def dumpCfg(cfg): + print('\n', cfg, '\n') # has variable '0xnnnnnnnn' addresses + for key in sorted(cfg.keys()): + sections = cfg[key].sections() + sprint(key) + sprint(sections) + for section in sections: + options = cfg[key].options(section) + sprint(section) + sprint(options) + for option in options: + sprint(option + ' = ' + cfg[key].Get(section, option)) + + dumpCfg(idleConf.defaultCfg) + dumpCfg(idleConf.userCfg) + print('\nlines = ', line, ', crc = ', crc, sep='') diff --git a/contrib/tools/python/src/Lib/idlelib/configHelpSourceEdit.py b/contrib/tools/python/src/Lib/idlelib/configHelpSourceEdit.py new file mode 100644 index 00000000000..62b010a0ca2 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/configHelpSourceEdit.py @@ -0,0 +1,168 @@ +"Dialog to specify or edit the parameters for a user configured help source." + +import os +import sys + +from Tkinter import * +import tkMessageBox +import tkFileDialog + +class GetHelpSourceDialog(Toplevel): + def __init__(self, parent, title, menuItem='', filePath='', _htest=False): + """Get menu entry and url/ local file location for Additional Help + + User selects a name for the Help resource and provides a web url + or a local file as its source. The user can enter a url or browse + for the file. + + _htest - bool, change box location when running htest + """ + Toplevel.__init__(self, parent) + self.configure(borderwidth=5) + self.resizable(height=FALSE, width=FALSE) + self.title(title) + self.transient(parent) + self.grab_set() + self.protocol("WM_DELETE_WINDOW", self.Cancel) + self.parent = parent + self.result = None + self.CreateWidgets() + self.menu.set(menuItem) + self.path.set(filePath) + self.withdraw() #hide while setting geometry + #needs to be done here so that the winfo_reqwidth is valid + self.update_idletasks() + #centre dialog over parent. below parent if running htest. + self.geometry( + "+%d+%d" % ( + parent.winfo_rootx() + + (parent.winfo_width()/2 - self.winfo_reqwidth()/2), + parent.winfo_rooty() + + ((parent.winfo_height()/2 - self.winfo_reqheight()/2) + if not _htest else 150))) + self.deiconify() #geometry set, unhide + self.bind('<Return>', self.Ok) + self.wait_window() + + def CreateWidgets(self): + self.menu = StringVar(self) + self.path = StringVar(self) + self.fontSize = StringVar(self) + self.frameMain = Frame(self, borderwidth=2, relief=GROOVE) + self.frameMain.pack(side=TOP, expand=TRUE, fill=BOTH) + labelMenu = Label(self.frameMain, anchor=W, justify=LEFT, + text='Menu Item:') + self.entryMenu = Entry(self.frameMain, textvariable=self.menu, + width=30) + self.entryMenu.focus_set() + labelPath = Label(self.frameMain, anchor=W, justify=LEFT, + text='Help File Path: Enter URL or browse for file') + self.entryPath = Entry(self.frameMain, textvariable=self.path, + width=40) + self.entryMenu.focus_set() + labelMenu.pack(anchor=W, padx=5, pady=3) + self.entryMenu.pack(anchor=W, padx=5, pady=3) + labelPath.pack(anchor=W, padx=5, pady=3) + self.entryPath.pack(anchor=W, padx=5, pady=3) + browseButton = Button(self.frameMain, text='Browse', width=8, + command=self.browseFile) + browseButton.pack(pady=3) + frameButtons = Frame(self) + frameButtons.pack(side=BOTTOM, fill=X) + self.buttonOk = Button(frameButtons, text='OK', + width=8, default=ACTIVE, command=self.Ok) + self.buttonOk.grid(row=0, column=0, padx=5,pady=5) + self.buttonCancel = Button(frameButtons, text='Cancel', + width=8, command=self.Cancel) + self.buttonCancel.grid(row=0, column=1, padx=5, pady=5) + + def browseFile(self): + filetypes = [ + ("HTML Files", "*.htm *.html", "TEXT"), + ("PDF Files", "*.pdf", "TEXT"), + ("Windows Help Files", "*.chm"), + ("Text Files", "*.txt", "TEXT"), + ("All Files", "*")] + path = self.path.get() + if path: + dir, base = os.path.split(path) + else: + base = None + if sys.platform[:3] == 'win': + dir = os.path.join(os.path.dirname(sys.executable), 'Doc') + if not os.path.isdir(dir): + dir = os.getcwd() + else: + dir = os.getcwd() + opendialog = tkFileDialog.Open(parent=self, filetypes=filetypes) + file = opendialog.show(initialdir=dir, initialfile=base) + if file: + self.path.set(file) + + def MenuOk(self): + "Simple validity check for a sensible menu item name" + menuOk = True + menu = self.menu.get() + menu.strip() + if not menu: + tkMessageBox.showerror(title='Menu Item Error', + message='No menu item specified', + parent=self) + self.entryMenu.focus_set() + menuOk = False + elif len(menu) > 30: + tkMessageBox.showerror(title='Menu Item Error', + message='Menu item too long:' + '\nLimit 30 characters.', + parent=self) + self.entryMenu.focus_set() + menuOk = False + return menuOk + + def PathOk(self): + "Simple validity check for menu file path" + pathOk = True + path = self.path.get() + path.strip() + if not path: #no path specified + tkMessageBox.showerror(title='File Path Error', + message='No help file path specified.', + parent=self) + self.entryPath.focus_set() + pathOk = False + elif path.startswith(('www.', 'http')): + pass + else: + if path[:5] == 'file:': + path = path[5:] + if not os.path.exists(path): + tkMessageBox.showerror(title='File Path Error', + message='Help file path does not exist.', + parent=self) + self.entryPath.focus_set() + pathOk = False + return pathOk + + def Ok(self, event=None): + if self.MenuOk() and self.PathOk(): + self.result = (self.menu.get().strip(), + self.path.get().strip()) + if sys.platform == 'darwin': + path = self.result[1] + if path.startswith(('www', 'file:', 'http:')): + pass + else: + # Mac Safari insists on using the URI form for local files + self.result = list(self.result) + self.result[1] = "file://" + path + self.grab_release() + self.destroy() + + def Cancel(self, event=None): + self.result = None + self.grab_release() + self.destroy() + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(GetHelpSourceDialog) diff --git a/contrib/tools/python/src/Lib/idlelib/configSectionNameDialog.py b/contrib/tools/python/src/Lib/idlelib/configSectionNameDialog.py new file mode 100644 index 00000000000..f28dc1a2836 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/configSectionNameDialog.py @@ -0,0 +1,95 @@ +""" +Dialog that allows user to specify a new config file section name. +Used to get new highlight theme and keybinding set names. +The 'return value' for the dialog, used two placed in configDialog.py, +is the .result attribute set in the Ok and Cancel methods. +""" +from Tkinter import * +import tkMessageBox +class GetCfgSectionNameDialog(Toplevel): + def __init__(self, parent, title, message, used_names, _htest=False): + """ + message - string, informational message to display + used_names - string collection, names already in use for validity check + _htest - bool, change box location when running htest + """ + Toplevel.__init__(self, parent) + self.configure(borderwidth=5) + self.resizable(height=FALSE, width=FALSE) + self.title(title) + self.transient(parent) + self.grab_set() + self.protocol("WM_DELETE_WINDOW", self.Cancel) + self.parent = parent + self.message = message + self.used_names = used_names + self.create_widgets() + self.withdraw() #hide while setting geometry + self.update_idletasks() + #needs to be done here so that the winfo_reqwidth is valid + self.messageInfo.config(width=self.frameMain.winfo_reqwidth()) + self.geometry( + "+%d+%d" % ( + parent.winfo_rootx() + + (parent.winfo_width()/2 - self.winfo_reqwidth()/2), + parent.winfo_rooty() + + ((parent.winfo_height()/2 - self.winfo_reqheight()/2) + if not _htest else 100) + ) ) #centre dialog over parent (or below htest box) + self.deiconify() #geometry set, unhide + self.wait_window() + def create_widgets(self): + self.name = StringVar(self.parent) + self.fontSize = StringVar(self.parent) + self.frameMain = Frame(self, borderwidth=2, relief=SUNKEN) + self.frameMain.pack(side=TOP, expand=TRUE, fill=BOTH) + self.messageInfo = Message(self.frameMain, anchor=W, justify=LEFT, + padx=5, pady=5, text=self.message) #,aspect=200) + entryName = Entry(self.frameMain, textvariable=self.name, width=30) + entryName.focus_set() + self.messageInfo.pack(padx=5, pady=5) #, expand=TRUE, fill=BOTH) + entryName.pack(padx=5, pady=5) + frameButtons = Frame(self, pady=2) + frameButtons.pack(side=BOTTOM) + self.buttonOk = Button(frameButtons, text='Ok', + width=8, command=self.Ok) + self.buttonOk.pack(side=LEFT, padx=5) + self.buttonCancel = Button(frameButtons, text='Cancel', + width=8, command=self.Cancel) + self.buttonCancel.pack(side=RIGHT, padx=5) + + def name_ok(self): + ''' After stripping entered name, check that it is a sensible + ConfigParser file section name. Return it if it is, '' if not. + ''' + name = self.name.get().strip() + if not name: #no name specified + tkMessageBox.showerror(title='Name Error', + message='No name specified.', parent=self) + elif len(name)>30: #name too long + tkMessageBox.showerror(title='Name Error', + message='Name too long. It should be no more than '+ + '30 characters.', parent=self) + name = '' + elif name in self.used_names: + tkMessageBox.showerror(title='Name Error', + message='This name is already in use.', parent=self) + name = '' + return name + def Ok(self, event=None): + name = self.name_ok() + if name: + self.result = name + self.grab_release() + self.destroy() + def Cancel(self, event=None): + self.result = '' + self.grab_release() + self.destroy() + +if __name__ == '__main__': + import unittest + unittest.main('idlelib.idle_test.test_config_name', verbosity=2, exit=False) + + from idlelib.idle_test.htest import run + run(GetCfgSectionNameDialog) diff --git a/contrib/tools/python/src/Lib/idlelib/dynOptionMenuWidget.py b/contrib/tools/python/src/Lib/idlelib/dynOptionMenuWidget.py new file mode 100644 index 00000000000..beca9e2e72c --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/dynOptionMenuWidget.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 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/contrib/tools/python/src/Lib/idlelib/help.py b/contrib/tools/python/src/Lib/idlelib/help.py new file mode 100644 index 00000000000..3ab4851336b --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/help.py @@ -0,0 +1,277 @@ +""" help.py: Implement the Idle help menu. +Contents are subject to revision at any time, without notice. + + +Help => About IDLE: diplay About Idle dialog + +<to be moved here from aboutDialog.py> + + +Help => IDLE Help: Display help.html with proper formatting. +Doc/library/idle.rst (Sphinx)=> Doc/build/html/library/idle.html +(help.copy_strip)=> Lib/idlelib/help.html + +HelpParser - Parse help.html and render to tk Text. + +HelpText - Display formatted help.html. + +HelpFrame - Contain text, scrollbar, and table-of-contents. +(This will be needed for display in a future tabbed window.) + +HelpWindow - Display HelpFrame in a standalone window. + +copy_strip - Copy idle.html to help.html, rstripping each line. + +show_idlehelp - Create HelpWindow. Called in EditorWindow.help_dialog. +""" +from HTMLParser import HTMLParser +from os.path import abspath, dirname, isdir, isfile, join +from platform import python_version +from Tkinter import Tk, Toplevel, Frame, Text, Scrollbar, Menu, Menubutton +import tkFont as tkfont +from idlelib.configHandler import idleConf + +use_ttk = False # until available to import +if use_ttk: + from tkinter.ttk import Menubutton + +## About IDLE ## + + +## IDLE Help ## + +class HelpParser(HTMLParser): + """Render help.html into a text widget. + + The overridden handle_xyz methods handle a subset of html tags. + The supplied text should have the needed tag configurations. + The behavior for unsupported tags, such as table, is undefined. + If the tags generated by Sphinx change, this class, especially + the handle_starttag and handle_endtags methods, might have to also. + """ + def __init__(self, text): + HTMLParser.__init__(self) + self.text = text # text widget we're rendering into + self.tags = '' # current block level text tags to apply + self.chartags = '' # current character level text tags + self.show = False # used so we exclude page navigation + self.hdrlink = False # used so we don't show header links + self.level = 0 # indentation level + self.pre = False # displaying preformatted text + self.hprefix = '' # prefix such as '25.5' to strip from headings + self.nested_dl = False # if we're in a nested <dl> + self.simplelist = False # simple list (no double spacing) + self.toc = [] # pair headers with text indexes for toc + self.header = '' # text within header tags for toc + + def indent(self, amt=1): + self.level += amt + self.tags = '' if self.level == 0 else 'l'+str(self.level) + + def handle_starttag(self, tag, attrs): + "Handle starttags in help.html." + class_ = '' + for a, v in attrs: + if a == 'class': + class_ = v + s = '' + if tag == 'div' and class_ == 'section': + self.show = True # start of main content + elif tag == 'div' and class_ == 'sphinxsidebar': + self.show = False # end of main content + elif tag == 'p' and class_ != 'first': + s = '\n\n' + elif tag == 'span' and class_ == 'pre': + self.chartags = 'pre' + elif tag == 'span' and class_ == 'versionmodified': + self.chartags = 'em' + elif tag == 'em': + self.chartags = 'em' + elif tag in ['ul', 'ol']: + if class_.find('simple') != -1: + s = '\n' + self.simplelist = True + else: + self.simplelist = False + self.indent() + elif tag == 'dl': + if self.level > 0: + self.nested_dl = True + elif tag == 'li': + s = '\n* ' if self.simplelist else '\n\n* ' + elif tag == 'dt': + s = '\n\n' if not self.nested_dl else '\n' # avoid extra line + self.nested_dl = False + elif tag == 'dd': + self.indent() + s = '\n' + elif tag == 'pre': + self.pre = True + if self.show: + self.text.insert('end', '\n\n') + self.tags = 'preblock' + elif tag == 'a' and class_ == 'headerlink': + self.hdrlink = True + elif tag == 'h1': + self.tags = tag + elif tag in ['h2', 'h3']: + if self.show: + self.header = '' + self.text.insert('end', '\n\n') + self.tags = tag + if self.show: + self.text.insert('end', s, (self.tags, self.chartags)) + + def handle_endtag(self, tag): + "Handle endtags in help.html." + if tag in ['h1', 'h2', 'h3']: + self.indent(0) # clear tag, reset indent + if self.show: + self.toc.append((self.header, self.text.index('insert'))) + elif tag in ['span', 'em']: + self.chartags = '' + elif tag == 'a': + self.hdrlink = False + elif tag == 'pre': + self.pre = False + self.tags = '' + elif tag in ['ul', 'dd', 'ol']: + self.indent(amt=-1) + + def handle_data(self, data): + "Handle date segments in help.html." + if self.show and not self.hdrlink: + d = data if self.pre else data.replace('\n', ' ') + if self.tags == 'h1': + self.hprefix = d[0:d.index(' ')] + if self.tags in ['h1', 'h2', 'h3'] and self.hprefix != '': + if d[0:len(self.hprefix)] == self.hprefix: + d = d[len(self.hprefix):].strip() + self.header += d + self.text.insert('end', d, (self.tags, self.chartags)) + + def handle_charref(self, name): + if self.show: + self.text.insert('end', unichr(int(name))) + + +class HelpText(Text): + "Display help.html." + def __init__(self, parent, filename): + "Configure tags and feed file to parser." + uwide = idleConf.GetOption('main', 'EditorWindow', 'width', type='int') + uhigh = idleConf.GetOption('main', 'EditorWindow', 'height', type='int') + uhigh = 3 * uhigh // 4 # lines average 4/3 of editor line height + Text.__init__(self, parent, wrap='word', highlightthickness=0, + padx=5, borderwidth=0, width=uwide, height=uhigh) + + normalfont = self.findfont(['TkDefaultFont', 'arial', 'helvetica']) + fixedfont = self.findfont(['TkFixedFont', 'monaco', 'courier']) + self['font'] = (normalfont, 12) + self.tag_configure('em', font=(normalfont, 12, 'italic')) + self.tag_configure('h1', font=(normalfont, 20, 'bold')) + self.tag_configure('h2', font=(normalfont, 18, 'bold')) + self.tag_configure('h3', font=(normalfont, 15, 'bold')) + self.tag_configure('pre', font=(fixedfont, 12), background='#f6f6ff') + self.tag_configure('preblock', font=(fixedfont, 10), lmargin1=25, + borderwidth=1, relief='solid', background='#eeffcc') + self.tag_configure('l1', lmargin1=25, lmargin2=25) + self.tag_configure('l2', lmargin1=50, lmargin2=50) + self.tag_configure('l3', lmargin1=75, lmargin2=75) + self.tag_configure('l4', lmargin1=100, lmargin2=100) + + self.parser = HelpParser(self) + with open(filename) as f: + contents = f.read().decode(encoding='utf-8') + self.parser.feed(contents) + self['state'] = 'disabled' + + def findfont(self, names): + "Return name of first font family derived from names." + for name in names: + if name.lower() in (x.lower() for x in tkfont.names(root=self)): + font = tkfont.Font(name=name, exists=True, root=self) + return font.actual()['family'] + elif name.lower() in (x.lower() + for x in tkfont.families(root=self)): + return name + + +class HelpFrame(Frame): + "Display html text, scrollbar, and toc." + def __init__(self, parent, filename): + Frame.__init__(self, parent) + text = HelpText(self, filename) + self['background'] = text['background'] + scroll = Scrollbar(self, command=text.yview) + text['yscrollcommand'] = scroll.set + self.rowconfigure(0, weight=1) + self.columnconfigure(1, weight=1) # text + self.toc_menu(text).grid(column=0, row=0, sticky='nw') + text.grid(column=1, row=0, sticky='nsew') + scroll.grid(column=2, row=0, sticky='ns') + + def toc_menu(self, text): + "Create table of contents as drop-down menu." + toc = Menubutton(self, text='TOC') + drop = Menu(toc, tearoff=False) + for lbl, dex in text.parser.toc: + drop.add_command(label=lbl, command=lambda dex=dex:text.yview(dex)) + toc['menu'] = drop + return toc + + +class HelpWindow(Toplevel): + "Display frame with rendered html." + def __init__(self, parent, filename, title): + Toplevel.__init__(self, parent) + self.wm_title(title) + self.protocol("WM_DELETE_WINDOW", self.destroy) + HelpFrame(self, filename).grid(column=0, row=0, sticky='nsew') + self.grid_columnconfigure(0, weight=1) + self.grid_rowconfigure(0, weight=1) + + +def copy_strip(): + """Copy idle.html to idlelib/help.html, stripping trailing whitespace. + + Files with trailing whitespace cannot be pushed to the hg cpython + repository. For 3.x (on Windows), help.html is generated, after + editing idle.rst in the earliest maintenance version, with + sphinx-build -bhtml . build/html + python_d.exe -c "from idlelib.help import copy_strip; copy_strip()" + After refreshing TortoiseHG workshop to generate a diff, + check both the diff and displayed text. Push the diff along with + the idle.rst change and merge both into default (or an intermediate + maintenance version). + + When the 'earlist' version gets its final maintenance release, + do an update as described above, without editing idle.rst, to + rebase help.html on the next version of idle.rst. Do not worry + about version changes as version is not displayed. Examine other + changes and the result of Help -> IDLE Help. + + If maintenance and default versions of idle.rst diverge, and + merging does not go smoothly, then consider generating + separate help.html files from separate idle.htmls. + """ + src = join(abspath(dirname(dirname(dirname(__file__)))), + 'Doc', 'build', 'html', 'library', 'idle.html') + dst = join(abspath(dirname(__file__)), 'help.html') + with open(src, 'r') as inn,\ + open(dst, 'w') as out: + for line in inn: + out.write(line.rstrip() + '\n') + print('idle.html copied to help.html') + +def show_idlehelp(parent): + "Create HelpWindow; called from Idle Help event handler." + filename = join(abspath(dirname(__file__)), 'help.html') + if not isfile(filename): + # try copy_strip, present message + return + HelpWindow(parent, filename, 'IDLE Help (%s)' % python_version()) + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(show_idlehelp) diff --git a/contrib/tools/python/src/Lib/idlelib/idle.py b/contrib/tools/python/src/Lib/idlelib/idle.py new file mode 100644 index 00000000000..141534dfe13 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle.py @@ -0,0 +1,13 @@ +import os.path +import sys + +# Enable running IDLE with idlelib in a non-standard location. +# This was once used to run development versions of IDLE. +# Because PEP 434 declared idle.py a public interface, +# removal should require deprecation. +idlelib_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if idlelib_dir not in sys.path: + sys.path.insert(0, idlelib_dir) + +from idlelib.PyShell import main # This is subject to change +main() diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/__init__.py b/contrib/tools/python/src/Lib/idlelib/idle_test/__init__.py new file mode 100644 index 00000000000..845c92d372a --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/__init__.py @@ -0,0 +1,15 @@ +'''idlelib.idle_test is a private implementation of test.test_idle, +which tests the IDLE application as part of the stdlib test suite. +Run IDLE tests alone with "python -m test.test_idle". +This package and its contained modules are subject to change and +any direct use is at your own risk. +''' +from os.path import dirname + +def load_tests(loader, standard_tests, pattern): + this_dir = dirname(__file__) + top_dir = dirname(dirname(this_dir)) + package_tests = loader.discover(start_dir=this_dir, pattern='test*.py', + top_level_dir=top_dir) + standard_tests.addTests(package_tests) + return standard_tests diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/htest.py b/contrib/tools/python/src/Lib/idlelib/idle_test/htest.py new file mode 100644 index 00000000000..9e2ddd2c19e --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/htest.py @@ -0,0 +1,403 @@ +'''Run human tests of Idle's window, dialog, and popup widgets. + +run(*tests) +Create a master Tk window. Within that, run each callable in tests +after finding the matching test spec in this file. If tests is empty, +run an htest for each spec dict in this file after finding the matching +callable in the module named in the spec. Close the window to skip or +end the test. + +In a tested module, let X be a global name bound to a callable (class +or function) whose .__name__ attrubute is also X (the usual situation). +The first parameter of X must be 'parent'. When called, the parent +argument will be the root window. X must create a child Toplevel +window (or subclass thereof). The Toplevel may be a test widget or +dialog, in which case the callable is the corresonding class. Or the +Toplevel may contain the widget to be tested or set up a context in +which a test widget is invoked. In this latter case, the callable is a +wrapper function that sets up the Toplevel and other objects. Wrapper +function names, such as _editor_window', should start with '_'. + + +End the module with + +if __name__ == '__main__': + <unittest, if there is one> + from idlelib.idle_test.htest import run + run(X) + +To have wrapper functions and test invocation code ignored by coveragepy +reports, put '# htest #' on the def statement header line. + +def _wrapper(parent): # htest # + +Also make sure that the 'if __name__' line matches the above. Then have +make sure that .coveragerc includes the following. + +[report] +exclude_lines = + .*# htest # + if __name__ == .__main__.: + +(The "." instead of "'" is intentional and necessary.) + + +To run any X, this file must contain a matching instance of the +following template, with X.__name__ prepended to '_spec'. +When all tests are run, the prefix is use to get X. + +_spec = { + 'file': '', + 'kwds': {'title': ''}, + 'msg': "" + } + +file (no .py): run() imports file.py. +kwds: augmented with {'parent':root} and passed to X as **kwds. +title: an example kwd; some widgets need this, delete if not. +msg: master window hints about testing the widget. + + +Modules and classes not being tested at the moment: +PyShell.PyShellEditorWindow +Debugger.Debugger +AutoCompleteWindow.AutoCompleteWindow +OutputWindow.OutputWindow (indirectly being tested with grep test) +''' + +from importlib import import_module +from idlelib.macosxSupport import _initializeTkVariantTests +import Tkinter as tk + +AboutDialog_spec = { + 'file': 'aboutDialog', + 'kwds': {'title': 'aboutDialog test', + '_htest': True, + }, + 'msg': "Test every button. Ensure Python, TK and IDLE versions " + "are correctly displayed.\n [Close] to exit.", + } + +_calltip_window_spec = { + 'file': 'CallTipWindow', + 'kwds': {}, + 'msg': "Typing '(' should display a calltip.\n" + "Typing ') should hide the calltip.\n" + } + +_class_browser_spec = { + 'file': 'ClassBrowser', + 'kwds': {}, + 'msg': "Inspect names of module, class(with superclass if " + "applicable), methods and functions.\nToggle nested items.\n" + "Double clicking on items prints a traceback for an exception " + "that is ignored." + } + +_color_delegator_spec = { + 'file': 'ColorDelegator', + 'kwds': {}, + 'msg': "The text is sample Python code.\n" + "Ensure components like comments, keywords, builtins,\n" + "string, definitions, and break are correctly colored.\n" + "The default color scheme is in idlelib/config-highlight.def" + } + +ConfigDialog_spec = { + 'file': 'configDialog', + 'kwds': {'title': 'ConfigDialogTest', + '_htest': True,}, + 'msg': "IDLE preferences dialog.\n" + "In the 'Fonts/Tabs' tab, changing font face, should update the " + "font face of the text in the area below it.\nIn the " + "'Highlighting' tab, try different color schemes. Clicking " + "items in the sample program should update the choices above it." + "\nIn the 'Keys', 'General' and 'Extensions' tabs, test settings " + "of interest." + "\n[Ok] to close the dialog.[Apply] to apply the settings and " + "and [Cancel] to revert all changes.\nRe-run the test to ensure " + "changes made have persisted." + } + +# TODO Improve message +_dyn_option_menu_spec = { + 'file': 'dynOptionMenuWidget', + 'kwds': {}, + 'msg': "Select one of the many options in the 'old option set'.\n" + "Click the button to change the option set.\n" + "Select one of the many options in the 'new option set'." + } + +# TODO edit wrapper +_editor_window_spec = { + 'file': 'EditorWindow', + 'kwds': {}, + 'msg': "Test editor functions of interest.\n" + "Best to close editor first." + } + +GetCfgSectionNameDialog_spec = { + 'file': 'configSectionNameDialog', + 'kwds': {'title':'Get Name', + 'message':'Enter something', + 'used_names': {'abc'}, + '_htest': True}, + 'msg': "After the text entered with [Ok] is stripped, <nothing>, " + "'abc', or more that 30 chars are errors.\n" + "Close 'Get Name' with a valid entry (printed to Shell), " + "[Cancel], or [X]", + } + +GetHelpSourceDialog_spec = { + 'file': 'configHelpSourceEdit', + 'kwds': {'title': 'Get helpsource', + '_htest': True}, + 'msg': "Enter menu item name and help file path\n " + "<nothing> and more than 30 chars are invalid menu item names.\n" + "<nothing>, file does not exist are invalid path items.\n" + "Test for incomplete web address for help file path.\n" + "A valid entry will be printed to shell with [0k].\n" + "[Cancel] will print None to shell", + } + +# Update once issue21519 is resolved. +GetKeysDialog_spec = { + 'file': 'keybindingDialog', + 'kwds': {'title': 'Test keybindings', + 'action': 'find-again', + 'currentKeySequences': [''] , + '_htest': True, + }, + 'msg': "Test for different key modifier sequences.\n" + "<nothing> is invalid.\n" + "No modifier key is invalid.\n" + "Shift key with [a-z],[0-9], function key, move key, tab, space " + "is invalid.\nNo validitity checking if advanced key binding " + "entry is used." + } + +_grep_dialog_spec = { + 'file': 'GrepDialog', + 'kwds': {}, + 'msg': "Click the 'Show GrepDialog' button.\n" + "Test the various 'Find-in-files' functions.\n" + "The results should be displayed in a new '*Output*' window.\n" + "'Right-click'->'Goto file/line' anywhere in the search results " + "should open that file \nin a new EditorWindow." + } + +_io_binding_spec = { + 'file': 'IOBinding', + 'kwds': {}, + 'msg': "Test the following bindings.\n" + "<Control-o> to open file from dialog.\n" + "Edit the file.\n" + "<Control-p> to print the file.\n" + "<Control-s> to save the file.\n" + "<Alt-s> to save-as another file.\n" + "<Control-c> to save-copy-as another file.\n" + "Check that changes were saved by opening the file elsewhere." + } + +_multi_call_spec = { + 'file': 'MultiCall', + 'kwds': {}, + 'msg': "The following actions should trigger a print to console or IDLE" + " Shell.\nEntering and leaving the text area, key entry, " + "<Control-Key>,\n<Alt-Key-a>, <Control-Key-a>, " + "<Alt-Control-Key-a>, \n<Control-Button-1>, <Alt-Button-1> and " + "focusing out of the window\nare sequences to be tested." + } + +_multistatus_bar_spec = { + 'file': 'MultiStatusBar', + 'kwds': {}, + 'msg': "Ensure presence of multi-status bar below text area.\n" + "Click 'Update Status' to change the multi-status text" + } + +_object_browser_spec = { + 'file': 'ObjectBrowser', + 'kwds': {}, + 'msg': "Double click on items upto the lowest level.\n" + "Attributes of the objects and related information " + "will be displayed side-by-side at each level." + } + +_path_browser_spec = { + 'file': 'PathBrowser', + 'kwds': {}, + 'msg': "Test for correct display of all paths in sys.path.\n" + "Toggle nested items upto the lowest level.\n" + "Double clicking on an item prints a traceback\n" + "for an exception that is ignored." + } + +_percolator_spec = { + 'file': 'Percolator', + 'kwds': {}, + 'msg': "There are two tracers which can be toggled using a checkbox.\n" + "Toggling a tracer 'on' by checking it should print tracer " + "output to the console or to the IDLE shell.\n" + "If both the tracers are 'on', the output from the tracer which " + "was switched 'on' later, should be printed first\n" + "Test for actions like text entry, and removal." + } + +_replace_dialog_spec = { + 'file': 'ReplaceDialog', + 'kwds': {}, + 'msg': "Click the 'Replace' button.\n" + "Test various replace options in the 'Replace dialog'.\n" + "Click [Close] or [X] to close the 'Replace Dialog'." + } + +_search_dialog_spec = { + 'file': 'SearchDialog', + 'kwds': {}, + 'msg': "Click the 'Search' button.\n" + "Test various search options in the 'Search dialog'.\n" + "Click [Close] or [X] to close the 'Search Dialog'." + } + +_scrolled_list_spec = { + 'file': 'ScrolledList', + 'kwds': {}, + 'msg': "You should see a scrollable list of items\n" + "Selecting (clicking) or double clicking an item " + "prints the name to the console or Idle shell.\n" + "Right clicking an item will display a popup." + } + +show_idlehelp_spec = { + 'file': 'help', + 'kwds': {}, + 'msg': "If the help text displays, this works.\n" + "Text is selectable. Window is scrollable." + } + +_stack_viewer_spec = { + 'file': 'StackViewer', + 'kwds': {}, + 'msg': "A stacktrace for a NameError exception.\n" + "Expand 'idlelib ...' and '<locals>'.\n" + "Check that exc_value, exc_tb, and exc_type are correct.\n" + } + +_tabbed_pages_spec = { + 'file': 'tabbedpages', + 'kwds': {}, + 'msg': "Toggle between the two tabs 'foo' and 'bar'\n" + "Add a tab by entering a suitable name for it.\n" + "Remove an existing tab by entering its name.\n" + "Remove all existing tabs.\n" + "<nothing> is an invalid add page and remove page name.\n" + } + +TextViewer_spec = { + 'file': 'textView', + 'kwds': {'title': 'Test textView', + 'text':'The quick brown fox jumps over the lazy dog.\n'*35, + '_htest': True}, + 'msg': "Test for read-only property of text.\n" + "Text is selectable. Window is scrollable.", + } + +_tooltip_spec = { + 'file': 'ToolTip', + 'kwds': {}, + 'msg': "Place mouse cursor over both the buttons\n" + "A tooltip should appear with some text." + } + +_tree_widget_spec = { + 'file': 'TreeWidget', + 'kwds': {}, + 'msg': "The canvas is scrollable.\n" + "Click on folders upto to the lowest level." + } + +_undo_delegator_spec = { + 'file': 'UndoDelegator', + 'kwds': {}, + 'msg': "Click [Undo] to undo any action.\n" + "Click [Redo] to redo any action.\n" + "Click [Dump] to dump the current state " + "by printing to the console or the IDLE shell.\n" + } + +_widget_redirector_spec = { + 'file': 'WidgetRedirector', + 'kwds': {}, + 'msg': "Every text insert should be printed to the console " + "or the IDLE shell." + } + +def run(*tests): + root = tk.Tk() + root.title('IDLE htest') + root.resizable(0, 0) + _initializeTkVariantTests(root) + + # a scrollable Label like constant width text widget. + frameLabel = tk.Frame(root, padx=10) + frameLabel.pack() + text = tk.Text(frameLabel, wrap='word') + text.configure(bg=root.cget('bg'), relief='flat', height=4, width=70) + scrollbar = tk.Scrollbar(frameLabel, command=text.yview) + text.config(yscrollcommand=scrollbar.set) + scrollbar.pack(side='right', fill='y', expand=False) + text.pack(side='left', fill='both', expand=True) + + test_list = [] # List of tuples of the form (spec, callable widget) + if tests: + for test in tests: + test_spec = globals()[test.__name__ + '_spec'] + test_spec['name'] = test.__name__ + test_list.append((test_spec, test)) + else: + for k, d in globals().items(): + if k.endswith('_spec'): + test_name = k[:-5] + test_spec = d + test_spec['name'] = test_name + mod = import_module('idlelib.' + test_spec['file']) + test = getattr(mod, test_name) + test_list.append((test_spec, test)) + + test_name = [tk.StringVar('')] + callable_object = [None] + test_kwds = [None] + + + def next(): + if len(test_list) == 1: + next_button.pack_forget() + test_spec, callable_object[0] = test_list.pop() + test_kwds[0] = test_spec['kwds'] + test_kwds[0]['parent'] = root + test_name[0].set('Test ' + test_spec['name']) + + text.configure(state='normal') # enable text editing + text.delete('1.0','end') + text.insert("1.0",test_spec['msg']) + text.configure(state='disabled') # preserve read-only property + + def run_test(): + widget = callable_object[0](**test_kwds[0]) + try: + print(widget.result) + except AttributeError: + pass + + button = tk.Button(root, textvariable=test_name[0], command=run_test) + button.pack() + next_button = tk.Button(root, text="Next", command=next) + next_button.pack() + + next() + + root.mainloop() + +if __name__ == '__main__': + run() diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/mock_idle.py b/contrib/tools/python/src/Lib/idlelib/idle_test/mock_idle.py new file mode 100644 index 00000000000..7b09f836f03 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/mock_idle.py @@ -0,0 +1,55 @@ +'''Mock classes that imitate idlelib modules or classes. + +Attributes and methods will be added as needed for tests. +''' + +from idlelib.idle_test.mock_tk import Text + +class Func(object): + '''Mock function captures args and returns result set by test. + + Attributes: + self.called - records call even if no args, kwds passed. + self.result - set by init, returned by call. + self.args - captures positional arguments. + self.kwds - captures keyword arguments. + + Most common use will probably be to mock methods. + Mock_tk.Var and Mbox_func are special variants of this. + ''' + def __init__(self, result=None): + self.called = False + self.result = result + self.args = None + self.kwds = None + def __call__(self, *args, **kwds): + self.called = True + self.args = args + self.kwds = kwds + if isinstance(self.result, BaseException): + raise self.result + else: + return self.result + + +class Editor(object): + '''Minimally imitate EditorWindow.EditorWindow class. + ''' + def __init__(self, flist=None, filename=None, key=None, root=None): + self.text = Text() + self.undo = UndoDelegator() + + def get_selection_indices(self): + first = self.text.index('1.0') + last = self.text.index('end') + return first, last + + +class UndoDelegator(object): + '''Minimally imitate UndoDelegator,UndoDelegator class. + ''' + # A real undo block is only needed for user interaction. + def undo_block_start(*args): + pass + def undo_block_stop(*args): + pass diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/mock_tk.py b/contrib/tools/python/src/Lib/idlelib/idle_test/mock_tk.py new file mode 100644 index 00000000000..56ca87695af --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/mock_tk.py @@ -0,0 +1,298 @@ +"""Classes that replace tkinter gui objects used by an object being tested. + +A gui object is anything with a master or parent parameter, which is +typically required in spite of what the doc strings say. +""" + +class Event(object): + '''Minimal mock with attributes for testing event handlers. + + This is not a gui object, but is used as an argument for callbacks + that access attributes of the event passed. If a callback ignores + the event, other than the fact that is happened, pass 'event'. + + Keyboard, mouse, window, and other sources generate Event instances. + Event instances have the following attributes: serial (number of + event), time (of event), type (of event as number), widget (in which + event occurred), and x,y (position of mouse). There are other + attributes for specific events, such as keycode for key events. + tkinter.Event.__doc__ has more but is still not complete. + ''' + def __init__(self, **kwds): + "Create event with attributes needed for test" + self.__dict__.update(kwds) + +class Var(object): + "Use for String/Int/BooleanVar: incomplete" + def __init__(self, master=None, value=None, name=None): + self.master = master + self.value = value + self.name = name + def set(self, value): + self.value = value + def get(self): + return self.value + +class Mbox_func(object): + """Generic mock for messagebox functions, which all have the same signature. + + Instead of displaying a message box, the mock's call method saves the + arguments as instance attributes, which test functions can then examime. + The test can set the result returned to ask function + """ + def __init__(self, result=None): + self.result = result # Return None for all show funcs + def __call__(self, title, message, *args, **kwds): + # Save all args for possible examination by tester + self.title = title + self.message = message + self.args = args + self.kwds = kwds + return self.result # Set by tester for ask functions + +class Mbox(object): + """Mock for tkinter.messagebox with an Mbox_func for each function. + + This module was 'tkMessageBox' in 2.x; hence the 'import as' in 3.x. + Example usage in test_module.py for testing functions in module.py: + --- +from idlelib.idle_test.mock_tk import Mbox +import module + +orig_mbox = module.tkMessageBox +showerror = Mbox.showerror # example, for attribute access in test methods + +class Test(unittest.TestCase): + + @classmethod + def setUpClass(cls): + module.tkMessageBox = Mbox + + @classmethod + def tearDownClass(cls): + module.tkMessageBox = orig_mbox + --- + For 'ask' functions, set func.result return value before calling the method + that uses the message function. When tkMessageBox functions are the + only gui alls in a method, this replacement makes the method gui-free, + """ + askokcancel = Mbox_func() # True or False + askquestion = Mbox_func() # 'yes' or 'no' + askretrycancel = Mbox_func() # True or False + askyesno = Mbox_func() # True or False + askyesnocancel = Mbox_func() # True, False, or None + showerror = Mbox_func() # None + showinfo = Mbox_func() # None + showwarning = Mbox_func() # None + +from _tkinter import TclError + +class Text(object): + """A semi-functional non-gui replacement for tkinter.Text text editors. + + The mock's data model is that a text is a list of \n-terminated lines. + The mock adds an empty string at the beginning of the list so that the + index of actual lines start at 1, as with Tk. The methods never see this. + Tk initializes files with a terminal \n that cannot be deleted. It is + invisible in the sense that one cannot move the cursor beyond it. + + This class is only tested (and valid) with strings of ascii chars. + For testing, we are not concerned with Tk Text's treatment of, + for instance, 0-width characters or character + accent. + """ + def __init__(self, master=None, cnf={}, **kw): + '''Initialize mock, non-gui, text-only Text widget. + + At present, all args are ignored. Almost all affect visual behavior. + There are just a few Text-only options that affect text behavior. + ''' + self.data = ['', '\n'] + + def index(self, index): + "Return string version of index decoded according to current text." + return "%s.%s" % self._decode(index, endflag=1) + + def _decode(self, index, endflag=0): + """Return a (line, char) tuple of int indexes into self.data. + + This implements .index without converting the result back to a string. + The result is contrained by the number of lines and linelengths of + self.data. For many indexes, the result is initially (1, 0). + + The input index may have any of several possible forms: + * line.char float: converted to 'line.char' string; + * 'line.char' string, where line and char are decimal integers; + * 'line.char lineend', where lineend='lineend' (and char is ignored); + * 'line.end', where end='end' (same as above); + * 'insert', the positions before terminal \n; + * 'end', whose meaning depends on the endflag passed to ._endex. + * 'sel.first' or 'sel.last', where sel is a tag -- not implemented. + """ + if isinstance(index, (float, bytes)): + index = str(index) + try: + index=index.lower() + except AttributeError: + raise TclError('bad text index "%s"' % index) + + lastline = len(self.data) - 1 # same as number of text lines + if index == 'insert': + return lastline, len(self.data[lastline]) - 1 + elif index == 'end': + return self._endex(endflag) + + line, char = index.split('.') + line = int(line) + + # Out of bounds line becomes first or last ('end') index + if line < 1: + return 1, 0 + elif line > lastline: + return self._endex(endflag) + + linelength = len(self.data[line]) -1 # position before/at \n + if char.endswith(' lineend') or char == 'end': + return line, linelength + # Tk requires that ignored chars before ' lineend' be valid int + + # Out of bounds char becomes first or last index of line + char = int(char) + if char < 0: + char = 0 + elif char > linelength: + char = linelength + return line, char + + def _endex(self, endflag): + '''Return position for 'end' or line overflow corresponding to endflag. + + -1: position before terminal \n; for .insert(), .delete + 0: position after terminal \n; for .get, .delete index 1 + 1: same viewed as beginning of non-existent next line (for .index) + ''' + n = len(self.data) + if endflag == 1: + return n, 0 + else: + n -= 1 + return n, len(self.data[n]) + endflag + + + def insert(self, index, chars): + "Insert chars before the character at index." + + if not chars: # ''.splitlines() is [], not [''] + return + chars = chars.splitlines(True) + if chars[-1][-1] == '\n': + chars.append('') + line, char = self._decode(index, -1) + before = self.data[line][:char] + after = self.data[line][char:] + self.data[line] = before + chars[0] + self.data[line+1:line+1] = chars[1:] + self.data[line+len(chars)-1] += after + + + def get(self, index1, index2=None): + "Return slice from index1 to index2 (default is 'index1+1')." + + startline, startchar = self._decode(index1) + if index2 is None: + endline, endchar = startline, startchar+1 + else: + endline, endchar = self._decode(index2) + + if startline == endline: + return self.data[startline][startchar:endchar] + else: + lines = [self.data[startline][startchar:]] + for i in range(startline+1, endline): + lines.append(self.data[i]) + lines.append(self.data[endline][:endchar]) + return ''.join(lines) + + + def delete(self, index1, index2=None): + '''Delete slice from index1 to index2 (default is 'index1+1'). + + Adjust default index2 ('index+1) for line ends. + Do not delete the terminal \n at the very end of self.data ([-1][-1]). + ''' + startline, startchar = self._decode(index1, -1) + if index2 is None: + if startchar < len(self.data[startline])-1: + # not deleting \n + endline, endchar = startline, startchar+1 + elif startline < len(self.data) - 1: + # deleting non-terminal \n, convert 'index1+1 to start of next line + endline, endchar = startline+1, 0 + else: + # do not delete terminal \n if index1 == 'insert' + return + else: + endline, endchar = self._decode(index2, -1) + # restricting end position to insert position excludes terminal \n + + if startline == endline and startchar < endchar: + self.data[startline] = self.data[startline][:startchar] + \ + self.data[startline][endchar:] + elif startline < endline: + self.data[startline] = self.data[startline][:startchar] + \ + self.data[endline][endchar:] + startline += 1 + for i in range(startline, endline+1): + del self.data[startline] + + def compare(self, index1, op, index2): + line1, char1 = self._decode(index1) + line2, char2 = self._decode(index2) + if op == '<': + return line1 < line2 or line1 == line2 and char1 < char2 + elif op == '<=': + return line1 < line2 or line1 == line2 and char1 <= char2 + elif op == '>': + return line1 > line2 or line1 == line2 and char1 > char2 + elif op == '>=': + return line1 > line2 or line1 == line2 and char1 >= char2 + elif op == '==': + return line1 == line2 and char1 == char2 + elif op == '!=': + return line1 != line2 or char1 != char2 + else: + raise TclError('''bad comparison operator "%s": ''' + '''must be <, <=, ==, >=, >, or !=''' % op) + + # The following Text methods normally do something and return None. + # Whether doing nothing is sufficient for a test will depend on the test. + + def mark_set(self, name, index): + "Set mark *name* before the character at index." + pass + + def mark_unset(self, *markNames): + "Delete all marks in markNames." + + def tag_remove(self, tagName, index1, index2=None): + "Remove tag tagName from all characters between index1 and index2." + pass + + # The following Text methods affect the graphics screen and return None. + # Doing nothing should always be sufficient for tests. + + def scan_dragto(self, x, y): + "Adjust the view of the text according to scan_mark" + + def scan_mark(self, x, y): + "Remember the current X, Y coordinates." + + def see(self, index): + "Scroll screen to make the character at INDEX is visible." + pass + + # The following is a Misc method inherited by Text. + # It should properly go in a Misc mock, but is included here for now. + + def bind(sequence=None, func=None, add=None): + "Bind to this widget at event sequence a call to function func." + pass diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/test_autocomplete.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_autocomplete.py new file mode 100644 index 00000000000..002751efcc6 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_autocomplete.py @@ -0,0 +1,140 @@ +import unittest +from test.test_support import requires +from Tkinter import Tk, Text + +import idlelib.AutoComplete as ac +import idlelib.AutoCompleteWindow as acw +from idlelib.idle_test.mock_idle import Func +from idlelib.idle_test.mock_tk import Event + +class AutoCompleteWindow: + def complete(): + return + +class DummyEditwin: + def __init__(self, root, text): + self.root = root + self.text = text + self.indentwidth = 8 + self.tabwidth = 8 + self.context_use_ps1 = True + + +class AutoCompleteTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + cls.text = Text(cls.root) + cls.editor = DummyEditwin(cls.root, cls.text) + + @classmethod + def tearDownClass(cls): + del cls.editor, cls.text + cls.root.destroy() + del cls.root + + def setUp(self): + self.editor.text.delete('1.0', 'end') + self.autocomplete = ac.AutoComplete(self.editor) + + def test_init(self): + self.assertEqual(self.autocomplete.editwin, self.editor) + + def test_make_autocomplete_window(self): + testwin = self.autocomplete._make_autocomplete_window() + self.assertIsInstance(testwin, acw.AutoCompleteWindow) + + def test_remove_autocomplete_window(self): + self.autocomplete.autocompletewindow = ( + self.autocomplete._make_autocomplete_window()) + self.autocomplete._remove_autocomplete_window() + self.assertIsNone(self.autocomplete.autocompletewindow) + + def test_force_open_completions_event(self): + # Test that force_open_completions_event calls _open_completions + o_cs = Func() + self.autocomplete.open_completions = o_cs + self.autocomplete.force_open_completions_event('event') + self.assertEqual(o_cs.args, (True, False, True)) + + def test_try_open_completions_event(self): + Equal = self.assertEqual + autocomplete = self.autocomplete + trycompletions = self.autocomplete.try_open_completions_event + o_c_l = Func() + autocomplete._open_completions_later = o_c_l + + # _open_completions_later should not be called with no text in editor + trycompletions('event') + Equal(o_c_l.args, None) + + # _open_completions_later should be called with COMPLETE_ATTRIBUTES (1) + self.text.insert('1.0', 're.') + trycompletions('event') + Equal(o_c_l.args, (False, False, False, 1)) + + # _open_completions_later should be called with COMPLETE_FILES (2) + self.text.delete('1.0', 'end') + self.text.insert('1.0', '"./Lib/') + trycompletions('event') + Equal(o_c_l.args, (False, False, False, 2)) + + def test_autocomplete_event(self): + Equal = self.assertEqual + autocomplete = self.autocomplete + + # Test that the autocomplete event is ignored if user is pressing a + # modifier key in addition to the tab key + ev = Event(mc_state=True) + self.assertIsNone(autocomplete.autocomplete_event(ev)) + del ev.mc_state + + # If autocomplete window is open, complete() method is called + self.text.insert('1.0', 're.') + # This must call autocomplete._make_autocomplete_window() + Equal(self.autocomplete.autocomplete_event(ev), 'break') + + # If autocomplete window is not active or does not exist, + # open_completions is called. Return depends on its return. + autocomplete._remove_autocomplete_window() + o_cs = Func() # .result = None + autocomplete.open_completions = o_cs + Equal(self.autocomplete.autocomplete_event(ev), None) + Equal(o_cs.args, (False, True, True)) + o_cs.result = True + Equal(self.autocomplete.autocomplete_event(ev), 'break') + Equal(o_cs.args, (False, True, True)) + + def test_open_completions_later(self): + # Test that autocomplete._delayed_completion_id is set + pass + + def test_delayed_open_completions(self): + # Test that autocomplete._delayed_completion_id set to None and that + # open_completions only called if insertion index is the same as + # _delayed_completion_index + pass + + def test_open_completions(self): + # Test completions of files and attributes as well as non-completion + # of errors + pass + + def test_fetch_completions(self): + # Test that fetch_completions returns 2 lists: + # For attribute completion, a large list containing all variables, and + # a small list containing non-private variables. + # For file completion, a large list containing all files in the path, + # and a small list containing files that do not start with '.' + pass + + def test_get_entity(self): + # Test that a name is in the namespace of sys.modules and + # __main__.__dict__ + pass + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/test_autoexpand.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_autoexpand.py new file mode 100644 index 00000000000..6be4fbf861b --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_autoexpand.py @@ -0,0 +1,141 @@ +"""Unit tests for idlelib.AutoExpand""" +import unittest +from test.test_support import requires +from Tkinter import Text, Tk +#from idlelib.idle_test.mock_tk import Text +from idlelib.AutoExpand import AutoExpand + + +class Dummy_Editwin: + # AutoExpand.__init__ only needs .text + def __init__(self, text): + self.text = text + +class AutoExpandTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + if 'Tkinter' in str(Text): + requires('gui') + cls.tk = Tk() + cls.text = Text(cls.tk) + else: + cls.text = Text() + cls.auto_expand = AutoExpand(Dummy_Editwin(cls.text)) + + @classmethod + def tearDownClass(cls): + del cls.text, cls.auto_expand + if hasattr(cls, 'tk'): + cls.tk.destroy() + del cls.tk + + def tearDown(self): + self.text.delete('1.0', 'end') + + def test_get_prevword(self): + text = self.text + previous = self.auto_expand.getprevword + equal = self.assertEqual + + equal(previous(), '') + + text.insert('insert', 't') + equal(previous(), 't') + + text.insert('insert', 'his') + equal(previous(), 'this') + + text.insert('insert', ' ') + equal(previous(), '') + + text.insert('insert', 'is') + equal(previous(), 'is') + + text.insert('insert', '\nsample\nstring') + equal(previous(), 'string') + + text.delete('3.0', 'insert') + equal(previous(), '') + + text.delete('1.0', 'end') + equal(previous(), '') + + def test_before_only(self): + previous = self.auto_expand.getprevword + expand = self.auto_expand.expand_word_event + equal = self.assertEqual + + self.text.insert('insert', 'ab ac bx ad ab a') + equal(self.auto_expand.getwords(), ['ab', 'ad', 'ac', 'a']) + expand('event') + equal(previous(), 'ab') + expand('event') + equal(previous(), 'ad') + expand('event') + equal(previous(), 'ac') + expand('event') + equal(previous(), 'a') + + def test_after_only(self): + # Also add punctuation 'noise' that shoud be ignored. + text = self.text + previous = self.auto_expand.getprevword + expand = self.auto_expand.expand_word_event + equal = self.assertEqual + + text.insert('insert', 'a, [ab] ac: () bx"" cd ac= ad ya') + text.mark_set('insert', '1.1') + equal(self.auto_expand.getwords(), ['ab', 'ac', 'ad', 'a']) + expand('event') + equal(previous(), 'ab') + expand('event') + equal(previous(), 'ac') + expand('event') + equal(previous(), 'ad') + expand('event') + equal(previous(), 'a') + + def test_both_before_after(self): + text = self.text + previous = self.auto_expand.getprevword + expand = self.auto_expand.expand_word_event + equal = self.assertEqual + + text.insert('insert', 'ab xy yz\n') + text.insert('insert', 'a ac by ac') + + text.mark_set('insert', '2.1') + equal(self.auto_expand.getwords(), ['ab', 'ac', 'a']) + expand('event') + equal(previous(), 'ab') + expand('event') + equal(previous(), 'ac') + expand('event') + equal(previous(), 'a') + + def test_other_expand_cases(self): + text = self.text + expand = self.auto_expand.expand_word_event + equal = self.assertEqual + + # no expansion candidate found + equal(self.auto_expand.getwords(), []) + equal(expand('event'), 'break') + + text.insert('insert', 'bx cy dz a') + equal(self.auto_expand.getwords(), []) + + # reset state by successfully expanding once + # move cursor to another position and expand again + text.insert('insert', 'ac xy a ac ad a') + text.mark_set('insert', '1.7') + expand('event') + initial_state = self.auto_expand.state + text.mark_set('insert', '1.end') + expand('event') + new_state = self.auto_expand.state + self.assertNotEqual(initial_state, new_state) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/test_calltips.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_calltips.py new file mode 100644 index 00000000000..147119ce377 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_calltips.py @@ -0,0 +1,185 @@ +import unittest +import idlelib.CallTips as ct +CTi = ct.CallTips() # needed for get_entity test in 2.7 +import textwrap +import types +import warnings + +default_tip = '' + +# Test Class TC is used in multiple get_argspec test methods +class TC(object): + 'doc' + tip = "(ai=None, *args)" + def __init__(self, ai=None, *b): 'doc' + __init__.tip = "(self, ai=None, *args)" + def t1(self): 'doc' + t1.tip = "(self)" + def t2(self, ai, b=None): 'doc' + t2.tip = "(self, ai, b=None)" + def t3(self, ai, *args): 'doc' + t3.tip = "(self, ai, *args)" + def t4(self, *args): 'doc' + t4.tip = "(self, *args)" + def t5(self, ai, b=None, *args, **kw): 'doc' + t5.tip = "(self, ai, b=None, *args, **kwargs)" + def t6(no, self): 'doc' + t6.tip = "(no, self)" + def __call__(self, ci): 'doc' + __call__.tip = "(self, ci)" + # attaching .tip to wrapped methods does not work + @classmethod + def cm(cls, a): 'doc' + @staticmethod + def sm(b): 'doc' + +tc = TC() + +signature = ct.get_arg_text # 2.7 and 3.x use different functions +class Get_signatureTest(unittest.TestCase): + # The signature function must return a string, even if blank. + # Test a variety of objects to be sure that none cause it to raise + # (quite aside from getting as correct an answer as possible). + # The tests of builtins may break if the docstrings change, + # but a red buildbot is better than a user crash (as has happened). + # For a simple mismatch, change the expected output to the actual. + + def test_builtins(self): + # 2.7 puts '()\n' where 3.x does not, other minor differences + + # Python class that inherits builtin methods + class List(list): "List() doc" + # Simulate builtin with no docstring for default argspec test + class SB: __call__ = None + + def gtest(obj, out): + self.assertEqual(signature(obj), out) + + if List.__doc__ is not None: + gtest(List, '()\n' + List.__doc__) + gtest(list.__new__, + 'T.__new__(S, ...) -> a new object with type S, a subtype of T') + gtest(list.__init__, + 'x.__init__(...) initializes x; see help(type(x)) for signature') + append_doc = "L.append(object) -- append object to end" + gtest(list.append, append_doc) + gtest([].append, append_doc) + gtest(List.append, append_doc) + + gtest(types.MethodType, '()\ninstancemethod(function, instance, class)') + gtest(SB(), default_tip) + + def test_signature_wrap(self): + # This is also a test of an old-style class + if textwrap.TextWrapper.__doc__ is not None: + self.assertEqual(signature(textwrap.TextWrapper), '''\ +(width=70, initial_indent='', subsequent_indent='', expand_tabs=True, + replace_whitespace=True, fix_sentence_endings=False, break_long_words=True, + drop_whitespace=True, break_on_hyphens=True)''') + + def test_docline_truncation(self): + def f(): pass + f.__doc__ = 'a'*300 + self.assertEqual(signature(f), '()\n' + 'a' * (ct._MAX_COLS-3) + '...') + + def test_multiline_docstring(self): + # Test fewer lines than max. + self.assertEqual(signature(list), + "()\nlist() -> new empty list\n" + "list(iterable) -> new list initialized from iterable's items") + + # Test max lines and line (currently) too long. + def f(): + pass + s = 'a\nb\nc\nd\n' + f.__doc__ = s + 300 * 'e' + 'f' + self.assertEqual(signature(f), + '()\n' + s + (ct._MAX_COLS - 3) * 'e' + '...') + + def test_functions(self): + def t1(): 'doc' + t1.tip = "()" + def t2(a, b=None): 'doc' + t2.tip = "(a, b=None)" + def t3(a, *args): 'doc' + t3.tip = "(a, *args)" + def t4(*args): 'doc' + t4.tip = "(*args)" + def t5(a, b=None, *args, **kwds): 'doc' + t5.tip = "(a, b=None, *args, **kwargs)" + + doc = '\ndoc' if t1.__doc__ is not None else '' + for func in (t1, t2, t3, t4, t5, TC): + self.assertEqual(signature(func), func.tip + doc) + + def test_methods(self): + doc = '\ndoc' if TC.__doc__ is not None else '' + for meth in (TC.t1, TC.t2, TC.t3, TC.t4, TC.t5, TC.t6, TC.__call__): + self.assertEqual(signature(meth), meth.tip + doc) + self.assertEqual(signature(TC.cm), "(a)" + doc) + self.assertEqual(signature(TC.sm), "(b)" + doc) + + def test_bound_methods(self): + # test that first parameter is correctly removed from argspec + doc = '\ndoc' if TC.__doc__ is not None else '' + for meth, mtip in ((tc.t1, "()"), (tc.t4, "(*args)"), (tc.t6, "(self)"), + (tc.__call__, '(ci)'), (tc, '(ci)'), (TC.cm, "(a)"),): + self.assertEqual(signature(meth), mtip + doc) + + def test_starred_parameter(self): + # test that starred first parameter is *not* removed from argspec + class C: + def m1(*args): pass + def m2(**kwds): pass + def f1(args, kwargs, *a, **k): pass + def f2(args, kwargs, args1, kwargs1, *a, **k): pass + c = C() + self.assertEqual(signature(C.m1), '(*args)') + self.assertEqual(signature(c.m1), '(*args)') + self.assertEqual(signature(C.m2), '(**kwargs)') + self.assertEqual(signature(c.m2), '(**kwargs)') + self.assertEqual(signature(f1), '(args, kwargs, *args1, **kwargs1)') + self.assertEqual(signature(f2), + '(args, kwargs, args1, kwargs1, *args2, **kwargs2)') + + def test_no_docstring(self): + def nd(s): pass + TC.nd = nd + self.assertEqual(signature(nd), "(s)") + self.assertEqual(signature(TC.nd), "(s)") + self.assertEqual(signature(tc.nd), "()") + + def test_attribute_exception(self): + class NoCall(object): + def __getattr__(self, name): + raise BaseException + class Call(NoCall): + def __call__(self, ci): + pass + for meth, mtip in ((NoCall, '()'), (Call, '()'), + (NoCall(), ''), (Call(), '(ci)')): + self.assertEqual(signature(meth), mtip) + + def test_non_callables(self): + for obj in (0, 0.0, '0', b'0', [], {}): + self.assertEqual(signature(obj), '') + +class Get_entityTest(unittest.TestCase): + # In 3.x, get_entity changed from 'instance method' to module function + # since 'self' not used. Use dummy instance until change 2.7 also. + def test_bad_entity(self): + self.assertIsNone(CTi.get_entity('1//0')) + def test_good_entity(self): + self.assertIs(CTi.get_entity('int'), int) + +class Py2Test(unittest.TestCase): + def test_paramtuple_float(self): + # 18539: (a,b) becomes '.0' in code object; change that but not 0.0 + with warnings.catch_warnings(): + # Suppess message of py3 deprecation of parameter unpacking + warnings.simplefilter("ignore") + exec "def f((a,b), c=0.0): pass" + self.assertEqual(signature(f), '(<tuple>, c=0.0)') + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=False) diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/test_config_name.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_config_name.py new file mode 100644 index 00000000000..2a4df6a7445 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_config_name.py @@ -0,0 +1,77 @@ +"""Unit tests for idlelib.configSectionNameDialog""" +import unittest +from idlelib.idle_test.mock_tk import Var, Mbox +from idlelib import configSectionNameDialog as name_dialog_module + +name_dialog = name_dialog_module.GetCfgSectionNameDialog + +class Dummy_name_dialog(object): + # Mock for testing the following methods of name_dialog + name_ok = name_dialog.name_ok.im_func + Ok = name_dialog.Ok.im_func + Cancel = name_dialog.Cancel.im_func + # Attributes, constant or variable, needed for tests + used_names = ['used'] + name = Var() + result = None + destroyed = False + def grab_release(self): + pass + def destroy(self): + self.destroyed = True + +# name_ok calls Mbox.showerror if name is not ok +orig_mbox = name_dialog_module.tkMessageBox +showerror = Mbox.showerror + +class ConfigNameTest(unittest.TestCase): + dialog = Dummy_name_dialog() + + @classmethod + def setUpClass(cls): + name_dialog_module.tkMessageBox = Mbox + + @classmethod + def tearDownClass(cls): + name_dialog_module.tkMessageBox = orig_mbox + + def test_blank_name(self): + self.dialog.name.set(' ') + self.assertEqual(self.dialog.name_ok(), '') + self.assertEqual(showerror.title, 'Name Error') + self.assertIn('No', showerror.message) + + def test_used_name(self): + self.dialog.name.set('used') + self.assertEqual(self.dialog.name_ok(), '') + self.assertEqual(showerror.title, 'Name Error') + self.assertIn('use', showerror.message) + + def test_long_name(self): + self.dialog.name.set('good'*8) + self.assertEqual(self.dialog.name_ok(), '') + self.assertEqual(showerror.title, 'Name Error') + self.assertIn('too long', showerror.message) + + def test_good_name(self): + self.dialog.name.set(' good ') + showerror.title = 'No Error' # should not be called + self.assertEqual(self.dialog.name_ok(), 'good') + self.assertEqual(showerror.title, 'No Error') + + def test_ok(self): + self.dialog.destroyed = False + self.dialog.name.set('good') + self.dialog.Ok() + self.assertEqual(self.dialog.result, 'good') + self.assertTrue(self.dialog.destroyed) + + def test_cancel(self): + self.dialog.destroyed = False + self.dialog.Cancel() + self.assertEqual(self.dialog.result, '') + self.assertTrue(self.dialog.destroyed) + + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=False) diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/test_configdialog.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_configdialog.py new file mode 100644 index 00000000000..ba651005f0b --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_configdialog.py @@ -0,0 +1,33 @@ +'''Unittests for idlelib/configHandler.py + +Coverage: 46% just by creating dialog. The other half is change code. + +''' +import unittest +from test.test_support import requires +from Tkinter import Tk +from idlelib.configDialog import ConfigDialog +from idlelib.macosxSupport import _initializeTkVariantTests + + +class ConfigDialogTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + cls.root.withdraw() + _initializeTkVariantTests(cls.root) + + @classmethod + def tearDownClass(cls): + cls.root.destroy() + del cls.root + + def test_dialog(self): + d = ConfigDialog(self.root, 'Test', _utest=True) + d.remove_var_callbacks() + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/test_delegator.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_delegator.py new file mode 100644 index 00000000000..b8ae5eeefe3 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_delegator.py @@ -0,0 +1,37 @@ +import unittest +from idlelib.Delegator import Delegator + +class DelegatorTest(unittest.TestCase): + + def test_mydel(self): + # test a simple use scenario + + # initialize + mydel = Delegator(int) + self.assertIs(mydel.delegate, int) + self.assertEqual(mydel._Delegator__cache, set()) + + # add an attribute: + self.assertRaises(AttributeError, mydel.__getattr__, 'xyz') + bl = mydel.bit_length + self.assertIs(bl, int.bit_length) + self.assertIs(mydel.__dict__['bit_length'], int.bit_length) + self.assertEqual(mydel._Delegator__cache, {'bit_length'}) + + # add a second attribute + mydel.numerator + self.assertEqual(mydel._Delegator__cache, {'bit_length', 'numerator'}) + + # delete the second (which, however, leaves it in the name cache) + del mydel.numerator + self.assertNotIn('numerator', mydel.__dict__) + self.assertIn('numerator', mydel._Delegator__cache) + + # reset by calling .setdelegate, which calls .resetcache + mydel.setdelegate(float) + self.assertIs(mydel.delegate, float) + self.assertNotIn('bit_length', mydel.__dict__) + self.assertEqual(mydel._Delegator__cache, set()) + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=2) diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/test_formatparagraph.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_formatparagraph.py new file mode 100644 index 00000000000..068ae381c33 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_formatparagraph.py @@ -0,0 +1,376 @@ +# 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.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_paragraph(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 <class '_tkinter.TclError'> +# : 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.im_func + +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): + del cls.text, cls.formatter + cls.root.destroy() + del cls.root + + 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/contrib/tools/python/src/Lib/idlelib/idle_test/test_grep.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_grep.py new file mode 100644 index 00000000000..e9f4f22ae65 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_grep.py @@ -0,0 +1,82 @@ +""" !Changing this line will break Test_findfile.test_found! +Non-gui unit tests for idlelib.GrepDialog methods. +dummy_command calls grep_it calls findfiles. +An exception raised in one method will fail callers. +Otherwise, tests are mostly independent. +*** Currently only test grep_it. +""" +import unittest +from test.test_support import captured_stdout, findfile +from idlelib.idle_test.mock_tk import Var +from idlelib.GrepDialog import GrepDialog +import re + +__file__ = findfile('idlelib/idle_test') + '/test_grep.py' + +class Dummy_searchengine: + '''GrepDialog.__init__ calls parent SearchDiabolBase which attaches the + passed in SearchEngine instance as attribute 'engine'. Only a few of the + many possible self.engine.x attributes are needed here. + ''' + def getpat(self): + return self._pat + +searchengine = Dummy_searchengine() + +class Dummy_grep: + # Methods tested + #default_command = GrepDialog.default_command + grep_it = GrepDialog.grep_it.im_func + findfiles = GrepDialog.findfiles.im_func + # Other stuff needed + recvar = Var(False) + engine = searchengine + def close(self): # gui method + pass + +grep = Dummy_grep() + +class FindfilesTest(unittest.TestCase): + # findfiles is really a function, not a method, could be iterator + # test that filename return filename + # test that idlelib has many .py files + # test that recursive flag adds idle_test .py files + pass + +class Grep_itTest(unittest.TestCase): + # Test captured reports with 0 and some hits. + # Should test file names, but Windows reports have mixed / and \ separators + # from incomplete replacement, so 'later'. + + def report(self, pat): + grep.engine._pat = pat + with captured_stdout() as s: + grep.grep_it(re.compile(pat), __file__) + lines = s.getvalue().split('\n') + lines.pop() # remove bogus '' after last \n + return lines + + def test_unfound(self): + pat = 'xyz*'*7 + lines = self.report(pat) + self.assertEqual(len(lines), 2) + self.assertIn(pat, lines[0]) + self.assertEqual(lines[1], 'No hits.') + + def test_found(self): + + pat = '""" !Changing this line will break Test_findfile.test_found!' + lines = self.report(pat) + self.assertEqual(len(lines), 5) + self.assertIn(pat, lines[0]) + self.assertIn('py: 1:', lines[1]) # line number 1 + self.assertIn('2', lines[3]) # hits found 2 + self.assertTrue(lines[4].startswith('(Hint:')) + +class Default_commandTest(unittest.TestCase): + # To write this, mode OutputWindow import to top of GrepDialog + # so it can be replaced by captured_stdout in class setup/teardown. + pass + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=False) diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/test_hyperparser.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_hyperparser.py new file mode 100644 index 00000000000..0a1809d2f70 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_hyperparser.py @@ -0,0 +1,192 @@ +"""Unittest for idlelib.HyperParser""" +import unittest +from test.test_support import requires +from Tkinter import Tk, Text +from idlelib.EditorWindow import EditorWindow +from idlelib.HyperParser import HyperParser + +class DummyEditwin: + def __init__(self, text): + self.text = text + self.indentwidth = 8 + self.tabwidth = 8 + self.context_use_ps1 = True + self.num_context_lines = 50, 500, 1000 + + _build_char_in_string_func = EditorWindow._build_char_in_string_func.im_func + is_char_in_string = EditorWindow.is_char_in_string.im_func + + +class HyperParserTest(unittest.TestCase): + code = ( + '"""This is a module docstring"""\n' + '# this line is a comment\n' + 'x = "this is a string"\n' + "y = 'this is also a string'\n" + 'l = [i for i in range(10)]\n' + 'm = [py*py for # comment\n' + ' py in l]\n' + 'x.__len__\n' + "z = ((r'asdf')+('a')))\n" + '[x for x in\n' + 'for = False\n' + ) + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + cls.root.withdraw() + cls.text = Text(cls.root) + cls.editwin = DummyEditwin(cls.text) + + @classmethod + def tearDownClass(cls): + del cls.text, cls.editwin + cls.root.destroy() + del cls.root + + def setUp(self): + self.text.insert('insert', self.code) + + def tearDown(self): + self.text.delete('1.0', 'end') + self.editwin.context_use_ps1 = True + + def get_parser(self, index): + """ + Return a parser object with index at 'index' + """ + return HyperParser(self.editwin, index) + + def test_init(self): + """ + test corner cases in the init method + """ + with self.assertRaises(ValueError) as ve: + self.text.tag_add('console', '1.0', '1.end') + p = self.get_parser('1.5') + self.assertIn('precedes', str(ve.exception)) + + # test without ps1 + self.editwin.context_use_ps1 = False + + # number of lines lesser than 50 + p = self.get_parser('end') + self.assertEqual(p.rawtext, self.text.get('1.0', 'end')) + + # number of lines greater than 50 + self.text.insert('end', self.text.get('1.0', 'end')*4) + p = self.get_parser('54.5') + + def test_is_in_string(self): + get = self.get_parser + + p = get('1.0') + self.assertFalse(p.is_in_string()) + p = get('1.4') + self.assertTrue(p.is_in_string()) + p = get('2.3') + self.assertFalse(p.is_in_string()) + p = get('3.3') + self.assertFalse(p.is_in_string()) + p = get('3.7') + self.assertTrue(p.is_in_string()) + p = get('4.6') + self.assertTrue(p.is_in_string()) + + def test_is_in_code(self): + get = self.get_parser + + p = get('1.0') + self.assertTrue(p.is_in_code()) + p = get('1.1') + self.assertFalse(p.is_in_code()) + p = get('2.5') + self.assertFalse(p.is_in_code()) + p = get('3.4') + self.assertTrue(p.is_in_code()) + p = get('3.6') + self.assertFalse(p.is_in_code()) + p = get('4.14') + self.assertFalse(p.is_in_code()) + + def test_get_surrounding_bracket(self): + get = self.get_parser + + def without_mustclose(parser): + # a utility function to get surrounding bracket + # with mustclose=False + return parser.get_surrounding_brackets(mustclose=False) + + def with_mustclose(parser): + # a utility function to get surrounding bracket + # with mustclose=True + return parser.get_surrounding_brackets(mustclose=True) + + p = get('3.2') + self.assertIsNone(with_mustclose(p)) + self.assertIsNone(without_mustclose(p)) + + p = get('5.6') + self.assertTupleEqual(without_mustclose(p), ('5.4', '5.25')) + self.assertTupleEqual(without_mustclose(p), with_mustclose(p)) + + p = get('5.23') + self.assertTupleEqual(without_mustclose(p), ('5.21', '5.24')) + self.assertTupleEqual(without_mustclose(p), with_mustclose(p)) + + p = get('6.15') + self.assertTupleEqual(without_mustclose(p), ('6.4', '6.end')) + self.assertIsNone(with_mustclose(p)) + + p = get('9.end') + self.assertIsNone(with_mustclose(p)) + self.assertIsNone(without_mustclose(p)) + + def test_get_expression(self): + get = self.get_parser + + p = get('4.2') + self.assertEqual(p.get_expression(), 'y ') + + p = get('4.7') + with self.assertRaises(ValueError) as ve: + p.get_expression() + self.assertIn('is inside a code', str(ve.exception)) + + p = get('5.25') + self.assertEqual(p.get_expression(), 'range(10)') + + p = get('6.7') + self.assertEqual(p.get_expression(), 'py') + + p = get('6.8') + self.assertEqual(p.get_expression(), '') + + p = get('7.9') + self.assertEqual(p.get_expression(), 'py') + + p = get('8.end') + self.assertEqual(p.get_expression(), 'x.__len__') + + p = get('9.13') + self.assertEqual(p.get_expression(), "r'asdf'") + + p = get('9.17') + with self.assertRaises(ValueError) as ve: + p.get_expression() + self.assertIn('is inside a code', str(ve.exception)) + + p = get('10.0') + self.assertEqual(p.get_expression(), '') + + p = get('11.3') + self.assertEqual(p.get_expression(), '') + + p = get('11.11') + self.assertEqual(p.get_expression(), 'False') + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/test_idlehistory.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_idlehistory.py new file mode 100644 index 00000000000..b0767570fc7 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_idlehistory.py @@ -0,0 +1,168 @@ +import unittest +from test.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() + cls.root.withdraw() + + 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/contrib/tools/python/src/Lib/idlelib/idle_test/test_io.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_io.py new file mode 100644 index 00000000000..ee017bb8c67 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_io.py @@ -0,0 +1,267 @@ +import unittest +import io +from idlelib.PyShell import PseudoInputFile, PseudoOutputFile +from test import test_support as support + + +class Base(object): + 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 S(Base, str): + pass + +class U(Base, unicode): + pass + +class BA(Base, bytearray): + pass + +class MockShell: + def __init__(self): + self.reset() + + def write(self, *args): + self.written.append(args) + + def readline(self): + return self.lines.pop() + + def close(self): + pass + + def reset(self): + self.written = [] + + def push(self, lines): + self.lines = list(lines)[::-1] + + +class PseudeOutputFilesTest(unittest.TestCase): + def test_misc(self): + shell = MockShell() + f = PseudoOutputFile(shell, 'stdout', 'utf-8') + self.assertIsInstance(f, io.TextIOBase) + self.assertEqual(f.encoding, 'utf-8') + self.assertIsNone(f.errors) + self.assertIsNone(f.newlines) + self.assertEqual(f.name, '<stdout>') + self.assertFalse(f.closed) + self.assertTrue(f.isatty()) + self.assertFalse(f.readable()) + self.assertTrue(f.writable()) + self.assertFalse(f.seekable()) + + def test_unsupported(self): + shell = MockShell() + f = PseudoOutputFile(shell, 'stdout', 'utf-8') + self.assertRaises(IOError, f.fileno) + self.assertRaises(IOError, f.tell) + self.assertRaises(IOError, f.seek, 0) + self.assertRaises(IOError, f.read, 0) + self.assertRaises(IOError, f.readline, 0) + + def test_write(self): + shell = MockShell() + f = PseudoOutputFile(shell, 'stdout', 'utf-8') + f.write('test') + self.assertEqual(shell.written, [('test', 'stdout')]) + shell.reset() + f.write('t\xe8st') + self.assertEqual(shell.written, [('t\xe8st', 'stdout')]) + shell.reset() + f.write(u't\xe8st') + self.assertEqual(shell.written, [(u't\xe8st', 'stdout')]) + shell.reset() + + f.write(S('t\xe8st')) + self.assertEqual(shell.written, [('t\xe8st', 'stdout')]) + self.assertEqual(type(shell.written[0][0]), str) + shell.reset() + f.write(BA('t\xe8st')) + self.assertEqual(shell.written, [('t\xe8st', 'stdout')]) + self.assertEqual(type(shell.written[0][0]), str) + shell.reset() + f.write(U(u't\xe8st')) + self.assertEqual(shell.written, [(u't\xe8st', 'stdout')]) + self.assertEqual(type(shell.written[0][0]), unicode) + shell.reset() + + self.assertRaises(TypeError, f.write) + self.assertEqual(shell.written, []) + self.assertRaises(TypeError, f.write, 123) + self.assertEqual(shell.written, []) + self.assertRaises(TypeError, f.write, 'test', 'spam') + self.assertEqual(shell.written, []) + + def test_writelines(self): + shell = MockShell() + f = PseudoOutputFile(shell, 'stdout', 'utf-8') + f.writelines([]) + self.assertEqual(shell.written, []) + shell.reset() + f.writelines(['one\n', 'two']) + self.assertEqual(shell.written, + [('one\n', 'stdout'), ('two', 'stdout')]) + shell.reset() + f.writelines(['on\xe8\n', 'tw\xf2']) + self.assertEqual(shell.written, + [('on\xe8\n', 'stdout'), ('tw\xf2', 'stdout')]) + shell.reset() + f.writelines([u'on\xe8\n', u'tw\xf2']) + self.assertEqual(shell.written, + [(u'on\xe8\n', 'stdout'), (u'tw\xf2', 'stdout')]) + shell.reset() + + f.writelines([S('t\xe8st')]) + self.assertEqual(shell.written, [('t\xe8st', 'stdout')]) + self.assertEqual(type(shell.written[0][0]), str) + shell.reset() + f.writelines([BA('t\xe8st')]) + self.assertEqual(shell.written, [('t\xe8st', 'stdout')]) + self.assertEqual(type(shell.written[0][0]), str) + shell.reset() + f.writelines([U(u't\xe8st')]) + self.assertEqual(shell.written, [(u't\xe8st', 'stdout')]) + self.assertEqual(type(shell.written[0][0]), unicode) + shell.reset() + + self.assertRaises(TypeError, f.writelines) + self.assertEqual(shell.written, []) + self.assertRaises(TypeError, f.writelines, 123) + self.assertEqual(shell.written, []) + self.assertRaises(TypeError, f.writelines, [123]) + self.assertEqual(shell.written, []) + self.assertRaises(TypeError, f.writelines, [], []) + self.assertEqual(shell.written, []) + + def test_close(self): + shell = MockShell() + f = PseudoOutputFile(shell, 'stdout', 'utf-8') + self.assertFalse(f.closed) + f.write('test') + f.close() + self.assertTrue(f.closed) + self.assertRaises(ValueError, f.write, 'x') + self.assertEqual(shell.written, [('test', 'stdout')]) + f.close() + self.assertRaises(TypeError, f.close, 1) + + +class PseudeInputFilesTest(unittest.TestCase): + def test_misc(self): + shell = MockShell() + f = PseudoInputFile(shell, 'stdin', 'utf-8') + self.assertIsInstance(f, io.TextIOBase) + self.assertEqual(f.encoding, 'utf-8') + self.assertIsNone(f.errors) + self.assertIsNone(f.newlines) + self.assertEqual(f.name, '<stdin>') + self.assertFalse(f.closed) + self.assertTrue(f.isatty()) + self.assertTrue(f.readable()) + self.assertFalse(f.writable()) + self.assertFalse(f.seekable()) + + def test_unsupported(self): + shell = MockShell() + f = PseudoInputFile(shell, 'stdin', 'utf-8') + self.assertRaises(IOError, f.fileno) + self.assertRaises(IOError, f.tell) + self.assertRaises(IOError, f.seek, 0) + self.assertRaises(IOError, f.write, 'x') + self.assertRaises(IOError, f.writelines, ['x']) + + def test_read(self): + shell = MockShell() + f = 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) + + +def test_main(): + support.run_unittest(PseudeOutputFilesTest, PseudeInputFilesTest) + +if __name__ == '__main__': + test_main() diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/test_parenmatch.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_parenmatch.py new file mode 100644 index 00000000000..1621981540f --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_parenmatch.py @@ -0,0 +1,121 @@ +"""Test idlelib.ParenMatch.""" +# This must currently be a gui test because ParenMatch methods use +# several text methods not defined on idlelib.idle_test.mock_tk.Text. + +import unittest +from test.test_support import requires +from Tkinter import Tk, Text +from idlelib.ParenMatch import ParenMatch + +class Mock: # 2.7 does not have unittest.mock + def __init__(self, *args, **kwargs): + self.called = False + + def __call__(self, *args, **kwargs): + self.called = True + + def reset_mock(self, *args, **kwargs): + self.called = False + + def after(self, *args, **kwargs): + pass + +class DummyEditwin: + def __init__(self, text): + self.text = text + self.indentwidth = 8 + self.tabwidth = 8 + self.context_use_ps1 = True + + +class ParenMatchTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + cls.text = Text(cls.root) + cls.editwin = DummyEditwin(cls.text) + cls.editwin.text_frame = Mock() + + @classmethod + def tearDownClass(cls): + del cls.text, cls.editwin + cls.root.destroy() + del cls.root + + def tearDown(self): + self.text.delete('1.0', 'end') + + def test_paren_expression(self): + """ + Test ParenMatch with 'expression' style. + """ + text = self.text + pm = ParenMatch(self.editwin) + pm.set_style('expression') + + text.insert('insert', 'def foobar(a, b') + pm.flash_paren_event('event') + self.assertIn('<<parenmatch-check-restore>>', text.event_info()) + self.assertTupleEqual(text.tag_prevrange('paren', 'end'), + ('1.10', '1.15')) + text.insert('insert', ')') + pm.restore_event() + self.assertNotIn('<<parenmatch-check-restore>>', text.event_info()) + self.assertEqual(text.tag_prevrange('paren', 'end'), ()) + + # paren_closed_event can only be tested as below + pm.paren_closed_event('event') + self.assertTupleEqual(text.tag_prevrange('paren', 'end'), + ('1.10', '1.16')) + + def test_paren_default(self): + """ + Test ParenMatch with 'default' style. + """ + text = self.text + pm = ParenMatch(self.editwin) + pm.set_style('default') + + text.insert('insert', 'def foobar(a, b') + pm.flash_paren_event('event') + self.assertIn('<<parenmatch-check-restore>>', text.event_info()) + self.assertTupleEqual(text.tag_prevrange('paren', 'end'), + ('1.10', '1.11')) + text.insert('insert', ')') + pm.restore_event() + self.assertNotIn('<<parenmatch-check-restore>>', text.event_info()) + self.assertEqual(text.tag_prevrange('paren', 'end'), ()) + + def test_paren_corner(self): + """ + Test corner cases in flash_paren_event and paren_closed_event. + + These cases force conditional expression and alternate paths. + """ + text = self.text + pm = ParenMatch(self.editwin) + + text.insert('insert', '# this is a commen)') + self.assertIsNone(pm.paren_closed_event('event')) + + text.insert('insert', '\ndef') + self.assertIsNone(pm.flash_paren_event('event')) + self.assertIsNone(pm.paren_closed_event('event')) + + text.insert('insert', ' a, *arg)') + self.assertIsNone(pm.paren_closed_event('event')) + + def test_handle_restore_timer(self): + pm = ParenMatch(self.editwin) + pm.restore_event = Mock() + pm.handle_restore_timer(0) + self.assertTrue(pm.restore_event.called) + pm.restore_event.reset_mock() + pm.handle_restore_timer(1) + self.assertFalse(pm.restore_event.called) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/test_pathbrowser.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_pathbrowser.py new file mode 100644 index 00000000000..f02841481bf --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_pathbrowser.py @@ -0,0 +1,28 @@ +import unittest +import os +import sys +import idlelib +from idlelib import PathBrowser + +class PathBrowserTest(unittest.TestCase): + + def test_DirBrowserTreeItem(self): + # Issue16226 - make sure that getting a sublist works + d = PathBrowser.DirBrowserTreeItem('') + d.GetSubList() + self.assertEqual('', d.GetText()) + + dir = os.path.split(os.path.abspath(idlelib.__file__))[0] + self.assertEqual(d.ispackagedir(dir), True) + self.assertEqual(d.ispackagedir(dir + '/Icons'), False) + + def test_PathBrowserTreeItem(self): + p = PathBrowser.PathBrowserTreeItem() + self.assertEqual(p.GetText(), 'sys.path') + sub = p.GetSubList() + self.assertEqual(len(sub), len(sys.path)) + # Following fails in 2.7 because old-style class + #self.assertEqual(type(sub[0]), PathBrowser.DirBrowserTreeItem) + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=False) diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/test_rstrip.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_rstrip.py new file mode 100644 index 00000000000..1c90b93d216 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_rstrip.py @@ -0,0 +1,49 @@ +import unittest +import idlelib.RstripExtension as rs +from idlelib.idle_test.mock_idle import Editor + +class rstripTest(unittest.TestCase): + + def test_rstrip_line(self): + editor = Editor() + text = editor.text + do_rstrip = rs.RstripExtension(editor).do_rstrip + + do_rstrip() + self.assertEqual(text.get('1.0', 'insert'), '') + text.insert('1.0', ' ') + do_rstrip() + self.assertEqual(text.get('1.0', 'insert'), '') + text.insert('1.0', ' \n') + do_rstrip() + self.assertEqual(text.get('1.0', 'insert'), '\n') + + def test_rstrip_multiple(self): + editor = Editor() + # Uncomment following to verify that test passes with real widgets. +## from idlelib.EditorWindow import EditorWindow as Editor +## from tkinter import Tk +## editor = Editor(root=Tk()) + text = editor.text + do_rstrip = rs.RstripExtension(editor).do_rstrip + + original = ( + "Line with an ending tab \n" + "Line ending in 5 spaces \n" + "Linewithnospaces\n" + " indented line\n" + " indented line with trailing space \n" + " ") + stripped = ( + "Line with an ending tab\n" + "Line ending in 5 spaces\n" + "Linewithnospaces\n" + " indented line\n" + " indented line with trailing space\n") + + text.insert('1.0', original) + do_rstrip() + self.assertEqual(text.get('1.0', 'insert'), stripped) + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=False) diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/test_searchdialogbase.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_searchdialogbase.py new file mode 100644 index 00000000000..32abfe6f792 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_searchdialogbase.py @@ -0,0 +1,164 @@ +'''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.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/contrib/tools/python/src/Lib/idlelib/idle_test/test_searchengine.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_searchengine.py new file mode 100644 index 00000000000..8bf9d4728a0 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_searchengine.py @@ -0,0 +1,329 @@ +'''Test functions and SearchEngine class in SearchEngine.py.''' + +# With mock replacements, the module does not use any gui widgets. +# The use of tk.Text is avoided (for now, until mock Text is improved) +# by patching instances with an index function returning what is needed. +# This works because mock Text.get does not use .index. + +import re +import unittest +#from test.test_support import requires +from Tkinter import BooleanVar, StringVar, TclError # ,Tk, Text +import tkMessageBox +from idlelib import SearchEngine as se +from idlelib.idle_test.mock_tk import Var, Mbox +from idlelib.idle_test.mock_tk import Text as mockText + +def setUpModule(): + # Replace s-e module tkinter imports other than non-gui TclError. + se.BooleanVar = Var + se.StringVar = Var + se.tkMessageBox = Mbox + +def tearDownModule(): + # Restore 'just in case', though other tests should also replace. + se.BooleanVar = BooleanVar + se.StringVar = StringVar + se.tkMessageBox = tkMessageBox + + +class Mock: + def __init__(self, *args, **kwargs): pass + +class GetTest(unittest.TestCase): + # SearchEngine.get returns singleton created & saved on first call. + def test_get(self): + saved_Engine = se.SearchEngine + se.SearchEngine = Mock # monkey-patch class + try: + root = Mock() + engine = se.get(root) + self.assertIsInstance(engine, se.SearchEngine) + self.assertIs(root._searchengine, engine) + self.assertIs(se.get(root), engine) + finally: + se.SearchEngine = saved_Engine # restore class to module + +class GetLineColTest(unittest.TestCase): + # Test simple text-independent helper function + def test_get_line_col(self): + self.assertEqual(se.get_line_col('1.0'), (1, 0)) + self.assertEqual(se.get_line_col('1.11'), (1, 11)) + + self.assertRaises(ValueError, se.get_line_col, ('1.0 lineend')) + self.assertRaises(ValueError, se.get_line_col, ('end')) + +class GetSelectionTest(unittest.TestCase): + # Test text-dependent helper function. +## # Need gui for text.index('sel.first/sel.last/insert'). +## @classmethod +## def setUpClass(cls): +## requires('gui') +## cls.root = Tk() +## +## @classmethod +## def tearDownClass(cls): +## cls.root.destroy() +## del cls.root + + def test_get_selection(self): + # text = Text(master=self.root) + text = mockText() + text.insert('1.0', 'Hello World!') + + # fix text.index result when called in get_selection + def sel(s): + # select entire text, cursor irrelevant + if s == 'sel.first': return '1.0' + if s == 'sel.last': return '1.12' + raise TclError + text.index = sel # replaces .tag_add('sel', '1.0, '1.12') + self.assertEqual(se.get_selection(text), ('1.0', '1.12')) + + def mark(s): + # no selection, cursor after 'Hello' + if s == 'insert': return '1.5' + raise TclError + text.index = mark # replaces .mark_set('insert', '1.5') + self.assertEqual(se.get_selection(text), ('1.5', '1.5')) + + +class ReverseSearchTest(unittest.TestCase): + # Test helper function that searches backwards within a line. + def test_search_reverse(self): + Equal = self.assertEqual + line = "Here is an 'is' test text." + prog = re.compile('is') + Equal(se.search_reverse(prog, line, len(line)).span(), (12, 14)) + Equal(se.search_reverse(prog, line, 14).span(), (12, 14)) + Equal(se.search_reverse(prog, line, 13).span(), (5, 7)) + Equal(se.search_reverse(prog, line, 7).span(), (5, 7)) + Equal(se.search_reverse(prog, line, 6), None) + + +class SearchEngineTest(unittest.TestCase): + # Test class methods that do not use Text widget. + + def setUp(self): + self.engine = se.SearchEngine(root=None) + # Engine.root is only used to create error message boxes. + # The mock replacement ignores the root argument. + + def test_is_get(self): + engine = self.engine + Equal = self.assertEqual + + Equal(engine.getpat(), '') + engine.setpat('hello') + Equal(engine.getpat(), 'hello') + + Equal(engine.isre(), False) + engine.revar.set(1) + Equal(engine.isre(), True) + + Equal(engine.iscase(), False) + engine.casevar.set(1) + Equal(engine.iscase(), True) + + Equal(engine.isword(), False) + engine.wordvar.set(1) + Equal(engine.isword(), True) + + Equal(engine.iswrap(), True) + engine.wrapvar.set(0) + Equal(engine.iswrap(), False) + + Equal(engine.isback(), False) + engine.backvar.set(1) + Equal(engine.isback(), True) + + def test_setcookedpat(self): + engine = self.engine + engine.setcookedpat('\s') + self.assertEqual(engine.getpat(), '\s') + engine.revar.set(1) + engine.setcookedpat('\s') + self.assertEqual(engine.getpat(), r'\\s') + + def test_getcookedpat(self): + engine = self.engine + Equal = self.assertEqual + + Equal(engine.getcookedpat(), '') + engine.setpat('hello') + Equal(engine.getcookedpat(), 'hello') + engine.wordvar.set(True) + Equal(engine.getcookedpat(), r'\bhello\b') + engine.wordvar.set(False) + + engine.setpat('\s') + Equal(engine.getcookedpat(), r'\\s') + engine.revar.set(True) + Equal(engine.getcookedpat(), '\s') + + def test_getprog(self): + engine = self.engine + Equal = self.assertEqual + + engine.setpat('Hello') + temppat = engine.getprog() + Equal(temppat.pattern, re.compile('Hello', re.IGNORECASE).pattern) + engine.casevar.set(1) + temppat = engine.getprog() + Equal(temppat.pattern, re.compile('Hello').pattern, 0) + + engine.setpat('') + Equal(engine.getprog(), None) + engine.setpat('+') + engine.revar.set(1) + Equal(engine.getprog(), None) + self.assertEqual(Mbox.showerror.message, + 'Error: nothing to repeat\nPattern: +') + + def test_report_error(self): + showerror = Mbox.showerror + Equal = self.assertEqual + pat = '[a-z' + msg = 'unexpected end of regular expression' + + Equal(self.engine.report_error(pat, msg), None) + Equal(showerror.title, 'Regular expression error') + expected_message = ("Error: " + msg + "\nPattern: [a-z") + Equal(showerror.message, expected_message) + + Equal(self.engine.report_error(pat, msg, 5), None) + Equal(showerror.title, 'Regular expression error') + expected_message += "\nOffset: 5" + Equal(showerror.message, expected_message) + + +class SearchTest(unittest.TestCase): + # Test that search_text makes right call to right method. + + @classmethod + def setUpClass(cls): +## requires('gui') +## cls.root = Tk() +## cls.text = Text(master=cls.root) + cls.text = mockText() + test_text = ( + 'First line\n' + 'Line with target\n' + 'Last line\n') + cls.text.insert('1.0', test_text) + cls.pat = re.compile('target') + + cls.engine = se.SearchEngine(None) + cls.engine.search_forward = lambda *args: ('f', args) + cls.engine.search_backward = lambda *args: ('b', args) + +## @classmethod +## def tearDownClass(cls): +## cls.root.destroy() +## del cls.root + + def test_search(self): + Equal = self.assertEqual + engine = self.engine + search = engine.search_text + text = self.text + pat = self.pat + + engine.patvar.set(None) + #engine.revar.set(pat) + Equal(search(text), None) + + def mark(s): + # no selection, cursor after 'Hello' + if s == 'insert': return '1.5' + raise TclError + text.index = mark + Equal(search(text, pat), ('f', (text, pat, 1, 5, True, False))) + engine.wrapvar.set(False) + Equal(search(text, pat), ('f', (text, pat, 1, 5, False, False))) + engine.wrapvar.set(True) + engine.backvar.set(True) + Equal(search(text, pat), ('b', (text, pat, 1, 5, True, False))) + engine.backvar.set(False) + + def sel(s): + if s == 'sel.first': return '2.10' + if s == 'sel.last': return '2.16' + raise TclError + text.index = sel + Equal(search(text, pat), ('f', (text, pat, 2, 16, True, False))) + Equal(search(text, pat, True), ('f', (text, pat, 2, 10, True, True))) + engine.backvar.set(True) + Equal(search(text, pat), ('b', (text, pat, 2, 10, True, False))) + Equal(search(text, pat, True), ('b', (text, pat, 2, 16, True, True))) + + +class ForwardBackwardTest(unittest.TestCase): + # Test that search_forward method finds the target. +## @classmethod +## def tearDownClass(cls): +## cls.root.destroy() +## del cls.root + + @classmethod + def setUpClass(cls): + cls.engine = se.SearchEngine(None) +## requires('gui') +## cls.root = Tk() +## cls.text = Text(master=cls.root) + cls.text = mockText() + # search_backward calls index('end-1c') + cls.text.index = lambda index: '4.0' + test_text = ( + 'First line\n' + 'Line with target\n' + 'Last line\n') + cls.text.insert('1.0', test_text) + cls.pat = re.compile('target') + cls.res = (2, (10, 16)) # line, slice indexes of 'target' + cls.failpat = re.compile('xyz') # not in text + cls.emptypat = re.compile('\w*') # empty match possible + + def make_search(self, func): + def search(pat, line, col, wrap, ok=0): + res = func(self.text, pat, line, col, wrap, ok) + # res is (line, matchobject) or None + return (res[0], res[1].span()) if res else res + return search + + def test_search_forward(self): + # search for non-empty match + Equal = self.assertEqual + forward = self.make_search(self.engine.search_forward) + pat = self.pat + Equal(forward(pat, 1, 0, True), self.res) + Equal(forward(pat, 3, 0, True), self.res) # wrap + Equal(forward(pat, 3, 0, False), None) # no wrap + Equal(forward(pat, 2, 10, False), self.res) + + Equal(forward(self.failpat, 1, 0, True), None) + Equal(forward(self.emptypat, 2, 9, True, ok=True), (2, (9, 9))) + #Equal(forward(self.emptypat, 2, 9, True), self.res) + # While the initial empty match is correctly ignored, skipping + # the rest of the line and returning (3, (0,4)) seems buggy - tjr. + Equal(forward(self.emptypat, 2, 10, True), self.res) + + def test_search_backward(self): + # search for non-empty match + Equal = self.assertEqual + backward = self.make_search(self.engine.search_backward) + pat = self.pat + Equal(backward(pat, 3, 5, True), self.res) + Equal(backward(pat, 2, 0, True), self.res) # wrap + Equal(backward(pat, 2, 0, False), None) # no wrap + Equal(backward(pat, 2, 16, False), self.res) + + Equal(backward(self.failpat, 3, 9, True), None) + Equal(backward(self.emptypat, 2, 10, True, ok=True), (2, (9,9))) + # Accepted because 9 < 10, not because ok=True. + # It is not clear that ok=True is useful going back - tjr + Equal(backward(self.emptypat, 2, 9, True), (2, (5, 9))) + + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=2) diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/test_text.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_text.py new file mode 100644 index 00000000000..50d3facec7c --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_text.py @@ -0,0 +1,227 @@ +# Test mock_tk.Text class against tkinter.Text class by running same tests with both. +import unittest +from test.test_support import requires + +from _tkinter import TclError + +class TextTest(object): + + hw = 'hello\nworld' # usual initial insert after initialization + hwn = hw+'\n' # \n present at initialization, before insert + + Text = None + def setUp(self): + self.text = self.Text() + + def test_init(self): + self.assertEqual(self.text.get('1.0'), '\n') + self.assertEqual(self.text.get('end'), '') + + def test_index_empty(self): + index = self.text.index + + for dex in (-1.0, 0.3, '1.-1', '1.0', '1.0 lineend', '1.end', '1.33', + 'insert'): + self.assertEqual(index(dex), '1.0') + + for dex in 'end', 2.0, '2.1', '33.44': + self.assertEqual(index(dex), '2.0') + + def test_index_data(self): + index = self.text.index + self.text.insert('1.0', self.hw) + + for dex in -1.0, 0.3, '1.-1', '1.0': + self.assertEqual(index(dex), '1.0') + + for dex in '1.0 lineend', '1.end', '1.33': + self.assertEqual(index(dex), '1.5') + + for dex in 'end', '33.44': + self.assertEqual(index(dex), '3.0') + + def test_get(self): + get = self.text.get + Equal = self.assertEqual + self.text.insert('1.0', self.hw) + + Equal(get('end'), '') + Equal(get('end', 'end'), '') + Equal(get('1.0'), 'h') + Equal(get('1.0', '1.1'), 'h') + Equal(get('1.0', '1.3'), 'hel') + Equal(get('1.1', '1.3'), 'el') + Equal(get('1.0', '1.0 lineend'), 'hello') + Equal(get('1.0', '1.10'), 'hello') + Equal(get('1.0 lineend'), '\n') + Equal(get('1.1', '2.3'), 'ello\nwor') + Equal(get('1.0', '2.5'), self.hw) + Equal(get('1.0', 'end'), self.hwn) + Equal(get('0.0', '5.0'), self.hwn) + + def test_insert(self): + insert = self.text.insert + get = self.text.get + Equal = self.assertEqual + + insert('1.0', self.hw) + Equal(get('1.0', 'end'), self.hwn) + + insert('1.0', '') # nothing + Equal(get('1.0', 'end'), self.hwn) + + insert('1.0', '*') + Equal(get('1.0', 'end'), '*hello\nworld\n') + + insert('1.0 lineend', '*') + Equal(get('1.0', 'end'), '*hello*\nworld\n') + + insert('2.3', '*') + Equal(get('1.0', 'end'), '*hello*\nwor*ld\n') + + insert('end', 'x') + Equal(get('1.0', 'end'), '*hello*\nwor*ldx\n') + + insert('1.4', 'x\n') + Equal(get('1.0', 'end'), '*helx\nlo*\nwor*ldx\n') + + def test_no_delete(self): + # if index1 == 'insert' or 'end' or >= end, there is no deletion + delete = self.text.delete + get = self.text.get + Equal = self.assertEqual + self.text.insert('1.0', self.hw) + + delete('insert') + Equal(get('1.0', 'end'), self.hwn) + + delete('end') + Equal(get('1.0', 'end'), self.hwn) + + delete('insert', 'end') + Equal(get('1.0', 'end'), self.hwn) + + delete('insert', '5.5') + Equal(get('1.0', 'end'), self.hwn) + + delete('1.4', '1.0') + Equal(get('1.0', 'end'), self.hwn) + + delete('1.4', '1.4') + Equal(get('1.0', 'end'), self.hwn) + + def test_delete_char(self): + delete = self.text.delete + get = self.text.get + Equal = self.assertEqual + self.text.insert('1.0', self.hw) + + delete('1.0') + Equal(get('1.0', '1.end'), 'ello') + + delete('1.0', '1.1') + Equal(get('1.0', '1.end'), 'llo') + + # delete \n and combine 2 lines into 1 + delete('1.end') + Equal(get('1.0', '1.end'), 'lloworld') + + self.text.insert('1.3', '\n') + delete('1.10') + Equal(get('1.0', '1.end'), 'lloworld') + + self.text.insert('1.3', '\n') + delete('1.3', '2.0') + Equal(get('1.0', '1.end'), 'lloworld') + + def test_delete_slice(self): + delete = self.text.delete + get = self.text.get + Equal = self.assertEqual + self.text.insert('1.0', self.hw) + + delete('1.0', '1.0 lineend') + Equal(get('1.0', 'end'), '\nworld\n') + + delete('1.0', 'end') + Equal(get('1.0', 'end'), '\n') + + self.text.insert('1.0', self.hw) + delete('1.0', '2.0') + Equal(get('1.0', 'end'), 'world\n') + + delete('1.0', 'end') + Equal(get('1.0', 'end'), '\n') + + self.text.insert('1.0', self.hw) + delete('1.2', '2.3') + Equal(get('1.0', 'end'), 'held\n') + + def test_multiple_lines(self): # insert and delete + self.text.insert('1.0', 'hello') + + self.text.insert('1.3', '1\n2\n3\n4\n5') + self.assertEqual(self.text.get('1.0', 'end'), 'hel1\n2\n3\n4\n5lo\n') + + self.text.delete('1.3', '5.1') + self.assertEqual(self.text.get('1.0', 'end'), 'hello\n') + + def test_compare(self): + compare = self.text.compare + Equal = self.assertEqual + # need data so indexes not squished to 1,0 + self.text.insert('1.0', 'First\nSecond\nThird\n') + + self.assertRaises(TclError, compare, '2.2', 'op', '2.2') + + for op, less1, less0, equal, greater0, greater1 in ( + ('<', True, True, False, False, False), + ('<=', True, True, True, False, False), + ('>', False, False, False, True, True), + ('>=', False, False, True, True, True), + ('==', False, False, True, False, False), + ('!=', True, True, False, True, True), + ): + Equal(compare('1.1', op, '2.2'), less1, op) + Equal(compare('2.1', op, '2.2'), less0, op) + Equal(compare('2.2', op, '2.2'), equal, op) + Equal(compare('2.3', op, '2.2'), greater0, op) + Equal(compare('3.3', op, '2.2'), greater1, op) + + +class MockTextTest(TextTest, unittest.TestCase): + + @classmethod + def setUpClass(cls): + from idlelib.idle_test.mock_tk import Text + cls.Text = Text + + def test_decode(self): + # test endflags (-1, 0) not tested by test_index (which uses +1) + decode = self.text._decode + Equal = self.assertEqual + self.text.insert('1.0', self.hw) + + Equal(decode('end', -1), (2, 5)) + Equal(decode('3.1', -1), (2, 5)) + Equal(decode('end', 0), (2, 6)) + Equal(decode('3.1', 0), (2, 6)) + + +class TkTextTest(TextTest, unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + from Tkinter import Tk, Text + cls.Text = Text + cls.root = Tk() + + @classmethod + def tearDownClass(cls): + cls.root.destroy() + del cls.root + + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=False) diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/test_textview.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_textview.py new file mode 100644 index 00000000000..fa437fcb6e5 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_textview.py @@ -0,0 +1,96 @@ +'''Test the functions and main class method of textView.py.''' + +import unittest +import os +from test.test_support import requires +from Tkinter import Tk +from idlelib import textView as tv +from idlelib.idle_test.mock_idle import Func +from idlelib.idle_test.mock_tk import Mbox + + +class TV(tv.TextViewer): # Use in TextViewTest + transient = Func() + grab_set = Func() + wait_window = Func() + +class textviewClassTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + cls.root.withdraw() + + @classmethod + def tearDownClass(cls): + cls.root.destroy() + del cls.root + + def setUp(self): + TV.transient.__init__() + TV.grab_set.__init__() + TV.wait_window.__init__() + + def test_init_modal(self): + view = TV(self.root, 'Title', 'test text') + self.assertTrue(TV.transient.called) + self.assertTrue(TV.grab_set.called) + self.assertTrue(TV.wait_window.called) + view.Ok() + + def test_init_nonmodal(self): + view = TV(self.root, 'Title', 'test text', modal=False) + self.assertFalse(TV.transient.called) + self.assertFalse(TV.grab_set.called) + self.assertFalse(TV.wait_window.called) + view.Ok() + + def test_ok(self): + view = TV(self.root, 'Title', 'test text', modal=False) + view.destroy = Func() + view.Ok() + self.assertTrue(view.destroy.called) + del view.destroy # Unmask the real function. + view.destroy() + + +class ViewFunctionTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + cls.root.withdraw() + cls.orig_mbox = tv.tkMessageBox + tv.tkMessageBox = Mbox + + @classmethod + def tearDownClass(cls): + cls.root.destroy() + del cls.root + tv.tkMessageBox = cls.orig_mbox + del cls.orig_mbox + + def test_view_text(self): + # If modal True, get tkinter error 'can't invoke "event" command'. + view = tv.view_text(self.root, 'Title', 'test text', modal=False) + self.assertIsInstance(view, tv.TextViewer) + view.Ok() + + def test_view_file(self): + test_dir = os.path.dirname(__file__) + testfile = os.path.join(test_dir, 'test_textview.py') + view = tv.view_file(self.root, 'Title', testfile, modal=False) + self.assertIsInstance(view, tv.TextViewer) + self.assertIn('Test', view.textView.get('1.0', '1.end')) + view.Ok() + + # Mock messagebox will be used; view_file will return None. + testfile = os.path.join(test_dir, '../notthere.py') + view = tv.view_file(self.root, 'Title', testfile, modal=False) + self.assertIsNone(view) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/test_warning.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_warning.py new file mode 100644 index 00000000000..da1d8a1d0ab --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_warning.py @@ -0,0 +1,73 @@ +'''Test warnings replacement in PyShell.py and run.py. + +This file could be expanded to include traceback overrides +(in same two modules). If so, change name. +Revise if output destination changes (http://bugs.python.org/issue18318). +Make sure warnings module is left unaltered (http://bugs.python.org/issue18081). +''' + +import unittest +from test.test_support import captured_stderr + +import warnings +# Try to capture default showwarning before Idle modules are imported. +showwarning = warnings.showwarning +# But if we run this file within idle, we are in the middle of the run.main loop +# and default showwarnings has already been replaced. +running_in_idle = 'idle' in showwarning.__name__ + +from idlelib import run +from idlelib import PyShell as shell + +# The following was generated from PyShell.idle_formatwarning +# and checked as matching expectation. +idlemsg = ''' +Warning (from warnings module): + File "test_warning.py", line 99 + Line of code +UserWarning: Test +''' +shellmsg = idlemsg + ">>> " + +class RunWarnTest(unittest.TestCase): + + @unittest.skipIf(running_in_idle, "Does not work when run within Idle.") + def test_showwarnings(self): + self.assertIs(warnings.showwarning, showwarning) + run.capture_warnings(True) + self.assertIs(warnings.showwarning, run.idle_showwarning_subproc) + run.capture_warnings(False) + self.assertIs(warnings.showwarning, showwarning) + + def test_run_show(self): + with captured_stderr() as f: + run.idle_showwarning_subproc( + 'Test', UserWarning, 'test_warning.py', 99, f, 'Line of code') + # The following uses .splitlines to erase line-ending differences + self.assertEqual(idlemsg.splitlines(), f.getvalue().splitlines()) + +class ShellWarnTest(unittest.TestCase): + + @unittest.skipIf(running_in_idle, "Does not work when run within Idle.") + def test_showwarnings(self): + self.assertIs(warnings.showwarning, showwarning) + shell.capture_warnings(True) + self.assertIs(warnings.showwarning, shell.idle_showwarning) + shell.capture_warnings(False) + self.assertIs(warnings.showwarning, showwarning) + + def test_idle_formatter(self): + # Will fail if format changed without regenerating idlemsg + s = shell.idle_formatwarning( + 'Test', UserWarning, 'test_warning.py', 99, 'Line of code') + self.assertEqual(idlemsg, s) + + def test_shell_show(self): + with captured_stderr() as f: + shell.idle_showwarning( + 'Test', UserWarning, 'test_warning.py', 99, f, 'Line of code') + self.assertEqual(shellmsg.splitlines(), f.getvalue().splitlines()) + + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=False) diff --git a/contrib/tools/python/src/Lib/idlelib/idle_test/test_widgetredir.py b/contrib/tools/python/src/Lib/idlelib/idle_test/test_widgetredir.py new file mode 100644 index 00000000000..e35ea4174d9 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idle_test/test_widgetredir.py @@ -0,0 +1,124 @@ +"""Unittest for idlelib.WidgetRedirector + +100% coverage +""" +from test.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.root = Tk() + cls.root.withdraw() + cls.text = Text(cls.root) + + @classmethod + def tearDownClass(cls): + del cls.text + cls.root.destroy() + del cls.root + + 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.root = Tk() + cls.root.withdraw() + cls.text = Text(cls.root) + + @classmethod + def tearDownClass(cls): + del cls.text + cls.root.destroy() + del cls.root + + 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.root.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.root.call(self.text._w, 'insert', 'boo'), '') + + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/contrib/tools/python/src/Lib/idlelib/idlever.py b/contrib/tools/python/src/Lib/idlelib/idlever.py new file mode 100644 index 00000000000..3e9f69a3e37 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/idlever.py @@ -0,0 +1,12 @@ +""" +The separate Idle version was eliminated years ago; +idlelib.idlever is no longer used by Idle +and will be removed in 3.6 or later. Use + from sys import version + IDLE_VERSION = version[:version.index(' ')] +""" +# Kept for now only for possible existing extension use +import warnings as w +w.warn(__doc__, DeprecationWarning, stacklevel=2) +from sys import version +IDLE_VERSION = version[:version.index(' ')] diff --git a/contrib/tools/python/src/Lib/idlelib/keybindingDialog.py b/contrib/tools/python/src/Lib/idlelib/keybindingDialog.py new file mode 100644 index 00000000000..755f1af47e2 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/keybindingDialog.py @@ -0,0 +1,268 @@ +""" +Dialog for building Tkinter accelerator key bindings +""" +from Tkinter import * +import tkMessageBox +import string +import sys + +class GetKeysDialog(Toplevel): + def __init__(self,parent,title,action,currentKeySequences,_htest=False): + """ + action - string, the name of the virtual event these keys will be + mapped to + currentKeys - list, a list of all key sequence lists currently mapped + to virtual events, for overlap checking + _htest - bool, change box location when running htest + """ + Toplevel.__init__(self, parent) + self.configure(borderwidth=5) + self.resizable(height=FALSE,width=FALSE) + self.title(title) + self.transient(parent) + self.grab_set() + self.protocol("WM_DELETE_WINDOW", self.Cancel) + self.parent = parent + self.action=action + self.currentKeySequences=currentKeySequences + self.result='' + self.keyString=StringVar(self) + self.keyString.set('') + self.SetModifiersForPlatform() # set self.modifiers, self.modifier_label + self.modifier_vars = [] + for modifier in self.modifiers: + variable = StringVar(self) + variable.set('') + self.modifier_vars.append(variable) + self.advanced = False + self.CreateWidgets() + self.LoadFinalKeyList() + self.withdraw() #hide while setting geometry + self.update_idletasks() + self.geometry( + "+%d+%d" % ( + parent.winfo_rootx() + + (parent.winfo_width()/2 - self.winfo_reqwidth()/2), + parent.winfo_rooty() + + ((parent.winfo_height()/2 - self.winfo_reqheight()/2) + if not _htest else 150) + ) ) #centre dialog over parent (or below htest box) + self.deiconify() #geometry set, unhide + self.wait_window() + + def CreateWidgets(self): + frameMain = Frame(self,borderwidth=2,relief=SUNKEN) + frameMain.pack(side=TOP,expand=TRUE,fill=BOTH) + frameButtons=Frame(self) + frameButtons.pack(side=BOTTOM,fill=X) + self.buttonOK = Button(frameButtons,text='OK', + width=8,command=self.OK) + self.buttonOK.grid(row=0,column=0,padx=5,pady=5) + self.buttonCancel = Button(frameButtons,text='Cancel', + width=8,command=self.Cancel) + self.buttonCancel.grid(row=0,column=1,padx=5,pady=5) + self.frameKeySeqBasic = Frame(frameMain) + self.frameKeySeqAdvanced = Frame(frameMain) + self.frameControlsBasic = Frame(frameMain) + self.frameHelpAdvanced = Frame(frameMain) + self.frameKeySeqAdvanced.grid(row=0,column=0,sticky=NSEW,padx=5,pady=5) + self.frameKeySeqBasic.grid(row=0,column=0,sticky=NSEW,padx=5,pady=5) + self.frameKeySeqBasic.lift() + self.frameHelpAdvanced.grid(row=1,column=0,sticky=NSEW,padx=5) + self.frameControlsBasic.grid(row=1,column=0,sticky=NSEW,padx=5) + self.frameControlsBasic.lift() + self.buttonLevel = Button(frameMain,command=self.ToggleLevel, + text='Advanced Key Binding Entry >>') + self.buttonLevel.grid(row=2,column=0,stick=EW,padx=5,pady=5) + labelTitleBasic = Label(self.frameKeySeqBasic, + text="New keys for '"+self.action+"' :") + labelTitleBasic.pack(anchor=W) + labelKeysBasic = Label(self.frameKeySeqBasic,justify=LEFT, + textvariable=self.keyString,relief=GROOVE,borderwidth=2) + labelKeysBasic.pack(ipadx=5,ipady=5,fill=X) + self.modifier_checkbuttons = {} + column = 0 + for modifier, variable in zip(self.modifiers, self.modifier_vars): + label = self.modifier_label.get(modifier, modifier) + check=Checkbutton(self.frameControlsBasic, + command=self.BuildKeyString, + text=label,variable=variable,onvalue=modifier,offvalue='') + check.grid(row=0,column=column,padx=2,sticky=W) + self.modifier_checkbuttons[modifier] = check + column += 1 + labelFnAdvice=Label(self.frameControlsBasic,justify=LEFT, + text=\ + "Select the desired modifier keys\n"+ + "above, and the final key from the\n"+ + "list on the right.\n\n" + + "Use upper case Symbols when using\n" + + "the Shift modifier. (Letters will be\n" + + "converted automatically.)") + labelFnAdvice.grid(row=1,column=0,columnspan=4,padx=2,sticky=W) + self.listKeysFinal=Listbox(self.frameControlsBasic,width=15,height=10, + selectmode=SINGLE) + self.listKeysFinal.bind('<ButtonRelease-1>',self.FinalKeySelected) + self.listKeysFinal.grid(row=0,column=4,rowspan=4,sticky=NS) + scrollKeysFinal=Scrollbar(self.frameControlsBasic,orient=VERTICAL, + command=self.listKeysFinal.yview) + self.listKeysFinal.config(yscrollcommand=scrollKeysFinal.set) + scrollKeysFinal.grid(row=0,column=5,rowspan=4,sticky=NS) + self.buttonClear=Button(self.frameControlsBasic, + text='Clear Keys',command=self.ClearKeySeq) + self.buttonClear.grid(row=2,column=0,columnspan=4) + labelTitleAdvanced = Label(self.frameKeySeqAdvanced,justify=LEFT, + text="Enter new binding(s) for '"+self.action+"' :\n"+ + "(These bindings will not be checked for validity!)") + labelTitleAdvanced.pack(anchor=W) + self.entryKeysAdvanced=Entry(self.frameKeySeqAdvanced, + textvariable=self.keyString) + self.entryKeysAdvanced.pack(fill=X) + labelHelpAdvanced=Label(self.frameHelpAdvanced,justify=LEFT, + text="Key bindings are specified using Tkinter keysyms as\n"+ + "in these samples: <Control-f>, <Shift-F2>, <F12>,\n" + "<Control-space>, <Meta-less>, <Control-Alt-Shift-X>.\n" + "Upper case is used when the Shift modifier is present!\n\n" + + "'Emacs style' multi-keystroke bindings are specified as\n" + + "follows: <Control-x><Control-y>, where the first key\n" + + "is the 'do-nothing' keybinding.\n\n" + + "Multiple separate bindings for one action should be\n"+ + "separated by a space, eg., <Alt-v> <Meta-v>." ) + labelHelpAdvanced.grid(row=0,column=0,sticky=NSEW) + + def SetModifiersForPlatform(self): + """Determine list of names of key modifiers for this platform. + + The names are used to build Tk bindings -- it doesn't matter if the + keyboard has these keys, it matters if Tk understands them. The + order is also important: key binding equality depends on it, so + config-keys.def must use the same ordering. + """ + if sys.platform == "darwin": + self.modifiers = ['Shift', 'Control', 'Option', 'Command'] + else: + self.modifiers = ['Control', 'Alt', 'Shift'] + self.modifier_label = {'Control': 'Ctrl'} # short name + + def ToggleLevel(self): + if self.buttonLevel.cget('text')[:8]=='Advanced': + self.ClearKeySeq() + self.buttonLevel.config(text='<< Basic Key Binding Entry') + self.frameKeySeqAdvanced.lift() + self.frameHelpAdvanced.lift() + self.entryKeysAdvanced.focus_set() + self.advanced = True + else: + self.ClearKeySeq() + self.buttonLevel.config(text='Advanced Key Binding Entry >>') + self.frameKeySeqBasic.lift() + self.frameControlsBasic.lift() + self.advanced = False + + def FinalKeySelected(self,event): + self.BuildKeyString() + + def BuildKeyString(self): + keyList = modifiers = self.GetModifiers() + finalKey = self.listKeysFinal.get(ANCHOR) + if finalKey: + finalKey = self.TranslateKey(finalKey, modifiers) + keyList.append(finalKey) + self.keyString.set('<' + string.join(keyList,'-') + '>') + + def GetModifiers(self): + modList = [variable.get() for variable in self.modifier_vars] + return [mod for mod in modList if mod] + + def ClearKeySeq(self): + self.listKeysFinal.select_clear(0,END) + self.listKeysFinal.yview(MOVETO, '0.0') + for variable in self.modifier_vars: + variable.set('') + self.keyString.set('') + + def LoadFinalKeyList(self): + #these tuples are also available for use in validity checks + self.functionKeys=('F1','F2','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.keys(): + key = translateDict[key] + if 'Shift' in modifiers and key in string.ascii_lowercase: + key = key.upper() + key = 'Key-' + key + return key + + def OK(self, event=None): + if self.advanced or self.KeysOK(): # doesn't check advanced string yet + self.result=self.keyString.get() + self.grab_release() + self.destroy() + + def Cancel(self, event=None): + self.result='' + self.grab_release() + self.destroy() + + def KeysOK(self): + '''Validity check on user's 'basic' keybinding selection. + + Doesn't check the string produced by the advanced dialog because + 'modifiers' isn't set. + + ''' + keys = self.keyString.get() + keys.strip() + finalKey = self.listKeysFinal.get(ANCHOR) + modifiers = self.GetModifiers() + # create a key sequence list for overlap check: + keySequence = keys.split() + keysOK = False + title = 'Key Sequence Error' + if not keys: + tkMessageBox.showerror(title=title, parent=self, + message='No keys specified.') + elif not keys.endswith('>'): + tkMessageBox.showerror(title=title, parent=self, + message='Missing the final Key') + elif (not modifiers + and finalKey not in self.functionKeys + self.moveKeys): + tkMessageBox.showerror(title=title, parent=self, + message='No modifier key(s) specified.') + elif (modifiers == ['Shift']) \ + and (finalKey not in + self.functionKeys + self.moveKeys + ('Tab', 'Space')): + msg = 'The shift modifier by itself may not be used with'\ + ' this key symbol.' + tkMessageBox.showerror(title=title, parent=self, message=msg) + elif keySequence in self.currentKeySequences: + msg = 'This key combination is already in use.' + tkMessageBox.showerror(title=title, parent=self, message=msg) + else: + keysOK = True + return keysOK + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(GetKeysDialog) diff --git a/contrib/tools/python/src/Lib/idlelib/macosxSupport.py b/contrib/tools/python/src/Lib/idlelib/macosxSupport.py new file mode 100644 index 00000000000..041d7008dcd --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/macosxSupport.py @@ -0,0 +1,237 @@ +""" +A number of functions that enhance IDLE on Mac OSX. +""" +import sys +import Tkinter +from os import path + + +import warnings + +def runningAsOSXApp(): + warnings.warn("runningAsOSXApp() is deprecated, use isAquaTk()", + DeprecationWarning, stacklevel=2) + return isAquaTk() + +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 + 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-idle>>', about_dialog) + root.bind('<<open-config-dialog>>', config_dialog) + root.createcommand('::tk::mac::ShowPreferences', config_dialog) + if flist: + root.bind('<<close-all-windows>>', 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', '<<about-idle>>'), + None, + ])) + tkversion = root.tk.eval('info patchlevel') + if tuple(map(int, tkversion.split('.'))) < (8, 4, 14): + # for earlier AquaTk versions, supply a Preferences menu item + Bindings.menudefs[0][1].append( + ('_Preferences....', '<<open-config-dialog>>'), + ) + if isCocoaTk(): + # replace default About dialog with About IDLE one + root.createcommand('tkAboutDialog', about_dialog) + # replace default "Help" item in Help menu + root.createcommand('::tk::mac::ShowHelp', help_dialog) + # remove redundant "IDLE Help" from menu + del 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/contrib/tools/python/src/Lib/idlelib/rpc.py b/contrib/tools/python/src/Lib/idlelib/rpc.py new file mode 100644 index 00000000000..43328e72184 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/rpc.py @@ -0,0 +1,597 @@ +"""RPC Implementation, originally written for the Python Idle IDE + +For security reasons, GvR requested that Idle's Python execution server process +connect to the Idle process, which listens for the connection. Since Idle has +only one client per server, this was not a limitation. + + +---------------------------------+ +-------------+ + | SocketServer.BaseRequestHandler | | SocketIO | + +---------------------------------+ +-------------+ + ^ | register() | + | | unregister()| + | +-------------+ + | ^ ^ + | | | + | + -------------------+ | + | | | + +-------------------------+ +-----------------+ + | RPCHandler | | RPCClient | + | [attribute of RPCServer]| | | + +-------------------------+ +-----------------+ + +The RPCServer handler class is expected to provide register/unregister methods. +RPCHandler inherits the mix-in class SocketIO, which provides these methods. + +See the Idle run.main() docstring for further information on how this was +accomplished in Idle. + +""" + +import sys +import os +import socket +import select +import SocketServer +import struct +import cPickle as pickle +import threading +import Queue +import traceback +import copy_reg +import types +import marshal + + +def unpickle_code(ms): + co = marshal.loads(ms) + assert isinstance(co, types.CodeType) + return co + +def pickle_code(co): + assert isinstance(co, types.CodeType) + ms = marshal.dumps(co) + return unpickle_code, (ms,) + +# XXX KBK 24Aug02 function pickling capability not used in Idle +# def unpickle_function(ms): +# return ms + +# def pickle_function(fn): +# assert isinstance(fn, type.FunctionType) +# return repr(fn) + +copy_reg.pickle(types.CodeType, pickle_code, unpickle_code) +# copy_reg.pickle(types.FunctionType, pickle_function, unpickle_function) + +BUFSIZE = 8*1024 +LOCALHOST = '127.0.0.1' + +class RPCServer(SocketServer.TCPServer): + + def __init__(self, addr, handlerclass=None): + if handlerclass is None: + handlerclass = RPCHandler + SocketServer.TCPServer.__init__(self, addr, handlerclass) + + def server_bind(self): + "Override TCPServer method, no bind() phase for connecting entity" + pass + + def server_activate(self): + """Override TCPServer method, connect() instead of listen() + + Due to the reversed connection, self.server_address is actually the + address of the Idle Client to which we are connecting. + + """ + self.socket.connect(self.server_address) + + def get_request(self): + "Override TCPServer method, return already connected socket" + return self.socket, self.server_address + + def handle_error(self, request, client_address): + """Override TCPServer method + + Error message goes to __stderr__. No error message if exiting + normally or socket raised EOF. Other exceptions not handled in + server code will cause os._exit. + + """ + try: + raise + except SystemExit: + raise + except: + erf = sys.__stderr__ + print>>erf, '\n' + '-'*40 + print>>erf, 'Unhandled server exception!' + print>>erf, 'Thread: %s' % threading.currentThread().getName() + print>>erf, 'Client Address: ', client_address + print>>erf, 'Request: ', repr(request) + traceback.print_exc(file=erf) + print>>erf, '\n*** Unrecoverable, server exiting!' + print>>erf, '-'*40 + os._exit(0) + +#----------------- end class RPCServer -------------------- + +objecttable = {} +request_queue = Queue.Queue(0) +response_queue = Queue.Queue(0) + + +class SocketIO(object): + + nextseq = 0 + + def __init__(self, sock, objtable=None, debugging=None): + self.sockthread = threading.currentThread() + if debugging is not None: + self.debugging = debugging + self.sock = sock + if objtable is None: + objtable = objecttable + self.objtable = objtable + self.responses = {} + self.cvars = {} + + def close(self): + sock = self.sock + self.sock = None + if sock is not None: + sock.close() + + def exithook(self): + "override for specific exit action" + os._exit(0) + + def debug(self, *args): + if not self.debugging: + return + s = self.location + " " + str(threading.currentThread().getName()) + for a in args: + s = s + " " + str(a) + print>>sys.__stderr__, s + + def register(self, oid, object): + self.objtable[oid] = object + + def unregister(self, oid): + try: + del self.objtable[oid] + except KeyError: + pass + + def localcall(self, seq, request): + self.debug("localcall:", request) + try: + how, (oid, methodname, args, kwargs) = request + except TypeError: + return ("ERROR", "Bad request format") + if oid not in self.objtable: + return ("ERROR", "Unknown object id: %r" % (oid,)) + obj = self.objtable[oid] + if methodname == "__methods__": + methods = {} + _getmethods(obj, methods) + return ("OK", methods) + if methodname == "__attributes__": + attributes = {} + _getattributes(obj, attributes) + return ("OK", attributes) + if not hasattr(obj, methodname): + return ("ERROR", "Unsupported method name: %r" % (methodname,)) + method = getattr(obj, methodname) + try: + if how == 'CALL': + ret = method(*args, **kwargs) + if isinstance(ret, RemoteObject): + ret = remoteref(ret) + return ("OK", ret) + elif how == 'QUEUE': + request_queue.put((seq, (method, args, kwargs))) + return("QUEUED", None) + else: + return ("ERROR", "Unsupported message type: %s" % how) + except SystemExit: + raise + except socket.error: + raise + except: + msg = "*** Internal Error: rpc.py:SocketIO.localcall()\n\n"\ + " Object: %s \n Method: %s \n Args: %s\n" + print>>sys.__stderr__, msg % (oid, method, args) + traceback.print_exc(file=sys.__stderr__) + return ("EXCEPTION", None) + + def remotecall(self, oid, methodname, args, kwargs): + self.debug("remotecall:asynccall: ", oid, methodname) + seq = self.asynccall(oid, methodname, args, kwargs) + return self.asyncreturn(seq) + + def remotequeue(self, oid, methodname, args, kwargs): + self.debug("remotequeue:asyncqueue: ", oid, methodname) + seq = self.asyncqueue(oid, methodname, args, kwargs) + return self.asyncreturn(seq) + + def asynccall(self, oid, methodname, args, kwargs): + request = ("CALL", (oid, methodname, args, kwargs)) + seq = self.newseq() + if threading.currentThread() != self.sockthread: + cvar = threading.Condition() + self.cvars[seq] = cvar + self.debug(("asynccall:%d:" % seq), oid, methodname, args, kwargs) + self.putmessage((seq, request)) + return seq + + def asyncqueue(self, oid, methodname, args, kwargs): + request = ("QUEUE", (oid, methodname, args, kwargs)) + seq = self.newseq() + if threading.currentThread() != self.sockthread: + cvar = threading.Condition() + self.cvars[seq] = cvar + self.debug(("asyncqueue:%d:" % seq), oid, methodname, args, kwargs) + self.putmessage((seq, request)) + return seq + + def asyncreturn(self, seq): + self.debug("asyncreturn:%d:call getresponse(): " % seq) + response = self.getresponse(seq, wait=0.05) + self.debug(("asyncreturn:%d:response: " % seq), response) + return self.decoderesponse(response) + + def decoderesponse(self, response): + how, what = response + if how == "OK": + return what + if how == "QUEUED": + return None + if how == "EXCEPTION": + self.debug("decoderesponse: EXCEPTION") + return None + if how == "EOF": + self.debug("decoderesponse: EOF") + self.decode_interrupthook() + return None + if how == "ERROR": + self.debug("decoderesponse: Internal ERROR:", what) + raise RuntimeError, what + raise SystemError, (how, what) + + def decode_interrupthook(self): + "" + raise EOFError + + def mainloop(self): + """Listen on socket until I/O not ready or EOF + + pollresponse() will loop looking for seq number None, which + never comes, and exit on EOFError. + + """ + try: + self.getresponse(myseq=None, wait=0.05) + except EOFError: + self.debug("mainloop:return") + return + + def getresponse(self, myseq, wait): + response = self._getresponse(myseq, wait) + if response is not None: + how, what = response + if how == "OK": + response = how, self._proxify(what) + return response + + def _proxify(self, obj): + if isinstance(obj, RemoteProxy): + return RPCProxy(self, obj.oid) + if isinstance(obj, types.ListType): + return map(self._proxify, obj) + # XXX Check for other types -- not currently needed + return obj + + def _getresponse(self, myseq, wait): + self.debug("_getresponse:myseq:", myseq) + if threading.currentThread() is self.sockthread: + # this thread does all reading of requests or responses + while 1: + response = self.pollresponse(myseq, wait) + if response is not None: + return response + else: + # wait for notification from socket handling thread + cvar = self.cvars[myseq] + cvar.acquire() + while myseq not in self.responses: + cvar.wait() + response = self.responses[myseq] + self.debug("_getresponse:%s: thread woke up: response: %s" % + (myseq, response)) + del self.responses[myseq] + del self.cvars[myseq] + cvar.release() + return response + + def newseq(self): + self.nextseq = seq = self.nextseq + 2 + return seq + + def putmessage(self, message): + self.debug("putmessage:%d:" % message[0]) + try: + s = pickle.dumps(message) + except pickle.PicklingError: + print >>sys.__stderr__, "Cannot pickle:", repr(message) + raise + s = struct.pack("<i", len(s)) + s + while len(s) > 0: + try: + r, w, x = select.select([], [self.sock], []) + n = self.sock.send(s[:BUFSIZE]) + except (AttributeError, TypeError): + raise IOError, "socket no longer exists" + s = s[n:] + + buffer = "" + bufneed = 4 + bufstate = 0 # meaning: 0 => reading count; 1 => reading data + + def pollpacket(self, wait): + self._stage0() + if len(self.buffer) < self.bufneed: + r, w, x = select.select([self.sock.fileno()], [], [], wait) + if len(r) == 0: + return None + try: + s = self.sock.recv(BUFSIZE) + except socket.error: + raise EOFError + if len(s) == 0: + raise EOFError + self.buffer += s + self._stage0() + return self._stage1() + + def _stage0(self): + if self.bufstate == 0 and len(self.buffer) >= 4: + s = self.buffer[:4] + self.buffer = self.buffer[4:] + self.bufneed = struct.unpack("<i", s)[0] + self.bufstate = 1 + + def _stage1(self): + if self.bufstate == 1 and len(self.buffer) >= self.bufneed: + packet = self.buffer[:self.bufneed] + self.buffer = self.buffer[self.bufneed:] + self.bufneed = 4 + self.bufstate = 0 + return packet + + def pollmessage(self, wait): + packet = self.pollpacket(wait) + if packet is None: + return None + try: + message = pickle.loads(packet) + except pickle.UnpicklingError: + print >>sys.__stderr__, "-----------------------" + print >>sys.__stderr__, "cannot unpickle packet:", repr(packet) + traceback.print_stack(file=sys.__stderr__) + print >>sys.__stderr__, "-----------------------" + raise + return message + + def pollresponse(self, myseq, wait): + """Handle messages received on the socket. + + Some messages received may be asynchronous 'call' or 'queue' requests, + and some may be responses for other threads. + + 'call' requests are passed to self.localcall() with the expectation of + immediate execution, during which time the socket is not serviced. + + 'queue' requests are used for tasks (which may block or hang) to be + processed in a different thread. These requests are fed into + request_queue by self.localcall(). Responses to queued requests are + taken from response_queue and sent across the link with the associated + sequence numbers. Messages in the queues are (sequence_number, + request/response) tuples and code using this module removing messages + from the request_queue is responsible for returning the correct + sequence number in the response_queue. + + pollresponse() will loop until a response message with the myseq + sequence number is received, and will save other responses in + self.responses and notify the owning thread. + + """ + while 1: + # send queued response if there is one available + try: + qmsg = response_queue.get(0) + except Queue.Empty: + pass + else: + seq, response = qmsg + message = (seq, ('OK', response)) + self.putmessage(message) + # poll for message on link + try: + message = self.pollmessage(wait) + if message is None: # socket not ready + return None + except EOFError: + self.handle_EOF() + return None + except AttributeError: + return None + seq, resq = message + how = resq[0] + self.debug("pollresponse:%d:myseq:%s" % (seq, myseq)) + # process or queue a request + if how in ("CALL", "QUEUE"): + self.debug("pollresponse:%d:localcall:call:" % seq) + response = self.localcall(seq, resq) + self.debug("pollresponse:%d:localcall:response:%s" + % (seq, response)) + if how == "CALL": + self.putmessage((seq, response)) + elif how == "QUEUE": + # don't acknowledge the 'queue' request! + pass + continue + # return if completed message transaction + elif seq == myseq: + return resq + # must be a response for a different thread: + else: + cv = self.cvars.get(seq, None) + # response involving unknown sequence number is discarded, + # probably intended for prior incarnation of server + if cv is not None: + cv.acquire() + self.responses[seq] = resq + cv.notify() + cv.release() + continue + + def handle_EOF(self): + "action taken upon link being closed by peer" + self.EOFhook() + self.debug("handle_EOF") + for key in self.cvars: + cv = self.cvars[key] + cv.acquire() + self.responses[key] = ('EOF', None) + cv.notify() + cv.release() + # call our (possibly overridden) exit function + self.exithook() + + def EOFhook(self): + "Classes using rpc client/server can override to augment EOF action" + pass + +#----------------- end class SocketIO -------------------- + +class RemoteObject(object): + # Token mix-in class + pass + +def remoteref(obj): + oid = id(obj) + objecttable[oid] = obj + return RemoteProxy(oid) + +class RemoteProxy(object): + + def __init__(self, oid): + self.oid = oid + +class RPCHandler(SocketServer.BaseRequestHandler, SocketIO): + + debugging = False + location = "#S" # Server + + def __init__(self, sock, addr, svr): + svr.current_handler = self ## cgt xxx + SocketIO.__init__(self, sock) + SocketServer.BaseRequestHandler.__init__(self, sock, addr, svr) + + def handle(self): + "handle() method required by SocketServer" + self.mainloop() + + def get_remote_proxy(self, oid): + return RPCProxy(self, oid) + +class RPCClient(SocketIO): + + debugging = False + location = "#C" # Client + + nextseq = 1 # Requests coming from the client are odd numbered + + def __init__(self, address, family=socket.AF_INET, type=socket.SOCK_STREAM): + self.listening_sock = socket.socket(family, type) + self.listening_sock.bind(address) + self.listening_sock.listen(1) + + def accept(self): + working_sock, address = self.listening_sock.accept() + if self.debugging: + print>>sys.__stderr__, "****** Connection request from ", address + if address[0] == LOCALHOST: + SocketIO.__init__(self, working_sock) + else: + print>>sys.__stderr__, "** Invalid host: ", address + raise socket.error + + def get_remote_proxy(self, oid): + return RPCProxy(self, oid) + +class RPCProxy(object): + + __methods = None + __attributes = None + + def __init__(self, sockio, oid): + self.sockio = sockio + self.oid = oid + + def __getattr__(self, name): + if self.__methods is None: + self.__getmethods() + if self.__methods.get(name): + return MethodProxy(self.sockio, self.oid, name) + if self.__attributes is None: + self.__getattributes() + if name in self.__attributes: + value = self.sockio.remotecall(self.oid, '__getattribute__', + (name,), {}) + return value + else: + raise AttributeError, name + + def __getattributes(self): + self.__attributes = self.sockio.remotecall(self.oid, + "__attributes__", (), {}) + + def __getmethods(self): + self.__methods = self.sockio.remotecall(self.oid, + "__methods__", (), {}) + +def _getmethods(obj, methods): + # Helper to get a list of methods from an object + # Adds names to dictionary argument 'methods' + for name in dir(obj): + attr = getattr(obj, name) + if hasattr(attr, '__call__'): + methods[name] = 1 + if type(obj) == types.InstanceType: + _getmethods(obj.__class__, methods) + if type(obj) == types.ClassType: + for super in obj.__bases__: + _getmethods(super, methods) + +def _getattributes(obj, attributes): + for name in dir(obj): + attr = getattr(obj, name) + if not hasattr(attr, '__call__'): + attributes[name] = 1 + +class MethodProxy(object): + + def __init__(self, sockio, oid, name): + self.sockio = sockio + self.oid = oid + self.name = name + + def __call__(self, *args, **kwargs): + value = self.sockio.remotecall(self.oid, self.name, args, kwargs) + return value + + +# XXX KBK 09Sep03 We need a proper unit test for this module. Previously +# existing test code was removed at Rev 1.27 (r34098). diff --git a/contrib/tools/python/src/Lib/idlelib/run.py b/contrib/tools/python/src/Lib/idlelib/run.py new file mode 100644 index 00000000000..518afabd1db --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/run.py @@ -0,0 +1,389 @@ +import sys +import linecache +import time +import socket +import traceback +import thread +import threading +import Queue + +from idlelib import CallTips +from idlelib import AutoComplete + +from idlelib import RemoteDebugger +from idlelib import RemoteObjectBrowser +from idlelib import StackViewer +from idlelib import rpc +from idlelib import PyShell +from idlelib import IOBinding + +import __main__ + +LOCALHOST = '127.0.0.1' + +import warnings + +def idle_showwarning_subproc( + message, category, filename, lineno, file=None, line=None): + """Show Idle-format warning after replacing warnings.showwarning. + + The only difference is the formatter called. + """ + if file is None: + file = sys.stderr + try: + file.write(PyShell.idle_formatwarning( + message, category, filename, lineno, line)) + except IOError: + pass # the file (probably stderr) is invalid - this warning gets lost. + +_warnings_showwarning = None + +def capture_warnings(capture): + "Replace warning.showwarning with idle_showwarning_subproc, or reverse." + + global _warnings_showwarning + if capture: + if _warnings_showwarning is None: + _warnings_showwarning = warnings.showwarning + warnings.showwarning = idle_showwarning_subproc + else: + if _warnings_showwarning is not None: + warnings.showwarning = _warnings_showwarning + _warnings_showwarning = None + +capture_warnings(True) + +# Thread shared globals: Establish a queue between a subthread (which handles +# the socket) and the main thread (which runs user code), plus global +# completion, exit and interruptable (the main thread) flags: + +exit_now = False +quitting = False +interruptable = False + +def main(del_exitfunc=False): + """Start the Python execution server in a subprocess + + In the Python subprocess, RPCServer is instantiated with handlerclass + MyHandler, which inherits register/unregister methods from RPCHandler via + the mix-in class SocketIO. + + When the RPCServer 'server' is instantiated, the TCPServer initialization + creates an instance of run.MyHandler and calls its handle() method. + handle() instantiates a run.Executive object, passing it a reference to the + 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 + call MyHandler(SocketIO) register/unregister methods via the reference to + register and unregister themselves. + + """ + global exit_now + global quitting + global no_exitfunc + no_exitfunc = del_exitfunc + #time.sleep(15) # test subprocess not responding + try: + assert(len(sys.argv) > 1) + port = int(sys.argv[-1]) + except: + print>>sys.stderr, "IDLE Subprocess: no IP port passed in sys.argv." + return + + capture_warnings(True) + sys.argv[:] = [""] + sockthread = threading.Thread(target=manage_socket, + name='SockThread', + args=((LOCALHOST, port),)) + sockthread.setDaemon(True) + sockthread.start() + while 1: + try: + if exit_now: + try: + exit() + except KeyboardInterrupt: + # exiting but got an extra KBI? Try again! + continue + try: + seq, request = rpc.request_queue.get(block=True, timeout=0.05) + except Queue.Empty: + continue + method, args, kwargs = request + ret = method(*args, **kwargs) + rpc.response_queue.put((seq, ret)) + except KeyboardInterrupt: + if quitting: + exit_now = True + continue + except SystemExit: + capture_warnings(False) + raise + except: + type, value, tb = sys.exc_info() + try: + print_exception() + rpc.response_queue.put((seq, None)) + except: + # Link didn't work, print same exception to __stderr__ + traceback.print_exception(type, value, tb, file=sys.__stderr__) + exit() + else: + continue + +def manage_socket(address): + for i in range(3): + time.sleep(i) + try: + server = MyRPCServer(address, MyHandler) + break + except socket.error as err: + print>>sys.__stderr__,"IDLE Subprocess: socket error: "\ + + err.args[1] + ", retrying...." + else: + print>>sys.__stderr__, "IDLE Subprocess: Connection to "\ + "IDLE GUI failed, exiting." + show_socket_error(err, address) + global exit_now + exit_now = True + return + server.handle_request() # A single request only + +def show_socket_error(err, address): + import Tkinter + import tkMessageBox + root = Tkinter.Tk() + fix_scaling(root) + root.withdraw() + if err.args[0] == 61: # connection refused + msg = "IDLE's subprocess can't connect to %s:%d. This may be due "\ + "to your personal firewall configuration. It is safe to "\ + "allow this internal connection because no data is visible on "\ + "external ports." % address + tkMessageBox.showerror("IDLE Subprocess Error", msg, parent=root) + else: + tkMessageBox.showerror("IDLE Subprocess Error", + "Socket Error: %s" % err.args[1], parent=root) + root.destroy() + +def print_exception(): + import linecache + linecache.checkcache() + flush_stdout() + efile = sys.stderr + typ, val, tb = excinfo = sys.exc_info() + sys.last_type, sys.last_value, sys.last_traceback = excinfo + tbe = traceback.extract_tb(tb) + print>>efile, '\nTraceback (most recent call last):' + exclude = ("run.py", "rpc.py", "threading.py", "Queue.py", + "RemoteDebugger.py", "bdb.py") + cleanup_traceback(tbe, exclude) + traceback.print_list(tbe, file=efile) + lines = traceback.format_exception_only(typ, val) + for line in lines: + print>>efile, line, + +def cleanup_traceback(tb, exclude): + "Remove excluded traces from beginning/end of tb; get cached lines" + orig_tb = tb[:] + while tb: + for rpcfile in exclude: + if tb[0][0].count(rpcfile): + break # found an exclude, break for: and delete tb[0] + else: + break # no excludes, have left RPC code, break while: + del tb[0] + while tb: + for rpcfile in exclude: + if tb[-1][0].count(rpcfile): + break + else: + break + del tb[-1] + if len(tb) == 0: + # exception was in IDLE internals, don't prune! + tb[:] = orig_tb[:] + print>>sys.stderr, "** IDLE Internal Exception: " + rpchandler = rpc.objecttable['exec'].rpchandler + for i in range(len(tb)): + fn, ln, nm, line = tb[i] + if nm == '?': + nm = "-toplevel-" + if fn.startswith("<pyshell#") and IOBinding.encoding != 'utf-8': + ln -= 1 # correction for coding cookie + if not line and fn.startswith("<pyshell#"): + line = rpchandler.remotecall('linecache', 'getline', + (fn, ln), {}) + tb[i] = fn, ln, nm, line + +def flush_stdout(): + try: + if sys.stdout.softspace: + sys.stdout.softspace = 0 + sys.stdout.write("\n") + except (AttributeError, EOFError): + pass + +def exit(): + """Exit subprocess, possibly after first deleting sys.exitfunc + + If config-main.cfg/.def 'General' 'delete-exitfunc' is True, then any + sys.exitfunc will be removed before exiting. (VPython support) + + """ + if no_exitfunc: + try: + del sys.exitfunc + except AttributeError: + pass + capture_warnings(False) + sys.exit(0) + + +def fix_scaling(root): + """Scale fonts on HiDPI displays.""" + import tkFont + scaling = float(root.tk.call('tk', 'scaling')) + if scaling > 1.4: + for name in tkFont.names(root): + font = tkFont.Font(root=root, name=name, exists=True) + size = int(font['size']) + if size < 0: + font['size'] = int(round(-0.75*size)) + + +class MyRPCServer(rpc.RPCServer): + + def handle_error(self, request, client_address): + """Override RPCServer method for IDLE + + Interrupt the MainThread and exit server if link is dropped. + + """ + global quitting + try: + raise + except SystemExit: + raise + except EOFError: + global exit_now + exit_now = True + thread.interrupt_main() + except: + erf = sys.__stderr__ + print>>erf, '\n' + '-'*40 + print>>erf, 'Unhandled server exception!' + print>>erf, 'Thread: %s' % threading.currentThread().getName() + print>>erf, 'Client Address: ', client_address + print>>erf, 'Request: ', repr(request) + traceback.print_exc(file=erf) + print>>erf, '\n*** Unrecoverable, server exiting!' + print>>erf, '-'*40 + quitting = True + thread.interrupt_main() + +class MyHandler(rpc.RPCHandler): + + def handle(self): + """Override base method""" + 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) + + # Keep a reference to stdin so that it won't try to exit IDLE if + # sys.stdin gets changed from within IDLE's shell. See issue17838. + self._keep_stdin = sys.stdin + + self.interp = self.get_remote_proxy("interp") + rpc.RPCHandler.getresponse(self, myseq=None, wait=0.05) + + def exithook(self): + "override SocketIO method - wait for MainThread to shut us down" + time.sleep(10) + + def EOFhook(self): + "Override SocketIO method - terminate wait on callback and exit thread" + global quitting + quitting = True + thread.interrupt_main() + + def decode_interrupthook(self): + "interrupt awakened thread" + global quitting + quitting = True + thread.interrupt_main() + + +class Executive(object): + + def __init__(self, rpchandler): + self.rpchandler = rpchandler + self.locals = __main__.__dict__ + self.calltip = CallTips.CallTips() + self.autocomplete = AutoComplete.AutoComplete() + + def runcode(self, code): + global interruptable + try: + self.usr_exc_info = None + interruptable = True + try: + exec code in self.locals + finally: + interruptable = False + except SystemExit: + # Scripts that raise SystemExit should just + # return to the interactive prompt + pass + except: + self.usr_exc_info = sys.exc_info() + if quitting: + exit() + print_exception() + jit = self.rpchandler.console.getvar("<<toggle-jit-stack-viewer>>") + if jit: + self.rpchandler.interp.open_remote_stack_viewer() + else: + flush_stdout() + + def interrupt_the_server(self): + if interruptable: + thread.interrupt_main() + + def start_the_debugger(self, gui_adap_oid): + return RemoteDebugger.start_debugger(self.rpchandler, gui_adap_oid) + + def stop_the_debugger(self, idb_adap_oid): + "Unregister the Idb Adapter. Link objects and Idb then subject to GC" + self.rpchandler.unregister(idb_adap_oid) + + def get_the_calltip(self, name): + return self.calltip.fetch_tip(name) + + def get_the_completion_list(self, what, mode): + return self.autocomplete.fetch_completions(what, mode) + + def stackviewer(self, flist_oid=None): + if self.usr_exc_info: + typ, val, tb = self.usr_exc_info + else: + return None + flist = None + if flist_oid is not None: + flist = self.rpchandler.get_remote_proxy(flist_oid) + while tb and tb.tb_frame.f_globals["__name__"] in ["rpc", "run"]: + tb = tb.tb_next + sys.last_type = typ + sys.last_value = val + item = StackViewer.StackTreeItem(flist, tb) + return RemoteObjectBrowser.remote_object_tree_item(item) + +capture_warnings(False) # Make sure turned off; see issue 18081 diff --git a/contrib/tools/python/src/Lib/idlelib/tabbedpages.py b/contrib/tools/python/src/Lib/idlelib/tabbedpages.py new file mode 100644 index 00000000000..0723d94537d --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/tabbedpages.py @@ -0,0 +1,498 @@ +"""An implementation of tabbed pages using only standard Tkinter. + +Originally developed for use in IDLE. Based on tabpage.py. + +Classes exported: +TabbedPageSet -- A Tkinter implementation of a tabbed-page widget. +TabSet -- A widget containing tabs (buttons) in one or more rows. + +""" +from Tkinter import * + +class InvalidNameError(Exception): pass +class AlreadyExistsError(Exception): pass + + +class TabSet(Frame): + """A widget containing tabs (buttons) in one or more rows. + + Only one tab may be selected at a time. + + """ + def __init__(self, page_set, select_command, + tabs=None, n_rows=1, max_tabs_per_row=5, + expand_tabs=False, **kw): + """Constructor arguments: + + select_command -- A callable which will be called when a tab is + selected. It is called with the name of the selected tab as an + argument. + + tabs -- A list of strings, the names of the tabs. Should be specified in + the desired tab order. The first tab will be the default and first + active tab. If tabs is None or empty, the TabSet will be initialized + empty. + + n_rows -- Number of rows of tabs to be shown. If n_rows <= 0 or is + None, then the number of rows will be decided by TabSet. See + _arrange_tabs() for details. + + max_tabs_per_row -- Used for deciding how many rows of tabs are needed, + when the number of rows is not constant. See _arrange_tabs() for + details. + + """ + Frame.__init__(self, page_set, **kw) + self.select_command = select_command + self.n_rows = n_rows + self.max_tabs_per_row = max_tabs_per_row + self.expand_tabs = expand_tabs + self.page_set = page_set + + self._tabs = {} + self._tab2row = {} + if tabs: + self._tab_names = list(tabs) + else: + self._tab_names = [] + self._selected_tab = None + self._tab_rows = [] + + self.padding_frame = Frame(self, height=2, + borderwidth=0, relief=FLAT, + background=self.cget('background')) + self.padding_frame.pack(side=TOP, fill=X, expand=False) + + self._arrange_tabs() + + def add_tab(self, tab_name): + """Add a new tab with the name given in tab_name.""" + if not tab_name: + raise InvalidNameError("Invalid Tab name: '%s'" % tab_name) + if tab_name in self._tab_names: + raise AlreadyExistsError("Tab named '%s' already exists" %tab_name) + + self._tab_names.append(tab_name) + self._arrange_tabs() + + def remove_tab(self, tab_name): + """Remove the tab named <tab_name>""" + if not tab_name in self._tab_names: + raise KeyError("No such Tab: '%s" % page_name) + + self._tab_names.remove(tab_name) + self._arrange_tabs() + + def set_selected_tab(self, tab_name): + """Show the tab named <tab_name> as the selected one""" + if tab_name == self._selected_tab: + return + if tab_name is not None and tab_name not in self._tabs: + raise KeyError("No such Tab: '%s" % page_name) + + # deselect the current selected tab + if self._selected_tab is not None: + self._tabs[self._selected_tab].set_normal() + self._selected_tab = None + + if tab_name is not None: + # activate the tab named tab_name + self._selected_tab = tab_name + tab = self._tabs[tab_name] + tab.set_selected() + # move the tab row with the selected tab to the bottom + tab_row = self._tab2row[tab] + tab_row.pack_forget() + tab_row.pack(side=TOP, fill=X, expand=0) + + def _add_tab_row(self, tab_names, expand_tabs): + if not tab_names: + return + + tab_row = Frame(self) + tab_row.pack(side=TOP, fill=X, expand=0) + self._tab_rows.append(tab_row) + + for tab_name in tab_names: + tab = TabSet.TabButton(tab_name, self.select_command, + tab_row, self) + if expand_tabs: + tab.pack(side=LEFT, fill=X, expand=True) + else: + tab.pack(side=LEFT) + self._tabs[tab_name] = tab + self._tab2row[tab] = tab_row + + # tab is the last one created in the above loop + tab.is_last_in_row = True + + def _reset_tab_rows(self): + while self._tab_rows: + tab_row = self._tab_rows.pop() + tab_row.destroy() + self._tab2row = {} + + def _arrange_tabs(self): + """ + Arrange the tabs in rows, in the order in which they were added. + + If n_rows >= 1, this will be the number of rows used. Otherwise the + number of rows will be calculated according to the number of tabs and + max_tabs_per_row. In this case, the number of rows may change when + adding/removing tabs. + + """ + # remove all tabs and rows + for tab_name in self._tabs.keys(): + self._tabs.pop(tab_name).destroy() + self._reset_tab_rows() + + if not self._tab_names: + return + + if self.n_rows is not None and self.n_rows > 0: + n_rows = self.n_rows + else: + # calculate the required number of rows + n_rows = (len(self._tab_names) - 1) // self.max_tabs_per_row + 1 + + # not expanding the tabs with more than one row is very ugly + expand_tabs = self.expand_tabs or n_rows > 1 + i = 0 # index in self._tab_names + for row_index in xrange(n_rows): + # calculate required number of tabs in this row + n_tabs = (len(self._tab_names) - i - 1) // (n_rows - row_index) + 1 + tab_names = self._tab_names[i:i + n_tabs] + i += n_tabs + self._add_tab_row(tab_names, expand_tabs) + + # re-select selected tab so it is properly displayed + selected = self._selected_tab + self.set_selected_tab(None) + if selected in self._tab_names: + self.set_selected_tab(selected) + + class TabButton(Frame): + """A simple tab-like widget.""" + + bw = 2 # borderwidth + + def __init__(self, name, select_command, tab_row, tab_set): + """Constructor arguments: + + name -- The tab's name, which will appear in its button. + + select_command -- The command to be called upon selection of the + tab. It is called with the tab's name as an argument. + + """ + Frame.__init__(self, tab_row, borderwidth=self.bw, relief=RAISED) + + self.name = name + self.select_command = select_command + self.tab_set = tab_set + self.is_last_in_row = False + + self.button = Radiobutton( + self, text=name, command=self._select_event, + padx=5, pady=1, takefocus=FALSE, indicatoron=FALSE, + highlightthickness=0, selectcolor='', borderwidth=0) + self.button.pack(side=LEFT, fill=X, expand=True) + + self._init_masks() + self.set_normal() + + def _select_event(self, *args): + """Event handler for tab selection. + + With TabbedPageSet, this calls TabbedPageSet.change_page, so that + selecting a tab changes the page. + + Note that this does -not- call set_selected -- it will be called by + TabSet.set_selected_tab, which should be called when whatever the + tabs are related to changes. + + """ + self.select_command(self.name) + return + + def set_selected(self): + """Assume selected look""" + self._place_masks(selected=True) + + def set_normal(self): + """Assume normal look""" + self._place_masks(selected=False) + + def _init_masks(self): + page_set = self.tab_set.page_set + background = page_set.pages_frame.cget('background') + # mask replaces the middle of the border with the background color + self.mask = Frame(page_set, borderwidth=0, relief=FLAT, + background=background) + # mskl replaces the bottom-left corner of the border with a normal + # left border + self.mskl = Frame(page_set, borderwidth=0, relief=FLAT, + background=background) + self.mskl.ml = Frame(self.mskl, borderwidth=self.bw, + relief=RAISED) + self.mskl.ml.place(x=0, y=-self.bw, + width=2*self.bw, height=self.bw*4) + # mskr replaces the bottom-right corner of the border with a normal + # right border + self.mskr = Frame(page_set, borderwidth=0, relief=FLAT, + background=background) + self.mskr.mr = Frame(self.mskr, borderwidth=self.bw, + relief=RAISED) + + def _place_masks(self, selected=False): + height = self.bw + if selected: + height += self.bw + + self.mask.place(in_=self, + relx=0.0, x=0, + rely=1.0, y=0, + relwidth=1.0, width=0, + relheight=0.0, height=height) + + self.mskl.place(in_=self, + relx=0.0, x=-self.bw, + rely=1.0, y=0, + relwidth=0.0, width=self.bw, + relheight=0.0, height=height) + + page_set = self.tab_set.page_set + if selected and ((not self.is_last_in_row) or + (self.winfo_rootx() + self.winfo_width() < + page_set.winfo_rootx() + page_set.winfo_width()) + ): + # for a selected tab, if its rightmost edge isn't on the + # rightmost edge of the page set, the right mask should be one + # borderwidth shorter (vertically) + height -= self.bw + + self.mskr.place(in_=self, + relx=1.0, x=0, + rely=1.0, y=0, + relwidth=0.0, width=self.bw, + relheight=0.0, height=height) + + self.mskr.mr.place(x=-self.bw, y=-self.bw, + width=2*self.bw, height=height + self.bw*2) + + # finally, lower the tab set so that all of the frames we just + # placed hide it + self.tab_set.lower() + +class TabbedPageSet(Frame): + """A Tkinter tabbed-pane widget. + + Constains set of 'pages' (or 'panes') with tabs above for selecting which + page is displayed. Only one page will be displayed at a time. + + Pages may be accessed through the 'pages' attribute, which is a dictionary + of pages, using the name given as the key. A page is an instance of a + subclass of Tk's Frame widget. + + The page widgets will be created (and destroyed when required) by the + TabbedPageSet. Do not call the page's pack/place/grid/destroy methods. + + Pages may be added or removed at any time using the add_page() and + remove_page() methods. + + """ + class Page(object): + """Abstract base class for TabbedPageSet's pages. + + Subclasses must override the _show() and _hide() methods. + + """ + uses_grid = False + + def __init__(self, page_set): + self.frame = Frame(page_set, borderwidth=2, relief=RAISED) + + def _show(self): + raise NotImplementedError + + def _hide(self): + raise NotImplementedError + + class PageRemove(Page): + """Page class using the grid placement manager's "remove" mechanism.""" + uses_grid = True + + def _show(self): + self.frame.grid(row=0, column=0, sticky=NSEW) + + def _hide(self): + self.frame.grid_remove() + + class PageLift(Page): + """Page class using the grid placement manager's "lift" mechanism.""" + uses_grid = True + + def __init__(self, page_set): + super(TabbedPageSet.PageLift, self).__init__(page_set) + self.frame.grid(row=0, column=0, sticky=NSEW) + self.frame.lower() + + def _show(self): + self.frame.lift() + + def _hide(self): + self.frame.lower() + + class PagePackForget(Page): + """Page class using the pack placement manager's "forget" mechanism.""" + def _show(self): + self.frame.pack(fill=BOTH, expand=True) + + def _hide(self): + self.frame.pack_forget() + + def __init__(self, parent, page_names=None, page_class=PageLift, + n_rows=1, max_tabs_per_row=5, expand_tabs=False, + **kw): + """Constructor arguments: + + page_names -- A list of strings, each will be the dictionary key to a + page's widget, and the name displayed on the page's tab. Should be + specified in the desired page order. The first page will be the default + and first active page. If page_names is None or empty, the + TabbedPageSet will be initialized empty. + + n_rows, max_tabs_per_row -- Parameters for the TabSet which will + manage the tabs. See TabSet's docs for details. + + page_class -- Pages can be shown/hidden using three mechanisms: + + * PageLift - All pages will be rendered one on top of the other. When + a page is selected, it will be brought to the top, thus hiding all + other pages. Using this method, the TabbedPageSet will not be resized + when pages are switched. (It may still be resized when pages are + added/removed.) + + * PageRemove - When a page is selected, the currently showing page is + hidden, and the new page shown in its place. Using this method, the + TabbedPageSet may resize when pages are changed. + + * PagePackForget - This mechanism uses the pack placement manager. + When a page is shown it is packed, and when it is hidden it is + unpacked (i.e. pack_forget). This mechanism may also cause the + TabbedPageSet to resize when the page is changed. + + """ + Frame.__init__(self, parent, **kw) + + self.page_class = page_class + self.pages = {} + self._pages_order = [] + self._current_page = None + self._default_page = None + + self.columnconfigure(0, weight=1) + self.rowconfigure(1, weight=1) + + self.pages_frame = Frame(self) + self.pages_frame.grid(row=1, column=0, sticky=NSEW) + if self.page_class.uses_grid: + self.pages_frame.columnconfigure(0, weight=1) + self.pages_frame.rowconfigure(0, weight=1) + + # the order of the following commands is important + self._tab_set = TabSet(self, self.change_page, n_rows=n_rows, + max_tabs_per_row=max_tabs_per_row, + expand_tabs=expand_tabs) + if page_names: + for name in page_names: + self.add_page(name) + self._tab_set.grid(row=0, column=0, sticky=NSEW) + + self.change_page(self._default_page) + + def add_page(self, page_name): + """Add a new page with the name given in page_name.""" + if not page_name: + raise InvalidNameError("Invalid TabPage name: '%s'" % page_name) + if page_name in self.pages: + raise AlreadyExistsError( + "TabPage named '%s' already exists" % page_name) + + self.pages[page_name] = self.page_class(self.pages_frame) + self._pages_order.append(page_name) + self._tab_set.add_tab(page_name) + + if len(self.pages) == 1: # adding first page + self._default_page = page_name + self.change_page(page_name) + + def remove_page(self, page_name): + """Destroy the page whose name is given in page_name.""" + if not page_name in self.pages: + raise KeyError("No such TabPage: '%s" % page_name) + + self._pages_order.remove(page_name) + + # handle removing last remaining, default, or currently shown page + if len(self._pages_order) > 0: + if page_name == self._default_page: + # set a new default page + self._default_page = self._pages_order[0] + else: + self._default_page = None + + if page_name == self._current_page: + self.change_page(self._default_page) + + self._tab_set.remove_tab(page_name) + page = self.pages.pop(page_name) + page.frame.destroy() + + def change_page(self, page_name): + """Show the page whose name is given in page_name.""" + if self._current_page == page_name: + return + if page_name is not None and page_name not in self.pages: + raise KeyError("No such TabPage: '%s'" % page_name) + + if self._current_page is not None: + self.pages[self._current_page]._hide() + self._current_page = None + + if page_name is not None: + self._current_page = page_name + self.pages[page_name]._show() + + self._tab_set.set_selected_tab(page_name) + +def _tabbed_pages(parent): + # test dialog + root=Tk() + width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) + root.geometry("+%d+%d"%(x, y + 175)) + root.title("Test tabbed pages") + tabPage=TabbedPageSet(root, page_names=['Foobar','Baz'], n_rows=0, + expand_tabs=False, + ) + tabPage.pack(side=TOP, expand=TRUE, fill=BOTH) + Label(tabPage.pages['Foobar'].frame, text='Foo', pady=20).pack() + Label(tabPage.pages['Foobar'].frame, text='Bar', pady=20).pack() + Label(tabPage.pages['Baz'].frame, text='Baz').pack() + entryPgName=Entry(root) + buttonAdd=Button(root, text='Add Page', + command=lambda:tabPage.add_page(entryPgName.get())) + buttonRemove=Button(root, text='Remove Page', + command=lambda:tabPage.remove_page(entryPgName.get())) + labelPgName=Label(root, text='name of page to add/remove:') + buttonAdd.pack(padx=5, pady=5) + buttonRemove.pack(padx=5, pady=5) + labelPgName.pack(padx=5) + entryPgName.pack(padx=5) + root.mainloop() + + +if __name__ == '__main__': + from idlelib.idle_test.htest import run + run(_tabbed_pages) diff --git a/contrib/tools/python/src/Lib/idlelib/textView.py b/contrib/tools/python/src/Lib/idlelib/textView.py new file mode 100644 index 00000000000..ec837f810c3 --- /dev/null +++ b/contrib/tools/python/src/Lib/idlelib/textView.py @@ -0,0 +1,97 @@ +"""Simple text browser for IDLE + +""" + +from Tkinter import * +import tkMessageBox + +class TextViewer(Toplevel): + """A simple text viewer dialog for IDLE + + """ + def __init__(self, parent, title, text, modal=True, _htest=False): + """Show the given text in a scrollable window with a 'close' button + + If modal option set to False, user can interact with other windows, + otherwise they will be unable to interact with other windows until + the textview window is closed. + + _htest - bool; change box location when running htest. + """ + Toplevel.__init__(self, parent) + self.configure(borderwidth=5) + # place dialog below parent if running htest + self.geometry("=%dx%d+%d+%d" % (750, 500, + parent.winfo_rootx() + 10, + parent.winfo_rooty() + (10 if not _htest else 100))) + #elguavas - config placeholders til config stuff completed + self.bg = '#ffffff' + self.fg = '#000000' + + self.CreateWidgets() + self.title(title) + self.protocol("WM_DELETE_WINDOW", self.Ok) + self.parent = parent + self.textView.focus_set() + #key bindings for this dialog + self.bind('<Return>',self.Ok) #dismiss dialog + self.bind('<Escape>',self.Ok) #dismiss dialog + self.textView.insert(0.0, text) + self.textView.config(state=DISABLED) + + self.is_modal = modal + if self.is_modal: + self.transient(parent) + self.grab_set() + self.wait_window() + + def CreateWidgets(self): + frameText = Frame(self, relief=SUNKEN, height=700) + frameButtons = Frame(self) + self.buttonOk = Button(frameButtons, text='Close', + command=self.Ok, takefocus=FALSE) + self.scrollbarView = Scrollbar(frameText, orient=VERTICAL, + takefocus=FALSE, highlightthickness=0) + self.textView = Text(frameText, wrap=WORD, highlightthickness=0, + fg=self.fg, bg=self.bg) + self.scrollbarView.config(command=self.textView.yview) + self.textView.config(yscrollcommand=self.scrollbarView.set) + self.buttonOk.pack() + self.scrollbarView.pack(side=RIGHT,fill=Y) + self.textView.pack(side=LEFT,expand=TRUE,fill=BOTH) + frameButtons.pack(side=BOTTOM,fill=X) + frameText.pack(side=TOP,expand=TRUE,fill=BOTH) + + def Ok(self, event=None): + if self.is_modal: + self.grab_release() + self.destroy() + + +def view_text(parent, title, text, modal=True): + return TextViewer(parent, title, text, modal) + +def view_file(parent, title, filename, encoding=None, modal=True): + try: + if encoding: + import codecs + textFile = codecs.open(filename, 'r') + else: + textFile = open(filename, 'r') + except IOError: + tkMessageBox.showerror(title='File Load Error', + message='Unable to load file %r .' % filename, + parent=parent) + except UnicodeDecodeError as err: + showerror(title='Unicode Decode Error', + message=str(err), + parent=parent) + else: + return view_text(parent, title, textFile.read(), modal) + + +if __name__ == '__main__': + import unittest + unittest.main('idlelib.idle_test.test_textview', verbosity=2, exit=False) + from idlelib.idle_test.htest import run + run(TextViewer) |