diff options
73 files changed, 12383 insertions, 0 deletions
diff --git a/Lib/idlelib/AutoExpand.py b/Lib/idlelib/AutoExpand.py new file mode 100644 index 0000000..0d57be4 --- /dev/null +++ b/Lib/idlelib/AutoExpand.py @@ -0,0 +1,92 @@ +import string +import re + +###$ event <<expand-word>> +###$ win <Alt-slash> +###$ unix <Alt-slash> + +class AutoExpand: + + keydefs = { + '<<expand-word>>': ['<Alt-slash>'], + } + + unix_keydefs = { + '<<expand-word>>': ['<Meta-slash>'], + } + + menudefs = [ + ('edit', [ + ('E_xpand word', '<<expand-word>>'), + ]), + ] + + wordchars = string.letters + string.digits + "_" + + def __init__(self, editwin): + self.text = editwin.text + self.text.wordlist = None # XXX what is this? + self.state = None + + def expand_word_event(self, event): + 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): + 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): + 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:] diff --git a/Lib/idlelib/AutoIndent.py b/Lib/idlelib/AutoIndent.py new file mode 100644 index 0000000..6d38481 --- /dev/null +++ b/Lib/idlelib/AutoIndent.py @@ -0,0 +1,554 @@ +import string +#from Tkinter import TclError +#import tkMessageBox +#import tkSimpleDialog + +###$ event <<newline-and-indent>> +###$ win <Key-Return> +###$ win <KP_Enter> +###$ unix <Key-Return> +###$ unix <KP_Enter> + +###$ event <<indent-region>> +###$ win <Control-bracketright> +###$ unix <Alt-bracketright> +###$ unix <Control-bracketright> + +###$ event <<dedent-region>> +###$ win <Control-bracketleft> +###$ unix <Alt-bracketleft> +###$ unix <Control-bracketleft> + +###$ event <<comment-region>> +###$ win <Alt-Key-3> +###$ unix <Alt-Key-3> + +###$ event <<uncomment-region>> +###$ win <Alt-Key-4> +###$ unix <Alt-Key-4> + +###$ event <<tabify-region>> +###$ win <Alt-Key-5> +###$ unix <Alt-Key-5> + +###$ event <<untabify-region>> +###$ win <Alt-Key-6> +###$ unix <Alt-Key-6> + +import PyParse + +class AutoIndent: + + menudefs = [ + ('format', [ # /s/edit/format dscherer@cmu.edu + None, + ('_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>>'), + ]), + ] + + keydefs = { + '<<smart-backspace>>': ['<Key-BackSpace>'], + '<<newline-and-indent>>': ['<Key-Return>', '<KP_Enter>'], + '<<smart-indent>>': ['<Key-Tab>'] + } + + windows_keydefs = { + '<<indent-region>>': ['<Control-bracketright>'], + '<<dedent-region>>': ['<Shift-Tab>', # dscherer@cmu.edu + '<Control-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>'], + } + + unix_keydefs = { + '<<indent-region>>': ['<Alt-bracketright>', + '<Meta-bracketright>', + '<Control-bracketright>'], + '<<dedent-region>>': ['<Alt-bracketleft>', + '<Meta-bracketleft>', + '<Control-bracketleft>'], + '<<comment-region>>': ['<Alt-Key-3>', '<Meta-Key-3>'], + '<<uncomment-region>>': ['<Alt-Key-4>', '<Meta-Key-4>'], + '<<tabify-region>>': ['<Alt-Key-5>', '<Meta-Key-5>'], + '<<untabify-region>>': ['<Alt-Key-6>', '<Meta-Key-6>'], + '<<toggle-tabs>>': ['<Alt-Key-t>'], + '<<change-indentwidth>>': ['<Alt-Key-u>'], + } + + # usetabs true -> literal tab characters are used by indent and + # dedent cmds, possibly mixed with spaces if + # indentwidth is not a multiple of tabwidth + # false -> tab characters are converted to spaces by indent + # and dedent cmds, and ditto TAB keystrokes + # indentwidth is the number of characters per logical indent level. + # 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. + usetabs = 1 + indentwidth = 4 + tabwidth = 8 # for IDLE use, must remain 8 until Tk is fixed + + # If context_use_ps1 is true, parsing searches back for a ps1 line; + # else searches for a popular (if, def, ...) Python stmt. + context_use_ps1 = 0 + + # 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. + num_context_lines = 50, 500, 5000000 + + def __init__(self, editwin): + self.editwin = editwin + self.text = editwin.text + + def config(self, **options): + for key, value in options.items(): + if key == 'usetabs': + self.usetabs = value + elif key == 'indentwidth': + self.indentwidth = value + elif key == 'tabwidth': + self.tabwidth = value + elif key == 'context_use_ps1': + self.context_use_ps1 = value + else: + raise KeyError, "bad option name: %s" % `key` + + # 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=1): + if guess and ispythonsource: + i = self.guess_indent() + if 2 <= i <= 8: + self.indentwidth = i + if self.indentwidth != self.tabwidth: + self.usetabs = 0 + + self.editwin.set_tabwidth(self.tabwidth) + + def smart_backspace_event(self, event): + text = self.text + first, last = self.editwin.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. + expand, tabwidth = string.expandtabs, self.tabwidth + have = len(expand(chars, tabwidth)) + assert have > 0 + want = int((have - 1) / self.indentwidth) * self.indentwidth + ncharsdeleted = 0 + while 1: + chars = chars[:-1] + ncharsdeleted = ncharsdeleted + 1 + have = len(expand(chars, 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 & return + # indent one level + text = self.text + first, last = self.editwin.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: + if self.usetabs: + pad = '\t' + else: + effective = len(string.expandtabs(prefix, + 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.editwin.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; just inject + # an empty line at the start + text.insert("insert linestart", '\n') + return "break" + indent = line[:i] + # strip whitespace before insert point + i = 0 + while line and line[-1] in " \t": + 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) + for context in self.num_context_lines: + startat = max(lno - context, 1) + startatindex = `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) + c = y.get_continuation_type() + if c != PyParse.C_NONE: + # The current stmt hasn't ended yet. + if c == PyParse.C_STRING: + # inside a string; 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 " + `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() + + auto_indent = newline_and_indent_event + + # Our editwin provides a 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.editwin.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() + 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() + for pos in range(len(lines)): + lines[pos] = string.expandtabs(lines[pos], tabwidth) + self.set_region(head, tail, chars, lines) + + def toggle_tabs_event(self, event): + if self.editwin.askyesno( + "Toggle tabs", + "Turn tabs " + ("on", "off")[self.usetabs] + "?", + parent=self.text): + self.usetabs = not self.usetabs + return "break" + + # XXX this isn't bound to anything -- see class 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.editwin.askinteger( + "Indent width", + "New indent width (1-16)", + parent=self.text, + initialvalue=self.indentwidth, + minvalue=1, + maxvalue=16) + if new and new != self.indentwidth: + self.indentwidth = new + return "break" + + def get_region(self): + text = self.text + first, last = self.editwin.get_selection_indices() + if first and last: + head = text.index(first + " linestart") + tail = text.index(last + "-1c lineend +1c") + else: + head = text.index("insert linestart") + tail = text.index("insert lineend +1c") + chars = text.get(head, tail) + lines = string.split(chars, "\n") + return head, tail, chars, lines + + def set_region(self, head, tail, chars, lines): + text = self.text + newchars = string.join(lines, "\n") + 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.editwin.askinteger( + "Tab width", + "Spaces per tab?", + parent=self.text, + initialvalue=self.tabwidth, + minvalue=1, + maxvalue=16) or self.tabwidth + + # 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: + + # .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 = `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: + # since we cut off the tokenizer early, we can trigger + # spurious errors + pass + finally: + _tokenize.tabsize = save_tabsize + return self.blkopenline, self.indentedline diff --git a/Lib/idlelib/Bindings.py b/Lib/idlelib/Bindings.py new file mode 100644 index 0000000..33c6c44 --- /dev/null +++ b/Lib/idlelib/Bindings.py @@ -0,0 +1,67 @@ +# This file defines the menu contents and key bindings. Note that +# 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 by the code here. This makes it possible to define the +# Debug menu here, which is only present in the PythonShell window. + +# changes by dscherer@cmu.edu: +# - Python shell moved to 'Run' menu +# - "Help" renamed to "IDLE Help" to distinguish from Python help. +# The distinction between the environment and the language is dim +# or nonexistent in a novice's mind. +# - Silly advice added + +import sys +import string +from keydefs import * + +menudefs = [ + # underscore prefixes character to underscore + ('file', [ + ('_New window', '<<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 Co_py As...', '<<save-copy-of-window-as-file>>'), + 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>>'), + ]), + ('run',[ + ('Python shell', '<<open-python-shell>>'), + ]), + ('debug', [ + ('_Go to file/line', '<<goto-file-line>>'), + ('_Stack viewer', '<<open-stack-viewer>>'), + ('!_Debugger', '<<toggle-debugger>>'), + ('!_Auto-open stack viewer', '<<toggle-jit-stack-viewer>>' ), + ]), + ('help', [ + ('_IDLE Help...', '<<help>>'), + ('Python _Documentation...', '<<python-docs>>'), + ('_Advice...', '<<good-advice>>'), + None, + ('_About IDLE...', '<<about-idle>>'), + ]), +] + +if sys.platform == 'win32': + default_keydefs = windows_keydefs +else: + default_keydefs = unix_keydefs + +del sys diff --git a/Lib/idlelib/CallTipWindow.py b/Lib/idlelib/CallTipWindow.py new file mode 100644 index 0000000..cbeab8c --- /dev/null +++ b/Lib/idlelib/CallTipWindow.py @@ -0,0 +1,71 @@ +# A CallTip window class for Tkinter/IDLE. +# After ToolTip.py, which uses ideas gleaned from PySol + +# Used by the CallTips IDLE extension. +import os +from Tkinter import * + +class CallTip: + + def __init__(self, widget): + self.widget = widget + self.tipwindow = None + self.id = None + self.x = self.y = 0 + + def showtip(self, text): + self.text = text + if self.tipwindow or not self.text: + return + self.widget.see("insert") + x, y, cx, cy = self.widget.bbox("insert") + x = x + self.widget.winfo_rootx() + 2 + y = y + cy + self.widget.winfo_rooty() + self.tipwindow = tw = Toplevel(self.widget) + tw.wm_overrideredirect(1) + tw.wm_geometry("+%d+%d" % (x, y)) + label = Label(tw, text=self.text, justify=LEFT, + background="#ffffe0", relief=SOLID, borderwidth=1, + font = self.widget['font']) + label.pack() + + def hidetip(self): + tw = self.tipwindow + self.tipwindow = None + if tw: + tw.destroy() + + +############################### +# +# Test Code +# +class container: # Conceptually an editor_window + def __init__(self): + root = Tk() + text = self.text = Text(root) + text.pack(side=LEFT, fill=BOTH, expand=1) + text.insert("insert", "string.split") + root.update() + self.calltip = CallTip(text) + + text.event_add("<<calltip-show>>", "(") + text.event_add("<<calltip-hide>>", ")") + text.bind("<<calltip-show>>", self.calltip_show) + text.bind("<<calltip-hide>>", self.calltip_hide) + + text.focus_set() + # root.mainloop() # not in idle + + def calltip_show(self, event): + self.calltip.showtip("Hello world") + + def calltip_hide(self, event): + self.calltip.hidetip() + +def main(): + # Test code + c=container() + +if __name__=='__main__': + main() diff --git a/Lib/idlelib/CallTips.py b/Lib/idlelib/CallTips.py new file mode 100644 index 0000000..04eccde --- /dev/null +++ b/Lib/idlelib/CallTips.py @@ -0,0 +1,190 @@ +# CallTips.py - An IDLE extension that provides "Call Tips" - ie, a floating window that +# displays parameter information as you open parens. + +import string +import sys +import types + +class CallTips: + + menudefs = [ + ] + + keydefs = { + '<<paren-open>>': ['<Key-parenleft>'], + '<<paren-close>>': ['<Key-parenright>'], + '<<check-calltip-cancel>>': ['<KeyRelease>'], + '<<calltip-cancel>>': ['<ButtonPress>', '<Key-Escape>'], + } + + windows_keydefs = { + } + + unix_keydefs = { + } + + def __init__(self, editwin): + self.editwin = editwin + self.text = editwin.text + self.calltip = None + if hasattr(self.text, "make_calltip_window"): + self._make_calltip_window = self.text.make_calltip_window + else: + self._make_calltip_window = self._make_tk_calltip_window + + def close(self): + self._make_calltip_window = None + + # Makes a Tk based calltip window. Used by IDLE, but not Pythonwin. + # See __init__ above for how this is used. + def _make_tk_calltip_window(self): + import CallTipWindow + return CallTipWindow.CallTip(self.text) + + def _remove_calltip_window(self): + if self.calltip: + self.calltip.hidetip() + self.calltip = None + + def paren_open_event(self, event): + self._remove_calltip_window() + arg_text = get_arg_text(self.get_object_at_cursor()) + if arg_text: + self.calltip_start = self.text.index("insert") + self.calltip = self._make_calltip_window() + self.calltip.showtip(arg_text) + return "" #so the event is handled normally. + + def paren_close_event(self, event): + # Now just hides, but later we should check if other + # paren'd expressions remain open. + self._remove_calltip_window() + return "" #so the event is handled normally. + + def check_calltip_cancel_event(self, event): + if self.calltip: + # If we have moved before the start of the calltip, + # or off the calltip line, then cancel the tip. + # (Later need to be smarter about multi-line, etc) + if self.text.compare("insert", "<=", self.calltip_start) or \ + self.text.compare("insert", ">", self.calltip_start + " lineend"): + self._remove_calltip_window() + return "" #so the event is handled normally. + + def calltip_cancel_event(self, event): + self._remove_calltip_window() + return "" #so the event is handled normally. + + def get_object_at_cursor(self, + wordchars="._" + string.uppercase + string.lowercase + string.digits): + # XXX - This needs to be moved to a better place + # so the "." attribute lookup code can also use it. + text = self.text + chars = text.get("insert linestart", "insert") + i = len(chars) + while i and chars[i-1] in wordchars: + i = i-1 + word = chars[i:] + if word: + # How is this for a hack! + import sys, __main__ + namespace = sys.modules.copy() + namespace.update(__main__.__dict__) + try: + return eval(word, namespace) + except: + pass + return None # Can't find an object. + +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 + +def get_arg_text(ob): + # Get a string describing the arguments for the given object. + argText = "" + if ob is not None: + argOffset = 0 + if type(ob)==types.ClassType: + # Look for the highest __init__ in the class chain. + fob = _find_constructor(ob) + if fob is None: + fob = lambda: None + else: + argOffset = 1 + elif type(ob)==types.MethodType: + # bit of a hack for methods - turn it into a function + # but we drop the "self" param. + fob = ob.im_func + argOffset = 1 + else: + fob = ob + # Try and build one for Python defined functions + if type(fob) in [types.FunctionType, types.LambdaType]: + try: + realArgs = fob.func_code.co_varnames[argOffset:fob.func_code.co_argcount] + defaults = fob.func_defaults or [] + defaults = list(map(lambda name: "=%s" % name, defaults)) + defaults = [""] * (len(realArgs)-len(defaults)) + defaults + items = map(lambda arg, dflt: arg+dflt, realArgs, defaults) + if fob.func_code.co_flags & 0x4: + items.append("...") + if fob.func_code.co_flags & 0x8: + items.append("***") + argText = string.join(items , ", ") + argText = "(%s)" % argText + except: + pass + # See if we can use the docstring + if hasattr(ob, "__doc__") and ob.__doc__: + pos = string.find(ob.__doc__, "\n") + if pos<0 or pos>70: pos=70 + if argText: argText = argText + "\n" + argText = argText + ob.__doc__[:pos] + + return argText + +################################################# +# +# Test code +# +if __name__=='__main__': + + def t1(): "()" + def t2(a, b=None): "(a, b=None)" + def t3(a, *args): "(a, ...)" + def t4(*args): "(...)" + def t5(a, *args): "(a, ...)" + def t6(a, b=None, *args, **kw): "(a, b=None, ..., ***)" + + class TC: + "(a=None, ...)" + def __init__(self, a=None, *b): "(a=None, ...)" + def t1(self): "()" + def t2(self, a, b=None): "(a, b=None)" + def t3(self, a, *args): "(a, ...)" + def t4(self, *args): "(...)" + def t5(self, a, *args): "(a, ...)" + def t6(self, a, b=None, *args, **kw): "(a, b=None, ..., ***)" + + def test( tests ): + failed=[] + for t in tests: + expected = t.__doc__ + "\n" + t.__doc__ + if get_arg_text(t) != expected: + failed.append(t) + print "%s - expected %s, but got %s" % (t, `expected`, `get_arg_text(t)`) + print "%d of %d tests failed" % (len(failed), len(tests)) + + tc = TC() + tests = t1, t2, t3, t4, t5, t6, \ + TC, tc.t1, tc.t2, tc.t3, tc.t4, tc.t5, tc.t6 + + test(tests) diff --git a/Lib/idlelib/ChangeLog b/Lib/idlelib/ChangeLog new file mode 100644 index 0000000..b853a34 --- /dev/null +++ b/Lib/idlelib/ChangeLog @@ -0,0 +1,1017 @@ +Tue Feb 15 18:08:19 2000 Guido van Rossum <guido@cnri.reston.va.us> + + * NEWS.txt: Notice status bar and stack viewer. + + * EditorWindow.py: Support for Moshe's status bar. + + * MultiStatusBar.py: Status bar code -- by Moshe Zadka. + + * OldStackViewer.py: + Adding the old stack viewer implementation back, for the debugger. + + * StackViewer.py: New stack viewer, uses a tree widget. + (XXX: the debugger doesn't yet use this.) + + * WindowList.py: + Correct a typo and remove an unqualified except that was hiding the error. + + * ClassBrowser.py: Add an XXX comment about the ClassBrowser AIP. + + * ChangeLog: Updated change log. + + * NEWS.txt: News update. Probably incomplete; what else is new? + + * README.txt: + Updated for pending IDLE 0.5 release (still very rough -- just getting + it out in a more convenient format than CVS). + + * TODO.txt: Tiny addition. + +Thu Sep 9 14:16:02 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * TODO.txt: A few new TODO entries. + +Thu Aug 26 23:06:22 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * Bindings.py: Add Python Documentation entry to Help menu. + + * EditorWindow.py: + Find the help.txt file relative to __file__ or ".", not in sys.path. + (Suggested by Moshe Zadka, but implemented differently.) + + Add <<python-docs>> event which, on Unix, brings up Netscape pointing + to http://www.python.doc/current/ (a local copy would be nice but its + location can't be predicted). Windows solution TBD. + +Wed Aug 11 14:55:43 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * TreeWidget.py: + Moshe noticed an inconsistency in his comment, so I'm rephrasing it to + be clearer. + + * TreeWidget.py: + Patch inspired by Moshe Zadka to search for the Icons directory in the + same directory as __file__, rather than searching for it along sys.path. + This works better when idle is a package. + +Thu Jul 15 13:11:02 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * TODO.txt: New wishes. + +Sat Jul 10 13:17:35 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * IdlePrefs.py: + Make the color for stderr red (i.e. the standard warning/danger/stop + color) rather than green. Suggested by Sam Schulenburg. + +Fri Jun 25 17:26:34 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * PyShell.py: Close debugger when closing. This may break a cycle. + + * Debugger.py: Break cycle on close. + + * ClassBrowser.py: Destroy the tree when closing. + + * TreeWidget.py: Add destroy() method to recursively destroy a tree. + + * PyShell.py: Extend _close() to break cycles. + Break some other cycles too (and destroy the root when done). + + * EditorWindow.py: + Add _close() method that does the actual cleanup (close() asks the + user what they want first if there's unsaved stuff, and may cancel). + It closes more than before. + + Add unload_extensions() method to unload all extensions; called from + _close(). It calls an extension's close() method if it has one. + + * Percolator.py: Add close() method that breaks cycles. + + * WidgetRedirector.py: Add unregister() method. + Unregister everything at closing. + Don't call close() in __del__, rely on explicit call to close(). + + * IOBinding.py, FormatParagraph.py, CallTips.py: + Add close() method that breaks a cycle. + +Fri Jun 11 15:03:00 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * AutoIndent.py, EditorWindow.py, FormatParagraph.py: + Tim Peters smart.patch: + + EditorWindow.py: + + + Added get_tabwidth & set_tabwidth "virtual text" methods, that get/set the + widget's view of what a tab means. + + + Moved TK_TABWIDTH_DEFAULT here from AutoIndent. + + + Renamed Mark's get_selection_index to get_selection_indices (sorry, Mark, + but the name was plain wrong <wink>). + + FormatParagraph.py: renamed use of get_selection_index. + + AutoIndent.py: + + + Moved TK_TABWIDTH_DEFAULT to EditorWindow. + + + Rewrote set_indentation_params to use new VTW get/set_tabwidth methods. + + + Changed smart_backspace_event to delete whitespace back to closest + preceding virtual tab stop or real character (note that this may require + inserting characters if backspacing over a tab!). + + + Nuked almost references to the selection tag, in favor of using + get_selection_indices. The sole exception is in set_region, for which no + "set_selection" abstraction has yet been agreed upon. + + + Had too much fun using the spiffy new features of the format-paragraph + cmd. + +Thu Jun 10 17:48:02 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * FormatParagraph.py: + Code by Mark Hammond to format paragraphs embedded in comments. + Read the comments (which I reformatted using the new feature :-) + for some limitations. + + * EditorWindow.py: + Added abstraction get_selection_index() (Mark Hammond). Also + reformatted some comment blocks to show off a cool feature I'm about + to check in next. + + * ClassBrowser.py: + Adapt to the new pyclbr's support of listing top-level functions. If + this functionality is not present (e.g. when used with a vintage + Python 1.5.2 installation) top-level functions are not listed. + + (Hmm... Any distribution of IDLE 0.5 should probably include a copy + of the new pyclbr.py!) + + * AutoIndent.py: + Fix off-by-one error in Tim's recent change to comment_region(): the + list of lines returned by get_region() contains an empty line at the + end representing the start of the next line, and this shouldn't be + commented out! + + * CallTips.py: + Mark Hammond writes: Here is another change that allows it to work for + class creation - tries to locate an __init__ function. Also updated + the test code to reflect your new "***" change. + + * CallTipWindow.py: + Mark Hammond writes: Tim's suggestion of copying the font for the + CallTipWindow from the text control makes sense, and actually makes + the control look better IMO. + +Wed Jun 9 20:34:57 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * CallTips.py: + Append "..." if the appropriate flag (for varargs) in co_flags is set. + Ditto "***" for kwargs. + +Tue Jun 8 13:06:07 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * ReplaceDialog.py: + Hmm... Tim didn't turn "replace all" into a single undo block. + I think I like it better if it os, so here. + + * ReplaceDialog.py: Tim Peters: made replacement atomic for undo/redo. + + * AutoIndent.py: Tim Peters: + + + Set usetabs=1. Editing pyclbr.py was driving me nuts <0.6 wink>. + usetabs=1 is the Emacs pymode default too, and thanks to indentwidth != + tabwidth magical usetabs disabling, new files are still created with tabs + turned off. The only implication is that if you open a file whose first + indent is a single tab, IDLE will now magically use tabs for that file (and + set indentwidth to 8). Note that the whole scheme doesn't work right for + PythonWin, though, since Windows users typically set tabwidth to 4; Mark + probably has to hide the IDLE algorithm from them (which he already knows). + + + Changed comment_region_event to stick "##" in front of every line. The + "holes" previously left on blank lines were visually confusing (made it + needlessly hard to figure out what to uncomment later). + +Mon Jun 7 15:38:40 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * TreeWidget.py, ObjectBrowser.py: + Remove unnecessary reference to pyclbr from test() code. + + * PyParse.py: Tim Peters: + + Smarter logic for finding a parse synch point. + + Does a half to a fifth the work in normal cases; don't notice the speedup, + but makes more breathing room for other extensions. + + Speeds terrible cases by at least a factor of 10. "Terrible" == e.g. you put + """ at the start of Tkinter.py, undo it, zoom to the bottom, and start + typing in code. Used to take about 8 seconds for ENTER to respond, now some + large fraction of a second. The new code gets indented correctly, despite + that it all remains "string colored" until the colorizer catches up (after + which, ENTER appears instantaneous again). + +Fri Jun 4 19:21:19 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * extend.py: Might as well enable CallTips by default. + If there are too many complaints I'll remove it again or fix it. + +Thu Jun 3 14:32:16 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * AutoIndent.py, EditorWindow.py, PyParse.py: + New offerings by Tim Peters; he writes: + + IDLE is now the first Python editor in the Universe not confused by my + doctest.py <wink>. + + As threatened, this defines IDLE's is_char_in_string function as a + method of EditorWindow. You just need to define one similarly in + whatever it is you pass as editwin to AutoIndent; looking at the + EditorWindow.py part of the patch should make this clear. + + * GrepDialog.py: Enclose pattern in quotes in status message. + + * CallTips.py: + Mark Hammond fixed some comments and improved the way the tip text is + constructed. + +Wed Jun 2 18:18:57 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * CallTips.py: + My fix to Mark's code: restore the universal check on <KeyRelease>. + Always cancel on <Key-Escape> or <ButtonPress>. + + * CallTips.py: + A version that Mark Hammond posted to the newsgroup. Has some newer + stuff for getting the tip. Had to fix the Key-( and Key-) events + for Unix. Will have to re-apply my patch for catching KeyRelease and + ButtonRelease events. + + * CallTipWindow.py, CallTips.py: + Call tips by Mark Hammond (plus tiny fix by me.) + + * IdleHistory.py: + Changes by Mark Hammond: (1) support optional output_sep argument to + the constructor so he can eliminate the sys.ps2 that PythonWin leaves + in the source; (2) remove duplicate history items. + + * AutoIndent.py: + Changes by Mark Hammond to allow using IDLE extensions in PythonWin as + well: make three dialog routines instance variables. + + * EditorWindow.py: + Change by Mark Hammond to allow using IDLE extensions in PythonWin as + well: make three dialog routines instance variables. + +Tue Jun 1 20:06:44 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * AutoIndent.py: Hah! A fix of my own to Tim's code! + Unix bindings for <<toggle-tabs>> and <<change-indentwidth>> were + missing, and somehow that meant the events were never generated, + even though they were in the menu. The new Unix bindings are now + the same as the Windows bindings (M-t and M-u). + + * AutoIndent.py, PyParse.py, PyShell.py: Tim Peters again: + + The new version (attached) is fast enough all the time in every real module + I have <whew!>. You can make it slow by, e.g., creating an open list with + 5,000 90-character identifiers (+ trailing comma) each on its own line, then + adding an item to the end -- but that still consumes less than a second on + my P5-166. Response time in real code appears instantaneous. + + Fixed some bugs. + + New feature: when hitting ENTER and the cursor is beyond the line's leading + indentation, whitespace is removed on both sides of the cursor; before + whitespace was removed only on the left; e.g., assuming the cursor is + between the comma and the space: + + def something(arg1, arg2): + ^ cursor to the left of here, and hit ENTER + arg2): # new line used to end up here + arg2): # but now lines up the way you expect + + New hack: AutoIndent has grown a context_use_ps1 Boolean config option, + defaulting to 0 (false) and set to 1 (only) by PyShell. Reason: handling + the fancy stuff requires looking backward for a parsing synch point; ps1 + lines are the only sensible thing to look for in a shell window, but are a + bad thing to look for in a file window (ps1 lines show up in my module + docstrings often). PythonWin's shell should set this true too. + + Persistent problem: strings containing def/class can still screw things up + completely. No improvement. Simplest workaround is on the user's head, and + consists of inserting e.g. + + def _(): pass + + (or any other def/class) after the end of the multiline string that's + screwing them up. This is especially irksome because IDLE's syntax coloring + is *not* confused, so when this happens the colors don't match the + indentation behavior they see. + + * AutoIndent.py: Tim Peters again: + + [Tim, after adding some bracket smarts to AutoIndent.py] + > ... + > What it can't possibly do without reparsing large gobs of text is + > suggest a reasonable indent level after you've *closed* a bracket + > left open on some previous line. + > ... + + The attached can, and actually fast enough to use -- most of the time. The + code is tricky beyond belief to achieve that, but it works so far; e.g., + + return len(string.expandtabs(str[self.stmt_start : + ^ indents to caret + i], + ^ indents to caret + self.tabwidth)) + 1 + ^ indents to caret + + It's about as smart as pymode now, wrt both bracket and backslash + continuation rules. It does require reparsing large gobs of text, and if it + happens to find something that looks like a "def" or "class" or sys.ps1 + buried in a multiline string, but didn't suck up enough preceding text to + see the start of the string, it's completely hosed. I can't repair that -- + it's just too slow to reparse from the start of the file all the time. + + AutoIndent has grown a new num_context_lines tuple attribute that controls + how far to look back, and-- like other params --this could/should be made + user-overridable at startup and per-file on the fly. + + * PyParse.py: New file by Tim Peters: + + One new file in the attached, PyParse.py. The LineStudier (whatever it was + called <wink>) class was removed from AutoIndent; PyParse subsumes its + functionality. + + * AutoIndent.py: Tim Peters keeps revising this module (more to come): + + Removed "New tabwidth" menu binding. + + Added "a tab means how many spaces?" dialog to block tabify and untabify. I + think prompting for this is good now: they're usually at-most-once-per-file + commands, and IDLE can't let them change tabwidth from the Tk default + anymore, so IDLE can no longer presume to have any idea what a tab means. + + Irony: for the purpose of keeping comments aligned via tabs, Tk's + non-default approach is much nicer than the Emacs/Notepad/Codewright/vi/etc + approach. + + * EditorWindow.py: + 1. Catch NameError on import (could be raised by case mismatch on Windows). + 2. No longer need to reset pyclbr cache and show watch cursor when calling + ClassBrowser -- the ClassBrowser takes care of pyclbr and the TreeWidget + takes care of the watch cursor. + 3. Reset the focus to the current window after error message about class + browser on buffer without filename. + + * Icons/minusnode.gif, Icons/plusnode.gif: Missed a few. + + * ClassBrowser.py, PathBrowser.py: Rewritten based on TreeWidget.py + + * ObjectBrowser.py: Object browser, based on TreeWidget.py. + + * TreeWidget.py: Tree widget done right. + + * ToolTip.py: As yet unused code for tool tips. + + * ScriptBinding.py: + Ensure sys.argv[0] is the script name on Run Script. + + * ZoomHeight.py: Move zoom height functionality to separate function. + + * Icons/folder.gif, Icons/openfolder.gif, Icons/python.gif, Icons/tk.gif: + A few icons used by ../TreeWidget.py and its callers. + + * AutoIndent.py: New version by Tim Peters improves block opening test. + +Fri May 21 04:46:17 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * Attic/History.py, PyShell.py: Rename History to IdleHistory. + Add isatty() to pseudo files. + + * StackViewer.py: Make initial stack viewer wider + + * TODO.txt: New wishes + + * AutoIndent.py, EditorWindow.py, PyShell.py: + Much improved autoindent and handling of tabs, + by Tim Peters. + +Mon May 3 15:49:52 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * AutoIndent.py, EditorWindow.py, FormatParagraph.py, UndoDelegator.py: + Tim Peters writes: + + I'm still unsure, but couldn't stand the virtual event trickery so tried a + different sin (adding undo_block_start/stop methods to the Text instance in + EditorWindow.py). Like it or not, it's efficient and works <wink>. Better + idea? + + Give the attached a whirl. Even if you hate the implementation, I think + you'll like the results. Think I caught all the "block edit" cmds, + including Format Paragraph, plus subtler ones involving smart indents and + backspacing. + + * WidgetRedirector.py: Tim Peters writes: + + [W]hile trying to dope out how redirection works, stumbled into two + possible glitches. In the first, it doesn't appear to make sense to try to + rename a command that's already been destroyed; in the second, the name + "previous" doesn't really bring to mind "ignore the previous value" <wink>. + +Fri Apr 30 19:39:25 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * __init__.py: Support for using idle as a package. + + * PathBrowser.py: + Avoid listing files more than once (e.g. foomodule.so has two hits: + once for foo + module.so, once for foomodule + .so). + +Mon Apr 26 22:20:38 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * ChangeLog, ColorDelegator.py, PyShell.py: Tim Peters strikes again: + + Ho ho ho -- that's trickier than it sounded! The colorizer is working with + "line.col" strings instead of Text marks, and the absolute coordinates of + the point of interest can change across the self.update call (voice of + baffled experience, when two quick backspaces no longer fooled it, but a + backspace followed by a quick ENTER did <wink>). + + Anyway, the attached appears to do the trick. CPU usage goes way up when + typing quickly into a long triple-quoted string, but the latency is fine for + me (a relatively fast typist on a relatively slow machine). Most of the + changes here are left over from reducing the # of vrbl names to help me + reason about the logic better; I hope the code is a *little* easier to + +Fri Apr 23 14:01:25 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * EditorWindow.py: + Provide full arguments to __import__ so it works in packagized IDLE. + +Thu Apr 22 23:20:17 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * help.txt: + Bunch of updates necessary due to recent changes; added docs for File + menu, command line and color preferences. + + * Bindings.py: Remove obsolete 'script' menu. + + * TODO.txt: Several wishes fulfilled. + + * OutputWindow.py: + Moved classes OnDemandOutputWindow and PseudoFile here, + from ScriptBinding.py where they are no longer needed. + + * ScriptBinding.py: + Mostly rewritten. Instead of the old Run module and Debug module, + there are two new commands: + + Import module (F5) imports or reloads the module and also adds its + name to the __main__ namespace. This gets executed in the PyShell + window under control of its debug settings. + + Run script (Control-F5) is similar but executes the contents of the + file directly in the __main__ namespace. + + * PyShell.py: Nits: document use of $IDLESTARTUP; display idle version + + * idlever.py: New version to celebrate new command line + + * OutputWindow.py: Added flush(), for completeness. + + * PyShell.py: + A lot of changes to make the command line more useful. You can now do: + idle.py -e file ... -- to edit files + idle.py script arg ... -- to run a script + idle.py -c cmd arg ... -- to run a command + Other options, see also the usage message (also new!) for more details: + -d -- enable debugger + -s -- run $IDLESTARTUP or $PYTHONSTARTUP + -t title -- set Python Shell window's title + sys.argv is set accordingly, unless -e is used. + sys.path is absolutized, and all relevant paths are inserted into it. + + Other changes: + - the environment in which commands are executed is now the + __main__ module + - explicitly save sys.stdout etc., don't restore from sys.__stdout__ + - new interpreter methods execsource(), execfile(), stuffsource() + - a few small nits + + * TODO.txt: + Some more TODO items. Made up my mind about command line args, + Run/Import, __main__. + + * ColorDelegator.py: + Super-elegant patch by Tim Peters that speeds up colorization + dramatically (up to 15 times he claims). Works by reading more than + one line at a time, up to 100-line chunks (starting with one line and + then doubling up to the limit). On a typical machine (e.g. Tim's + P5-166) this doesn't reduce interactive responsiveness in a noticeable + way. + +Wed Apr 21 15:49:34 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * ColorDelegator.py: + Patch by Tim Peters to speed up colorizing of big multiline strings. + +Tue Apr 20 17:32:52 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * extend.txt: + For an event 'foo-bar', the corresponding method must be called + foo_bar_event(). Therefore, fix the references to zoom_height() in + the example. + + * IdlePrefs.py: Restored the original IDLE color scheme. + + * PyShell.py, IdlePrefs.py, ColorDelegator.py, EditorWindow.py: + Color preferences code by Loren Luke (massaged by me somewhat) + + * SearchEngine.py: + Patch by Mark Favas: it fixes the search engine behaviour where an + unsuccessful search wraps around and re-searches that part of the file + between the start of the search and the end of the file - only really + an issue for very large files, but... (also removes a redundant + m.span() call). + +Mon Apr 19 16:26:02 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * TODO.txt: A few wishes are now fulfilled. + + * AutoIndent.py: Tim Peters implements some of my wishes: + + o Makes the tab key intelligently insert spaces when appropriate + (see Help list banter twixt David Ascher and me; idea stolen from + every other editor on earth <wink>). + + o newline_and_indent_event trims trailing whitespace on the old + line (pymode and Codewright). + + o newline_and_indent_event no longer fooled by trailing whitespace or + comment after ":" (pymode, PTUI). + + o newline_and_indent_event now reduces the new line's indentation after + return, break, continue, raise and pass stmts (pymode). + + The last two are easy to fool in the presence of strings & + continuations, but pymode requires Emacs's high-powered C parsing + functions to avoid that in finite time. + +====================================================================== + Python release 1.5.2c1, IDLE version 0.4 +====================================================================== + +Wed Apr 7 18:41:59 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * README.txt, NEWS.txt: New version. + + * idlever.py: Version bump awaiting impending new release. + (Not much has changed :-( ) + +Mon Mar 29 14:52:28 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * ScriptBinding.py, PyShell.py: + At Tim Peters' recommendation, add a dummy flush() method to + PseudoFile. + +Thu Mar 11 23:21:23 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * PathBrowser.py: Don't crash when sys.path contains an empty string. + + * Attic/Outline.py: This file was never supposed to be part of IDLE. + + * PathBrowser.py: + - Don't crash in the case where a superclass is a string instead of a + pyclbr.Class object; this can happen when the superclass is + unrecognizable (to pyclbr), e.g. when module renaming is used. + + - Show a watch cursor when calling pyclbr (since it may take a while + recursively parsing imported modules!). + +Wed Mar 10 05:18:02 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * EditorWindow.py, Bindings.py: Add PathBrowser to File module + + * PathBrowser.py: "Path browser" - 4 scrolled lists displaying: + directories on sys.path + modules in selected directory + classes in selected module + methods of selected class + + Sinlge clicking in a directory, module or class item updates the next + column with info about the selected item. Double clicking in a + module, class or method item opens the file (and selects the clicked + item if it is a class or method). + + I guess eventually I should be using a tree widget for this, but the + ones I've seen don't work well enough, so for now I use the old + Smalltalk or NeXT style multi-column hierarchical browser. + + * MultiScrolledLists.py: + New utility: multiple scrolled lists in parallel + + * ScrolledList.py: - White background. + - Display "(None)" (or text of your choosing) when empty. + - Don't set the focus. + +====================================================================== + Python release 1.5.2b2, IDLE version 0.3 +====================================================================== + +Wed Feb 17 22:47:41 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * NEWS.txt: News in 0.3. + + * README.txt, idlever.py: Bump version to 0.3. + + * EditorWindow.py: + After all, we don't need to call the callbacks ourselves! + + * WindowList.py: + When deleting, call the callbacks *after* deleting the window from our list! + + * EditorWindow.py: + Fix up the Windows menu via the new callback mechanism instead of + depending on menu post commands (which don't work when the menu is + torn off). + + * WindowList.py: + Support callbacks to patch up Windows menus everywhere. + + * ChangeLog: Oh, why not. Checking in the Emacs-generated change log. + +Tue Feb 16 22:34:17 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * ScriptBinding.py: + Only pop up the stack viewer when requested in the Debug menu. + +Mon Feb 8 22:27:49 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * WindowList.py: Don't crash if a window no longer exists. + + * TODO.txt: Restructured a bit. + +Mon Feb 1 23:06:17 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * PyShell.py: Add current dir or paths of file args to sys.path. + + * Debugger.py: Add canonic() function -- for brand new bdb.py feature. + + * StackViewer.py: Protect against accessing an empty stack. + +Fri Jan 29 20:44:45 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * ZoomHeight.py: + Use only the height to decide whether to zoom in or out. + +Thu Jan 28 22:24:30 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * EditorWindow.py, FileList.py: + Make sure the Tcl variables are shared between windows. + + * PyShell.py, EditorWindow.py, Bindings.py: + Move menu/key binding code from Bindings.py to EditorWindow.py, + with changed APIs -- it makes much more sense there. + Also add a new feature: if the first character of a menu label is + a '!', it gets a checkbox. Checkboxes are bound to Boolean Tcl variables + that can be accessed through the new getvar/setvar/getrawvar API; + the variable is named after the event to which the menu is bound. + + * Debugger.py: Add Quit button to the debugger window. + + * SearchDialog.py: + When find_again() finds exactly the current selection, it's a failure. + + * idle.py, Attic/idle: Rename idle -> idle.py + +Mon Jan 18 15:18:57 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * EditorWindow.py, WindowList.py: Only deiconify when iconic. + + * TODO.txt: Misc + +Tue Jan 12 22:14:34 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * testcode.py, Attic/test.py: + Renamed test.py to testcode.py so one can import Python's + test package from inside IDLE. (Suggested by Jack Jansen.) + + * EditorWindow.py, ColorDelegator.py: + Hack to close a window that is colorizing. + + * Separator.py: Vladimir Marangozov's patch: + The separator dances too much and seems to jump by arbitrary amounts + in arbitrary directions when I try to move it for resizing the frames. + This patch makes it more quiet. + +Mon Jan 11 14:52:40 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * TODO.txt: Some requests have been fulfilled. + + * EditorWindow.py: + Set the cursor to a watch when opening the class browser (which may + take quite a while, browsing multiple files). + + Newer, better center() -- but assumes no wrapping. + + * SearchBinding.py: + Got rid of debug print statement in goto_line_event(). + + * ScriptBinding.py: + I think I like it better if it prints the traceback even when it displays + the stack viewer. + + * Debugger.py: Bind ESC to close-window. + + * ClassBrowser.py: Use a HSeparator between the classes and the items. + Make the list of classes wider by default (40 chars). + Bind ESC to close-window. + + * Separator.py: + Separator classes (draggable divider between two panes). + +Sat Jan 9 22:01:33 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * WindowList.py: + Don't traceback when wakeup() is called when the window has been destroyed. + This can happen when a torn-of Windows menu references closed windows. + And Tim Peters claims that the Windows menu is his favorite to tear off... + + * EditorWindow.py: Allow tearing off of the Windows menu. + + * StackViewer.py: Close on ESC. + + * help.txt: Updated a bunch of things (it was mostly still 0.1!) + + * extend.py: Added ScriptBinding to standard bindings. + + * ScriptBinding.py: + This now actually works. See doc string. It can run a module (i.e. + import or reload) or debug it (same with debugger control). Output + goes to a fresh output window, only created when needed. + +====================================================================== + Python release 1.5.2b1, IDLE version 0.2 +====================================================================== + +Fri Jan 8 17:26:02 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * README.txt, NEWS.txt: What's new in this release. + + * Bindings.py, PyShell.py: + Paul Prescod's patches to allow the stack viewer to pop up when a + traceback is printed. + +Thu Jan 7 00:12:15 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * FormatParagraph.py: + Change paragraph width limit to 70 (like Emacs M-Q). + + * README.txt: + Separating TODO from README. Slight reformulation of features. No + exact release date. + + * TODO.txt: Separating TODO from README. + +Mon Jan 4 21:19:09 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * FormatParagraph.py: + Hm. There was a boundary condition error at the end of the file too. + + * SearchBinding.py: Hm. Add Unix binding for replace, too. + + * keydefs.py: Ran eventparse.py again. + + * FormatParagraph.py: Added Unix Meta-q key binding; + fix find_paragraph when at start of file. + + * AutoExpand.py: Added Meta-/ binding for Unix as alt for Alt-/. + + * SearchBinding.py: + Add unix binding for grep (otherwise the menu entry doesn't work!) + + * ZoomHeight.py: Adjusted Unix height to work with fvwm96. :=( + + * GrepDialog.py: Need to import sys! + + * help.txt, extend.txt, README.txt: Formatted some paragraphs + + * extend.py, FormatParagraph.py: + Add new extension to reformat a (text) paragraph. + + * ZoomHeight.py: Typo in Win specific height setting. + +Sun Jan 3 00:47:35 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * AutoIndent.py: Added something like Tim Peters' backspace patch. + + * ZoomHeight.py: Adapted to Unix (i.e., more hardcoded constants). + +Sat Jan 2 21:28:54 1999 Guido van Rossum <guido@cnri.reston.va.us> + + * keydefs.py, idlever.py, idle.pyw, idle.bat, help.txt, extend.txt, extend.py, eventparse.py, ZoomHeight.py, WindowList.py, UndoDelegator.py, StackViewer.py, SearchEngine.py, SearchDialogBase.py, SearchDialog.py, ScrolledList.py, SearchBinding.py, ScriptBinding.py, ReplaceDialog.py, Attic/README, README.txt, PyShell.py, Attic/PopupMenu.py, OutputWindow.py, IOBinding.py, Attic/HelpWindow.py, History.py, GrepDialog.py, FileList.py, FrameViewer.py, EditorWindow.py, Debugger.py, Delegator.py, ColorDelegator.py, Bindings.py, ClassBrowser.py, AutoExpand.py, AutoIndent.py: + Checking in IDLE 0.2. + + Much has changed -- too much, in fact, to write down. + The big news is that there's a standard way to write IDLE extensions; + see extend.txt. Some sample extensions have been provided, and + some existing code has been converted to extensions. Probably the + biggest new user feature is a new search dialog with more options, + search and replace, and even search in files (grep). + + This is exactly as downloaded from my laptop after returning + from the holidays -- it hasn't even been tested on Unix yet. + +Fri Dec 18 15:52:54 1998 Guido van Rossum <guido@cnri.reston.va.us> + + * FileList.py, ClassBrowser.py: + Fix the class browser to work even when the file is not on sys.path. + +Tue Dec 8 20:39:36 1998 Guido van Rossum <guido@cnri.reston.va.us> + + * Attic/turtle.py: Moved to Python 1.5.2/Lib + +Fri Nov 27 03:19:20 1998 Guido van Rossum <guido@cnri.reston.va.us> + + * help.txt: Typo + + * EditorWindow.py, FileList.py: Support underlining of menu labels + + * Bindings.py: + New approach, separate tables for menus (platform-independent) and key + definitions (platform-specific), and generating accelerator strings + automatically from the key definitions. + +Mon Nov 16 18:37:42 1998 Guido van Rossum <guido@cnri.reston.va.us> + + * Attic/README: Clarify portability and main program. + + * Attic/README: Added intro for 0.1 release and append Grail notes. + +Mon Oct 26 18:49:00 1998 Guido van Rossum <guido@cnri.reston.va.us> + + * Attic/turtle.py: root is now a global called _root + +Sat Oct 24 16:38:38 1998 Guido van Rossum <guido@cnri.reston.va.us> + + * Attic/turtle.py: Raise the root window on reset(). + Different action on WM_DELETE_WINDOW is more likely to do the right thing, + allowing us to destroy old windows. + + * Attic/turtle.py: + Split the goto() function in two: _goto() is the internal one, + using Canvas coordinates, and goto() uses turtle coordinates + and accepts variable argument lists. + + * Attic/turtle.py: Cope with destruction of the window + + * Attic/turtle.py: Turtle graphics + + * Debugger.py: Use of Breakpoint class should be bdb.Breakpoint. + +Mon Oct 19 03:33:40 1998 Guido van Rossum <guido@cnri.reston.va.us> + + * SearchBinding.py: + Speed up the search a bit -- don't drag a mark around... + + * PyShell.py: + Change our special entries from <console#N> to <pyshell#N>. + Patch linecache.checkcache() to keep our special entries alive. + Add popup menu to all editor windows to set a breakpoint. + + * Debugger.py: + Use and pass through the 'force' flag to set_dict() where appropriate. + Default source and globals checkboxes to false. + Don't interact in user_return(). + Add primitive set_breakpoint() method. + + * ColorDelegator.py: + Raise priority of 'sel' tag so its foreground (on Windows) will take + priority over text colorization (which on Windows is almost the + same color as the selection background). + + Define a tag and color for breakpoints ("BREAK"). + + * Attic/PopupMenu.py: Disable "Open stack viewer" and "help" commands. + + * StackViewer.py: + Add optional 'force' argument (default 0) to load_dict(). + If set, redo the display even if it's the same dict. + +Fri Oct 16 21:10:12 1998 Guido van Rossum <guido@cnri.reston.va.us> + + * StackViewer.py: Do nothing when loading the same dict as before. + + * PyShell.py: Details for debugger interface. + + * Debugger.py: + Restructured and more consistent. Save checkboxes across instantiations. + + * EditorWindow.py, Attic/README, Bindings.py: + Get rid of conflicting ^X binding. Use ^W. + + * Debugger.py, StackViewer.py: + Debugger can now show local and global variables. + + * Debugger.py: Oops + + * Debugger.py, PyShell.py: Better debugger support (show stack etc). + + * Attic/PopupMenu.py: Follow renames in StackViewer module + + * StackViewer.py: + Rename classes to StackViewer (the widget) and StackBrowser (the toplevel). + + * ScrolledList.py: Add close() method + + * EditorWindow.py: Clarify 'Open Module' dialog text + + * StackViewer.py: Restructured into a browser and a widget. + +Thu Oct 15 23:27:08 1998 Guido van Rossum <guido@cnri.reston.va.us> + + * ClassBrowser.py, ScrolledList.py: + Generalized the scrolled list which is the base for the class and + method browser into a separate class in its own module. + + * Attic/test.py: Cosmetic change + + * Debugger.py: Don't show function name if there is none + +Wed Oct 14 03:43:05 1998 Guido van Rossum <guido@cnri.reston.va.us> + + * Debugger.py, PyShell.py: Polish the Debugger GUI a bit. + Closing it now also does the right thing. + +Tue Oct 13 23:51:13 1998 Guido van Rossum <guido@cnri.reston.va.us> + + * Debugger.py, PyShell.py, Bindings.py: + Ad primitive debugger interface (so far it will step and show you the + source, but it doesn't yet show the stack). + + * Attic/README: Misc + + * StackViewer.py: Whoops -- referenced self.top before it was set. + + * help.txt: Added history and completion commands. + + * help.txt: Updated + + * FileList.py: Add class browser functionality. + + * StackViewer.py: + Add a close() method and bind to WM_DELETE_WINDOW protocol + + * PyShell.py: Clear the linecache before printing a traceback + + * Bindings.py: Added class browser binding. + + * ClassBrowser.py: Much improved, much left to do. + + * PyShell.py: Make the return key do what I mean more often. + + * ClassBrowser.py: + Adding the beginnings of a Class browser. Incomplete, yet. + + * EditorWindow.py, Bindings.py: + Add new command, "Open module". You select or type a module name, + and it opens the source. + +Mon Oct 12 23:59:27 1998 Guido van Rossum <guido@cnri.reston.va.us> + + * PyShell.py: Subsume functionality from Popup menu in Debug menu. + Other stuff so the PyShell window can be resurrected from the Windows menu. + + * FileList.py: Get rid of PopUp menu. + Create a simple Windows menu. (Imperfect when Untitled windows exist.) + Add wakeup() method: deiconify, raise, focus. + + * EditorWindow.py: Generalize menu creation. + + * Bindings.py: Add Debug and Help menu items. + + * EditorWindow.py: Added a menu bar to every window. + + * Bindings.py: Add menu configuration to the event configuration. + + * Attic/PopupMenu.py: Pass a root to the help window. + + * SearchBinding.py: + Add parent argument to 'to to line number' dialog box. + +Sat Oct 10 19:15:32 1998 Guido van Rossum <guido@cnri.reston.va.us> + + * StackViewer.py: + Add a label at the top showing (very basic) help for the stack viewer. + Add a label at the bottom showing the exception info. + + * Attic/test.py, Attic/idle: Add Unix main script and test program. + + * idle.pyw, help.txt, WidgetRedirector.py, UndoDelegator.py, StackViewer.py, SearchBinding.py, Attic/README, PyShell.py, Attic/PopupMenu.py, Percolator.py, Outline.py, IOBinding.py, History.py, Attic/HelpWindow.py, FrameViewer.py, FileList.py, EditorWindow.py, Delegator.py, ColorDelegator.py, Bindings.py, AutoIndent.py, AutoExpand.py: + Initial checking of Tk-based Python IDE. + Features: text editor with syntax coloring and undo; + subclassed into interactive Python shell which adds history. + diff --git a/Lib/idlelib/ClassBrowser.py b/Lib/idlelib/ClassBrowser.py new file mode 100644 index 0000000..f440164 --- /dev/null +++ b/Lib/idlelib/ClassBrowser.py @@ -0,0 +1,224 @@ +"""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 string +import pyclbr + +# XXX Patch pyclbr with dummies if it's vintage Python 1.5.2: +if not hasattr(pyclbr, "readmodule_ex"): + pyclbr.readmodule_ex = pyclbr.readmodule +if not hasattr(pyclbr, "Function"): + class Function(pyclbr.Class): + pass + pyclbr.Function = Function + +import PyShell +from WindowList import ListedToplevel +from TreeWidget import TreeNode, TreeItem, ScrolledCanvas + +class ClassBrowser: + + def __init__(self, flist, name, path): + # XXX This API should change, if the file doesn't end in ".py" + # XXX the code here is bogus! + self.name = name + self.file = os.path.join(path[0], self.name + ".py") + 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) + self.settitle() + top.focus_set() + # create scrolled canvas + sc = ScrolledCanvas(top, bg="white", 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, msg: + return [] + items = [] + self.classes = {} + for key, cl in dict.items(): + if cl.module == name: + s = key + if 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)" % string.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: + return not not self.cl.methods + + 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 = PyShell.flist.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 = PyShell.flist.open(self.file) + edit.gotoline(self.cl.methods[self.name]) + +def main(): + 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] + ClassBrowser(PyShell.flist, name, [dir]) + if sys.stdin is sys.__stdin__: + mainloop() + +if __name__ == "__main__": + main() diff --git a/Lib/idlelib/ColorDelegator.py b/Lib/idlelib/ColorDelegator.py new file mode 100644 index 0000000..77edfe8 --- /dev/null +++ b/Lib/idlelib/ColorDelegator.py @@ -0,0 +1,234 @@ +import time +import string +import re +import keyword +from Tkinter import * +from Delegator import Delegator +from IdleConf import idleconf + +#$ event <<toggle-auto-coloring>> +#$ win <Control-slash> +#$ unix <Control-slash> + +__debug__ = 0 + + +def any(name, list): + return "(?P<%s>" % name + string.join(list, "|") + ")" + +def make_pat(): + kw = r"\b" + any("KEYWORD", keyword.kwlist) + r"\b" + comment = any("COMMENT", [r"#[^\n]*"]) + sqstring = r"(\b[rR])?'[^'\\\n]*(\\.[^'\\\n]*)*'?" + dqstring = r'(\b[rR])?"[^"\\\n]*(\\.[^"\\\n]*)*"?' + sq3string = r"(\b[rR])?'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?" + dq3string = r'(\b[rR])?"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?' + string = any("STRING", [sq3string, dq3string, sqstring, dqstring]) + return kw + "|" + 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 + + 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") + + def config_colors(self): + for tag, cnf in self.tagdefs.items(): + if cnf: + apply(self.tag_configure, (tag,), cnf) + self.tag_raise('sel') + + cconf = idleconf.getsection('Colors') + + tagdefs = { + "COMMENT": cconf.getcolor("comment"), + "KEYWORD": cconf.getcolor("keyword"), + "STRING": cconf.getcolor("string"), + "DEFINITION": cconf.getcolor("definition"), + "SYNC": cconf.getcolor("sync"), + "TODO": cconf.getcolor("todo"), + "BREAK": cconf.getcolor("break"), + # The following is used by ReplaceDialog: + "hit": cconf.getcolor("hit"), + } + + 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 = 1 + colorizing = 0 + + 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 = 1 + 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 = 0 + self.stop_colorizing = 1 + 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 = 1 + 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 = 0 + self.colorizing = 1 + if __debug__: print "colorizing..." + t0 = time.clock() + self.recolorize_main() + t1 = time.clock() + if __debug__: print "%.3f seconds" % (t1-t0) + finally: + self.colorizing = 0 + 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 1: + 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 = 0 + 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, "->", `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 = 0 + 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 main(): + from Percolator import Percolator + root = Tk() + root.wm_protocol("WM_DELETE_WINDOW", root.quit) + text = Text(background="white") + text.pack(expand=1, fill="both") + text.focus_set() + p = Percolator(text) + d = ColorDelegator() + p.insertfilter(d) + root.mainloop() + +if __name__ == "__main__": + main() diff --git a/Lib/idlelib/ConfigParser.py b/Lib/idlelib/ConfigParser.py new file mode 100644 index 0000000..e1ce9dd --- /dev/null +++ b/Lib/idlelib/ConfigParser.py @@ -0,0 +1,382 @@ +"""Configuration file parser. + +A setup file consists of sections, lead by a "[section]" header, +and followed by "name: value" entries, with continuations and such in +the style of RFC 822. + +The option values can contain format strings which refer to other values in +the same section, or values in a special [DEFAULT] section. + +For example: + + something: %(dir)s/whatever + +would resolve the "%(dir)s" to the value of dir. All reference +expansions are done late, on demand. + +Intrinsic defaults can be specified by passing them into the +ConfigParser constructor as a dictionary. + +class: + +ConfigParser -- responsible for for parsing a list of + configuration files, and managing the parsed database. + + methods: + + __init__(defaults=None) + create the parser and specify a dictionary of intrinsic defaults. The + keys must be strings, the values must be appropriate for %()s string + interpolation. Note that `__name__' is always an intrinsic default; + it's value is the section's name. + + sections() + return all the configuration section names, sans DEFAULT + + has_section(section) + return whether the given section exists + + options(section) + return list of configuration options for the named section + + has_option(section, option) + return whether the given section has the given option + + read(filenames) + read and parse the list of named configuration files, given by + name. A single filename is also allowed. Non-existing files + are ignored. + + readfp(fp, filename=None) + read and parse one configuration file, given as a file object. + The filename defaults to fp.name; it is only used in error + messages (if fp has no `name' attribute, the string `<???>' is used). + + get(section, option, raw=0, vars=None) + return a string value for the named option. All % interpolations are + expanded in the return values, based on the defaults passed into the + constructor and the DEFAULT section. Additional substitutions may be + provided using the `vars' argument, which must be a dictionary whose + contents override any pre-existing defaults. + + getint(section, options) + like get(), but convert value to an integer + + getfloat(section, options) + like get(), but convert value to a float + + getboolean(section, options) + like get(), but convert value to a boolean (currently defined as 0 or + 1, only) +""" + +import sys +import string +import re + +DEFAULTSECT = "DEFAULT" + + + +# exception classes +class Error: + def __init__(self, msg=''): + self._msg = msg + def __repr__(self): + return self._msg + +class NoSectionError(Error): + def __init__(self, section): + Error.__init__(self, 'No section: %s' % section) + self.section = section + +class DuplicateSectionError(Error): + def __init__(self, section): + Error.__init__(self, "Section %s already exists" % section) + self.section = section + +class NoOptionError(Error): + def __init__(self, option, section): + Error.__init__(self, "No option `%s' in section: %s" % + (option, section)) + self.option = option + self.section = section + +class InterpolationError(Error): + def __init__(self, reference, option, section, rawval): + Error.__init__(self, + "Bad value substitution:\n" + "\tsection: [%s]\n" + "\toption : %s\n" + "\tkey : %s\n" + "\trawval : %s\n" + % (section, option, reference, rawval)) + self.reference = reference + self.option = option + self.section = section + +class MissingSectionHeaderError(Error): + def __init__(self, filename, lineno, line): + Error.__init__( + self, + 'File contains no section headers.\nfile: %s, line: %d\n%s' % + (filename, lineno, line)) + self.filename = filename + self.lineno = lineno + self.line = line + +class ParsingError(Error): + def __init__(self, filename): + Error.__init__(self, 'File contains parsing errors: %s' % filename) + self.filename = filename + self.errors = [] + + def append(self, lineno, line): + self.errors.append((lineno, line)) + self._msg = self._msg + '\n\t[line %2d]: %s' % (lineno, line) + + + +class ConfigParser: + def __init__(self, defaults=None): + self.__sections = {} + if defaults is None: + self.__defaults = {} + else: + self.__defaults = defaults + + def defaults(self): + return self.__defaults + + def sections(self): + """Return a list of section names, excluding [DEFAULT]""" + # self.__sections will never have [DEFAULT] in it + return self.__sections.keys() + + def add_section(self, section): + """Create a new section in the configuration. + + Raise DuplicateSectionError if a section by the specified name + already exists. + """ + if self.__sections.has_key(section): + raise DuplicateSectionError(section) + self.__sections[section] = {} + + def has_section(self, section): + """Indicate whether the named section is present in the configuration. + + The DEFAULT section is not acknowledged. + """ + return self.__sections.has_key(section) + + def options(self, section): + """Return a list of option names for the given section name.""" + try: + opts = self.__sections[section].copy() + except KeyError: + raise NoSectionError(section) + opts.update(self.__defaults) + return opts.keys() + + def has_option(self, section, option): + """Return whether the given section has the given option.""" + try: + opts = self.__sections[section] + except KeyError: + raise NoSectionError(section) + return opts.has_key(option) + + def read(self, filenames): + """Read and parse a filename or a list of filenames. + + Files that cannot be opened are silently ignored; this is + designed so that you can specify a list of potential + configuration file locations (e.g. current directory, user's + home directory, systemwide directory), and all existing + configuration files in the list will be read. A single + filename may also be given. + """ + if type(filenames) is type(''): + filenames = [filenames] + for filename in filenames: + try: + fp = open(filename) + except IOError: + continue + self.__read(fp, filename) + fp.close() + + def readfp(self, fp, filename=None): + """Like read() but the argument must be a file-like object. + + The `fp' argument must have a `readline' method. Optional + second argument is the `filename', which if not given, is + taken from fp.name. If fp has no `name' attribute, `<???>' is + used. + + """ + if filename is None: + try: + filename = fp.name + except AttributeError: + filename = '<???>' + self.__read(fp, filename) + + def get(self, section, option, raw=0, vars=None): + """Get an option value for a given section. + + All % interpolations are expanded in the return values, based on the + defaults passed into the constructor, unless the optional argument + `raw' is true. Additional substitutions may be provided using the + `vars' argument, which must be a dictionary whose contents overrides + any pre-existing defaults. + + The section DEFAULT is special. + """ + try: + sectdict = self.__sections[section].copy() + except KeyError: + if section == DEFAULTSECT: + sectdict = {} + else: + raise NoSectionError(section) + d = self.__defaults.copy() + d.update(sectdict) + # Update with the entry specific variables + if vars: + d.update(vars) + option = self.optionxform(option) + try: + rawval = d[option] + except KeyError: + raise NoOptionError(option, section) + # do the string interpolation + if raw: + return rawval + + value = rawval # Make it a pretty variable name + depth = 0 + while depth < 10: # Loop through this until it's done + depth = depth + 1 + if string.find(value, "%(") >= 0: + try: + value = value % d + except KeyError, key: + raise InterpolationError(key, option, section, rawval) + else: + return value + + def __get(self, section, conv, option): + return conv(self.get(section, option)) + + def getint(self, section, option): + return self.__get(section, string.atoi, option) + + def getfloat(self, section, option): + return self.__get(section, string.atof, option) + + def getboolean(self, section, option): + v = self.get(section, option) + val = string.atoi(v) + if val not in (0, 1): + raise ValueError, 'Not a boolean: %s' % v + return val + + def optionxform(self, optionstr): + return string.lower(optionstr) + + # + # Regular expressions for parsing section headers and options. Note a + # slight semantic change from the previous version, because of the use + # of \w, _ is allowed in section header names. + SECTCRE = re.compile( + r'\[' # [ + r'(?P<header>[-\w_.*,(){}]+)' # a lot of stuff found by IvL + r'\]' # ] + ) + OPTCRE = re.compile( + r'(?P<option>[-\w_.*,(){}]+)' # a lot of stuff found by IvL + r'[ \t]*(?P<vi>[:=])[ \t]*' # any number of space/tab, + # followed by separator + # (either : or =), followed + # by any # space/tab + r'(?P<value>.*)$' # everything up to eol + ) + + def __read(self, fp, fpname): + """Parse a sectioned setup file. + + The sections in setup file contains a title line at the top, + indicated by a name in square brackets (`[]'), plus key/value + options lines, indicated by `name: value' format lines. + Continuation are represented by an embedded newline then + leading whitespace. Blank lines, lines beginning with a '#', + and just about everything else is ignored. + """ + cursect = None # None, or a dictionary + optname = None + lineno = 0 + e = None # None, or an exception + while 1: + line = fp.readline() + if not line: + break + lineno = lineno + 1 + # comment or blank line? + if string.strip(line) == '' or line[0] in '#;': + continue + if string.lower(string.split(line)[0]) == 'rem' \ + and line[0] in "rR": # no leading whitespace + continue + # continuation line? + if line[0] in ' \t' and cursect is not None and optname: + value = string.strip(line) + if value: + cursect[optname] = cursect[optname] + '\n ' + value + # a section header or option header? + else: + # is it a section header? + mo = self.SECTCRE.match(line) + if mo: + sectname = mo.group('header') + if self.__sections.has_key(sectname): + cursect = self.__sections[sectname] + elif sectname == DEFAULTSECT: + cursect = self.__defaults + else: + cursect = {'__name__': sectname} + self.__sections[sectname] = cursect + # So sections can't start with a continuation line + optname = None + # no section header in the file? + elif cursect is None: + raise MissingSectionHeaderError(fpname, lineno, `line`) + # an option line? + else: + mo = self.OPTCRE.match(line) + if mo: + optname, vi, optval = mo.group('option', 'vi', 'value') + optname = string.lower(optname) + if vi in ('=', ':') and ';' in optval: + # ';' is a comment delimiter only if it follows + # a spacing character + pos = string.find(optval, ';') + if pos and optval[pos-1] in string.whitespace: + optval = optval[:pos] + optval = string.strip(optval) + # allow empty values + if optval == '""': + optval = '' + cursect[optname] = optval + else: + # a non-fatal parsing error occurred. set up the + # exception but keep going. the exception will be + # raised at the end of the file and will contain a + # list of all bogus lines + if not e: + e = ParsingError(fpname) + e.append(lineno, `line`) + # if any parsing errors occurred, raise an exception + if e: + raise e diff --git a/Lib/idlelib/Debugger.py b/Lib/idlelib/Debugger.py new file mode 100644 index 0000000..267d39f --- /dev/null +++ b/Lib/idlelib/Debugger.py @@ -0,0 +1,308 @@ +import os +import bdb +import traceback +from Tkinter import * +from WindowList import ListedToplevel + +import StackViewer + + +class Debugger(bdb.Bdb): + + interacting = 0 + + vstack = vsource = vlocals = vglobals = None + + def __init__(self, pyshell): + bdb.Bdb.__init__(self) + self.pyshell = pyshell + self.make_gui() + + def canonic(self, filename): + # Canonicalize filename -- called by Bdb + return os.path.normcase(os.path.abspath(filename)) + + def close(self, event=None): + if self.interacting: + self.top.bell() + return + if self.stackviewer: + self.stackviewer.close(); self.stackviewer = None + self.pyshell.close_debugger() + self.top.destroy() + + def run(self, *args): + try: + self.interacting = 1 + return apply(bdb.Bdb.run, (self,) + args) + finally: + self.interacting = 0 + + def user_line(self, frame): + self.interaction(frame) + + def user_return(self, frame, rv): + # XXX show rv? + ##self.interaction(frame) + pass + + def user_exception(self, frame, info): + self.interaction(frame, info) + + 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.vsource.set(1) + 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.vglobals.set(1) + 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() + + frame = None + + def interaction(self, frame, info=None): + self.frame = frame + code = frame.f_code + file = code.co_filename + base = os.path.basename(file) + lineno = frame.f_lineno + # + message = "%s:%s" % (base, lineno) + if code.co_name != "?": + message = "%s: %s()" % (message, code.co_name) + 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.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.tkraise() + self.root.mainloop() + # + 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 + code = frame.f_code + file = code.co_filename + lineno = frame.f_lineno + if file[:1] + file[-1:] != "<>" and os.path.exists(file): + edit = self.flist.open(file) + if edit: + edit.gotoline(lineno) + + def cont(self): + self.set_continue() + self.root.quit() + + def step(self): + self.set_step() + self.root.quit() + + def next(self): + self.set_next(self.frame) + self.root.quit() + + def ret(self): + self.set_return(self.frame) + self.root.quit() + + def quit(self): + self.set_quit() + self.root.quit() + + stackviewer = None + + def show_stack(self): + if not self.stackviewer and self.vstack.get(): + self.stackviewer = sv = StackViewer.StackViewer( + self.fstack, self.flist, self) + if self.frame: + stack, i = self.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, (frame, lineno)): + self.frame = frame + self.show_variables() + + localsviewer = None + globalsviewer = None + + def show_locals(self): + lv = self.localsviewer + if self.vlocals.get(): + if not lv: + self.localsviewer = StackViewer.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 = StackViewer.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) + if gv: + gv.load_dict(gdict, force) + + def set_breakpoint_here(self, edit): + text = edit.text + filename = edit.io.filename + if not filename: + text.bell() + return + lineno = int(float(text.index("insert"))) + msg = self.set_break(filename, lineno) + if msg: + text.bell() + return + text.tag_add("BREAK", "insert linestart", "insert lineend +1char") + + # A literal copy of Bdb.set_break() without the print statement at the end + def set_break(self, filename, lineno, temporary=0, cond = None): + import linecache # Import as late as possible + line = linecache.getline(filename, lineno) + if not line: + return 'That line does not exist!' + if not self.breaks.has_key(filename): + self.breaks[filename] = [] + list = self.breaks[filename] + if not lineno in list: + list.append(lineno) + bp = bdb.Breakpoint(filename, lineno, temporary, cond) diff --git a/Lib/idlelib/Delegator.py b/Lib/idlelib/Delegator.py new file mode 100644 index 0000000..3665247 --- /dev/null +++ b/Lib/idlelib/Delegator.py @@ -0,0 +1,34 @@ + +class Delegator: + + # The cache is only used to be able to change delegates! + + def __init__(self, delegate=None): + self.delegate = delegate + self.__cache = {} + + def __getattr__(self, name): + attr = getattr(self.delegate, name) # May raise AttributeError + setattr(self, name, attr) + self.__cache[name] = attr + return attr + + def resetcache(self): + for key in self.__cache.keys(): + try: + delattr(self, key) + except AttributeError: + pass + self.__cache.clear() + + def cachereport(self): + keys = self.__cache.keys() + keys.sort() + print keys + + def setdelegate(self, delegate): + self.resetcache() + self.delegate = delegate + + def getdelegate(self): + return self.delegate diff --git a/Lib/idlelib/EditorWindow.py b/Lib/idlelib/EditorWindow.py new file mode 100644 index 0000000..9f76ef7 --- /dev/null +++ b/Lib/idlelib/EditorWindow.py @@ -0,0 +1,749 @@ +# changes by dscherer@cmu.edu +# - created format and run menus +# - added silly advice dialog (apologies to Douglas Adams) +# - made Python Documentation work on Windows (requires win32api to +# do a ShellExecute(); other ways of starting a web browser are awkward) + +import sys +import os +import string +import re +import imp +from Tkinter import * +import tkSimpleDialog +import tkMessageBox +import idlever +import WindowList +from IdleConf import idleconf + +# The default tab setting for a Text widget, in average-width characters. +TK_TABWIDTH_DEFAULT = 8 + +# File menu + +#$ event <<open-module>> +#$ win <Alt-m> +#$ unix <Control-x><Control-m> + +#$ event <<open-class-browser>> +#$ win <Alt-c> +#$ unix <Control-x><Control-b> + +#$ event <<open-path-browser>> + +#$ event <<close-window>> +#$ unix <Control-x><Control-0> +#$ unix <Control-x><Key-0> +#$ win <Alt-F4> + +# Edit menu + +#$ event <<Copy>> +#$ win <Control-c> +#$ unix <Alt-w> + +#$ event <<Cut>> +#$ win <Control-x> +#$ unix <Control-w> + +#$ event <<Paste>> +#$ win <Control-v> +#$ unix <Control-y> + +#$ event <<select-all>> +#$ win <Alt-a> +#$ unix <Alt-a> + +# Help menu + +#$ event <<help>> +#$ win <F1> +#$ unix <F1> + +#$ event <<about-idle>> + +# Events without menu entries + +#$ event <<remove-selection>> +#$ win <Escape> + +#$ event <<center-insert>> +#$ win <Control-l> +#$ unix <Control-l> + +#$ event <<do-nothing>> +#$ unix <Control-x> + + +about_title = "About IDLE" +about_text = """\ +IDLE %s + +An Integrated DeveLopment Environment for Python + +by Guido van Rossum + +This version of IDLE has been modified by David Scherer + (dscherer@cmu.edu). See readme.txt for details. +""" % idlever.IDLE_VERSION + +class EditorWindow: + + from Percolator import Percolator + from ColorDelegator import ColorDelegator + from UndoDelegator import UndoDelegator + from IOBinding import IOBinding + import Bindings + from Tkinter import Toplevel + from MultiStatusBar import MultiStatusBar + + about_title = about_title + about_text = about_text + + vars = {} + + def __init__(self, flist=None, filename=None, key=None, root=None): + edconf = idleconf.getsection('EditorWindow') + coconf = idleconf.getsection('Colors') + self.flist = flist + root = root or flist.root + self.root = root + if flist: + self.vars = flist.vars + self.menubar = Menu(root) + self.top = top = self.Toplevel(root, menu=self.menubar) + self.vbar = vbar = Scrollbar(top, name='vbar') + self.text_frame = text_frame = Frame(top) + self.text = text = Text(text_frame, name='text', padx=5, + foreground=coconf.getdef('normal-foreground'), + background=coconf.getdef('normal-background'), + highlightcolor=coconf.getdef('hilite-foreground'), + highlightbackground=coconf.getdef('hilite-background'), + insertbackground=coconf.getdef('cursor-background'), + width=edconf.getint('width'), + height=edconf.getint('height'), + wrap="none") + + self.createmenubar() + self.apply_bindings() + + self.top.protocol("WM_DELETE_WINDOW", self.close) + self.top.bind("<<close-window>>", self.close_event) + text.bind("<<center-insert>>", self.center_insert_event) + text.bind("<<help>>", self.help_dialog) + text.bind("<<good-advice>>", self.good_advice) + text.bind("<<python-docs>>", self.python_docs) + text.bind("<<about-idle>>", self.about_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("<3>", self.right_menu_event) + if flist: + flist.inversedict[self] = key + if key: + flist.dict[key] = self + text.bind("<<open-new-window>>", self.flist.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) + + vbar['command'] = text.yview + vbar.pack(side=RIGHT, fill=Y) + + text['yscrollcommand'] = vbar.set + text['font'] = edconf.get('font-name'), edconf.get('font-size') + text_frame.pack(side=LEFT, fill=BOTH, expand=1) + text.pack(side=TOP, fill=BOTH, expand=1) + text.focus_set() + + self.per = per = self.Percolator(text) + if self.ispythonsource(filename): + self.color = color = self.ColorDelegator(); per.insertfilter(color) + ##print "Initial colorizer" + else: + ##print "No initial colorizer" + self.color = None + self.undo = undo = self.UndoDelegator(); per.insertfilter(undo) + self.io = io = self.IOBinding(self) + + 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) + io.set_filename_change_hook(self.filename_change_hook) + + if filename: + if os.path.exists(filename): + io.loadfile(filename) + else: + io.set_filename(filename) + + self.saved_change_hook() + + 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 + + if self.extensions.has_key('AutoIndent'): + self.extensions['AutoIndent'].set_indentation_params( + self.ispythonsource(filename)) + self.set_status_bar() + + def set_status_bar(self): + self.status_bar = self.MultiStatusBar(self.text_frame) + 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) + self.text.bind('<KeyRelease>', self.set_line_and_column) + self.text.bind('<ButtonRelease>', self.set_line_and_column) + self.text.after_idle(self.set_line_and_column) + + def set_line_and_column(self, event=None): + line, column = string.split(self.text.index(INSERT), '.') + self.status_bar.set_label('column', 'Col: %s' % column) + self.status_bar.set_label('line', 'Ln: %s' % line) + + def wakeup(self): + if self.top.wm_state() == "iconic": + self.top.wm_deiconify() + else: + self.top.tkraise() + self.text.focus_set() + + menu_specs = [ + ("file", "_File"), + ("edit", "_Edit"), + ("format", "F_ormat"), + ("run", "_Run"), + ("windows", "_Windows"), + ("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) + mbar.add_cascade(label=label, menu=menu, underline=underline) + self.fill_menus() + + def postwindowsmenu(self): + # Only called when Windows menu exists + # XXX Actually, this Just-In-Time updating interferes badly + # XXX with the tear-off feature. It would be better to update + # XXX all Windows menus whenever the list of windows changes. + 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.tag_remove("sel", "1.0", "end") + 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") + rmenu.tk_popup(event.x_root, event.y_root) + if iswin: + self.text.config(cursor="ibeam") + + rmenu_specs = [ + # ("Label", "<<virtual-event>>"), ... + ("Close", "<<close-window>>"), # Example + ] + + def make_rmenu(self): + rmenu = Menu(self.text, tearoff=0) + for label, eventname in self.rmenu_specs: + def command(text=self.text, eventname=eventname): + text.event_generate(eventname) + rmenu.add_command(label=label, command=command) + self.rmenu = rmenu + + def about_dialog(self, event=None): + tkMessageBox.showinfo(self.about_title, self.about_text, + master=self.text) + + helpfile = "help.txt" + + def good_advice(self, event=None): + tkMessageBox.showinfo('Advice', "Don't Panic!", master=self.text) + + def help_dialog(self, event=None): + try: + helpfile = os.path.join(os.path.dirname(__file__), self.helpfile) + except NameError: + helpfile = self.helpfile + if self.flist: + self.flist.open(helpfile) + else: + self.io.loadfile(helpfile) + + help_viewer = "netscape -remote 'openurl(%(url)s)' 2>/dev/null || " \ + "netscape %(url)s &" + help_url = "http://www.python.org/doc/current/" + + def python_docs(self, event=None): + if sys.platform=='win32': + try: + import win32api + import ExecBinding + doc = os.path.join( os.path.dirname( ExecBinding.pyth_exe ), "doc", "index.html" ) + win32api.ShellExecute(0, None, doc, None, sys.path[0], 1) + except: + pass + else: + cmd = self.help_viewer % {"url": self.help_url} + os.system(cmd) + + 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 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 = string.strip(name) + if not name: + name = tkSimpleDialog.askstring("Module", + "Enter the name of a Python module\n" + "to search on sys.path and open:", + parent=self.text) + if name: + name = string.strip(name) + if not name: + return + # XXX Ought to support package syntax + # XXX Ought to insert current file's directory in front of path + try: + (f, file, (suffix, mode, type)) = imp.find_module(name) + except (NameError, ImportError), msg: + tkMessageBox.showerror("Import error", str(msg), parent=self.text) + return + if type != 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) + else: + self.io.loadfile(file) + + def open_class_browser(self, event=None): + filename = self.io.filename + if not filename: + tkMessageBox.showerror( + "No filename", + "This buffer has no associated filename", + master=self.text) + self.text.focus_set() + return None + head, tail = os.path.split(filename) + base, ext = os.path.splitext(tail) + import ClassBrowser + ClassBrowser.ClassBrowser(self.flist, base, [head]) + + def open_path_browser(self, event=None): + 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: + return 1 + base, ext = os.path.splitext(os.path.basename(filename)) + if os.path.normcase(ext) in (".py", ".pyw"): + return 1 + try: + f = open(filename) + line = f.readline() + f.close() + except IOError: + return 0 + return line[:2] == '#!' and string.find(line, 'python') >= 0 + + def close_hook(self): + if self.flist: + self.flist.close_edit(self) + + 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() + if self.ispythonsource(self.io.filename): + self.addcolorizer() + else: + self.rmcolorizer() + + def addcolorizer(self): + if self.color: + return + ##print "Add colorizer" + self.per.removefilter(self.undo) + self.color = self.ColorDelegator() + self.per.insertfilter(self.color) + self.per.insertfilter(self.undo) + + def rmcolorizer(self): + if not self.color: + return + ##print "Remove colorizer" + self.per.removefilter(self.undo) + self.per.removefilter(self.color) + self.color = None + self.per.insertfilter(self.undo) + + def saved_change_hook(self): + short = self.short_title() + long = self.long_title() + if short and long: + title = short + " - " + long + 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) + return filename + + def long_title(self): + return 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 close_event(self, event): + self.close() + + def maybesave(self): + if self.io: + return self.io.maybesave() + + def close(self): + self.top.wm_deiconify() + self.top.tkraise() + reply = self.maybesave() + if reply != "cancel": + self._close() + return reply + + def _close(self): + WindowList.unregister_callback(self.postwindowsmenu) + if self.close_hook: + self.close_hook() + self.flist = None + colorizing = 0 + self.unload_extensions() + self.io.close(); self.io = None + self.undo = None # XXX + if self.color: + colorizing = self.color.colorizing + doh = colorizing and self.top + self.color.close(doh) # Cancel colorization + self.text = None + self.vars = None + self.per.close(); self.per = None + if not colorizing: + self.top.destroy() + + 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", `name` + import traceback + traceback.print_exc() + + def get_standard_extension_names(self): + return idleconf.getextensions() + + def load_extension(self, name): + mod = __import__(name, globals(), locals(), []) + cls = getattr(mod, name) + ins = cls(self) + self.extensions[name] = ins + kdnames = ["keydefs"] + if sys.platform == 'win32': + kdnames.append("windows_keydefs") + elif sys.platform == 'mac': + kdnames.append("mac_keydefs") + else: + kdnames.append("unix_keydefs") + keydefs = {} + for kdname in kdnames: + if hasattr(ins, kdname): + keydefs.update(getattr(ins, kdname)) + if keydefs: + self.apply_bindings(keydefs) + for vevent in keydefs.keys(): + methodname = string.replace(vevent, "-", "_") + 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)) + if hasattr(ins, "menudefs"): + self.fill_menus(ins.menudefs, keydefs) + return ins + + 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: + apply(text.event_add, (event,) + tuple(keylist)) + + def fill_menus(self, defs=None, keydefs=None): + # Fill the menus. Menus that are absent or None in + # self.menudict are ignored. + if defs is None: + defs = self.Bindings.menudefs + if keydefs is None: + keydefs = self.Bindings.default_keydefs + menudict = self.menudict + text = self.text + for mname, itemlist in defs: + menu = menudict.get(mname) + if not menu: + continue + for item in itemlist: + if not item: + menu.add_separator() + else: + label, event = item + checkbutton = (label[:1] == '!') + if checkbutton: + label = label[1:] + underline, label = prepstr(label) + accelerator = get_accelerator(keydefs, event) + def command(text=text, event=event): + text.event_generate(event) + if checkbutton: + var = self.getrawvar(event, 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.getrawvar(name) + if var: + return var.get() + + def setvar(self, name, value, vartype=None): + var = self.getrawvar(name, vartype) + if var: + var.set(value) + + def getrawvar(self, name, vartype=None): + var = self.vars.get(name) + if not var and vartype: + self.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" * newtabwith) + text.configure(tabs=pixels) + +def prepstr(s): + # Helper to extract the underscore from a string, e.g. + # prepstr("Co_py") returns (2, "Copy"). + i = string.find(s, '_') + if i >= 0: + s = s[:i] + s[i+1:] + return i, s + + +keynames = { + 'bracketleft': '[', + 'bracketright': ']', + 'slash': '/', +} + +def get_accelerator(keydefs, event): + keylist = keydefs.get(event) + if not keylist: + return "" + s = keylist[0] + s = re.sub(r"-[a-z]\b", lambda m: string.upper(m.group()), s) + s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s) + s = re.sub("Key-", "", s) + s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu + s = re.sub("Control-", "Ctrl-", s) + s = re.sub("-", "+", s) + s = re.sub("><", " ", s) + s = re.sub("<", "", s) + s = re.sub(">", "", s) + return s + + +def fixwordbreaks(root): + # Make sure that Tk's double-click and next/previous word + # operations use our definition of a word (i.e. an identifier) + tk = root.tk + tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded + tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]') + tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]') + + +def test(): + root = Tk() + fixwordbreaks(root) + root.withdraw() + if sys.argv[1:]: + filename = sys.argv[1] + else: + filename = None + edit = EditorWindow(root=root, filename=filename) + edit.set_close_hook(root.quit) + root.mainloop() + root.destroy() + +if __name__ == '__main__': + test() diff --git a/Lib/idlelib/ExecBinding.py b/Lib/idlelib/ExecBinding.py new file mode 100644 index 0000000..67b0822 --- /dev/null +++ b/Lib/idlelib/ExecBinding.py @@ -0,0 +1,198 @@ +"""Extension to execute a script in a separate process + +David Scherer <dscherer@cmu.edu> + + The ExecBinding module, a replacement for ScriptBinding, executes + programs in a separate process. Unlike previous versions, this version + communicates with the user process via an RPC protocol (see the 'protocol' + module). The user program is loaded by the 'loader' and 'Remote' + modules. Its standard output and input are directed back to the + ExecBinding class through the RPC mechanism and implemented here. + + A "stop program" command is provided and bound to control-break. Closing + the output window also stops the running program. +""" + +import sys +import os +import imp +import OutputWindow +import protocol +import spawn +import traceback +import tempfile + +# Find Python and the loader. This should be done as early in execution +# as possible, because if the current directory or sys.path is changed +# it may no longer be possible to get correct paths for these things. + +pyth_exe = spawn.hardpath( sys.executable ) +load_py = spawn.hardpath( imp.find_module("loader")[1] ) + +# The following mechanism matches loaders up with ExecBindings that are +# trying to load something. + +waiting_for_loader = [] + +def loader_connect(client, addr): + if waiting_for_loader: + a = waiting_for_loader.pop(0) + try: + return a.connect(client, addr) + except: + return loader_connect(client,addr) + +protocol.publish('ExecBinding', loader_connect) + +class ExecBinding: + keydefs = { + '<<run-complete-script>>': ['<F5>'], + '<<stop-execution>>': ['<Cancel>'], #'<Control-c>' + } + + menudefs = [ + ('run', [None, + ('Run program', '<<run-complete-script>>'), + ('Stop program', '<<stop-execution>>'), + ] + ), + ] + + delegate = 1 + + def __init__(self, editwin): + self.editwin = editwin + self.client = None + self.temp = [] + + if not hasattr(editwin, 'source_window'): + self.delegate = 0 + self.output = OutputWindow.OnDemandOutputWindow(editwin.flist) + self.output.close_hook = self.stopProgram + self.output.source_window = editwin + else: + if (self.editwin.source_window and + self.editwin.source_window.extensions.has_key('ExecBinding') and + not self.editwin.source_window.extensions['ExecBinding'].delegate): + delegate = self.editwin.source_window.extensions['ExecBinding'] + self.run_complete_script_event = delegate.run_complete_script_event + self.stop_execution_event = delegate.stop_execution_event + + def __del__(self): + self.stopProgram() + + def stop_execution_event(self, event): + if self.client: + self.stopProgram() + self.write('\nProgram stopped.\n','stderr') + + def run_complete_script_event(self, event): + filename = self.getfilename() + if not filename: return + filename = os.path.abspath(filename) + + self.stopProgram() + + self.commands = [ ('run', filename) ] + waiting_for_loader.append(self) + spawn.spawn( pyth_exe, load_py ) + + def connect(self, client, addr): + # Called by loader_connect() above. It is remotely possible that + # we get connected to two loaders if the user is running the + # program repeatedly in a short span of time. In this case, we + # simply return None, refusing to connect and letting the redundant + # loader die. + if self.client: return None + + self.client = client + client.set_close_hook( self.connect_lost ) + + title = self.editwin.short_title() + if title: + self.output.set_title(title + " Output") + else: + self.output.set_title("Output") + self.output.write('\n',"stderr") + self.output.scroll_clear() + + return self + + def connect_lost(self): + # Called by the client's close hook when the loader closes its + # socket. + + # We print a disconnect message only if the output window is already + # open. + if self.output.owin and self.output.owin.text: + self.output.owin.interrupt() + self.output.write("\nProgram disconnected.\n","stderr") + + for t in self.temp: + try: + os.remove(t) + except: + pass + self.temp = [] + self.client = None + + def get_command(self): + # Called by Remote to find out what it should be executing. + # Later this will be used to implement debugging, interactivity, etc. + if self.commands: + return self.commands.pop(0) + return ('finish',) + + def program_exception(self, type, value, tb, first, last): + if type == SystemExit: return 0 + + for i in range(len(tb)): + filename, lineno, name, line = tb[i] + if filename in self.temp: + filename = 'Untitled' + tb[i] = filename, lineno, name, line + + list = traceback.format_list(tb[first:last]) + exc = traceback.format_exception_only( type, value ) + + self.write('Traceback (innermost last)\n', 'stderr') + for i in (list+exc): + self.write(i, 'stderr') + + self.commands = [] + return 1 + + def write(self, text, tag): + self.output.write(text,tag) + + def readline(self): + return self.output.readline() + + def stopProgram(self): + if self.client: + self.client.close() + self.client = None + + def getfilename(self): + # Save all files which have been named, because they might be modules + for edit in self.editwin.flist.inversedict.keys(): + if edit.io and edit.io.filename and not edit.get_saved(): + edit.io.save(None) + + # Experimental: execute unnamed buffer + if not self.editwin.io.filename: + filename = os.path.normcase(os.path.abspath(tempfile.mktemp())) + self.temp.append(filename) + if self.editwin.io.writefile(filename): + return filename + + # If the file isn't save, we save it. If it doesn't have a filename, + # the user will be prompted. + if self.editwin.io and not self.editwin.get_saved(): + self.editwin.io.save(None) + + # If the file *still* isn't saved, we give up. + if not self.editwin.get_saved(): + return + + return self.editwin.io.filename diff --git a/Lib/idlelib/FileList.py b/Lib/idlelib/FileList.py new file mode 100644 index 0000000..5d9aafe --- /dev/null +++ b/Lib/idlelib/FileList.py @@ -0,0 +1,150 @@ +# changes by dscherer@cmu.edu +# - FileList.open() takes an optional 3rd parameter action, which is +# called instead of creating a new EditorWindow. This enables +# things like 'open in same window'. + +import os +from Tkinter import * +import tkMessageBox + +import WindowList + +#$ event <<open-new-window>> +#$ win <Control-n> +#$ unix <Control-x><Control-n> + +# (This is labeled as 'Exit'in the File menu) +#$ event <<close-all-windows>> +#$ win <Control-q> +#$ unix <Control-x><Control-c> + +class FileList: + + from EditorWindow import EditorWindow + EditorWindow.Toplevel = WindowList.ListedToplevel # XXX Patch it! + + def __init__(self, root): + self.root = root + self.dict = {} + self.inversedict = {} + self.vars = {} # For EditorWindow.getrawvar (shared Tcl variables) + + + def goodname(self, filename): + filename = self.canonize(filename) + key = os.path.normcase(filename) + if self.dict.has_key(key): + edit = self.dict[key] + filename = edit.io.filename or filename + return filename + + def open(self, filename, action=None): + assert filename + filename = self.canonize(filename) + if os.path.isdir(filename): + tkMessageBox.showerror( + "Is A Directory", + "The path %s is a directory." % `filename`, + master=self.root) + return None + key = os.path.normcase(filename) + if self.dict.has_key(key): + edit = self.dict[key] + edit.wakeup() + return edit + if not os.path.exists(filename): + tkMessageBox.showinfo( + "New File", + "Opening non-existent file %s" % `filename`, + master=self.root) + if action is None: + return self.EditorWindow(self, filename, key) + else: + return action(filename) + + def new(self): + return self.EditorWindow(self) + + def new_callback(self, event): + self.new() + return "break" + + def close_all_callback(self, event): + for edit in self.inversedict.keys(): + reply = edit.close() + if reply == "cancel": + break + return "break" + + def close_edit(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 self.dict.has_key(newkey): + conflict = self.dict[newkey] + self.inversedict[conflict] = None + tkMessageBox.showerror( + "Name Conflict", + "You now have multiple edit windows open for %s" % `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 EditorWindow import fixwordbreaks + import sys + root = Tk() + fixwordbreaks(root) + root.withdraw() + flist = FileList(root) + if sys.argv[1:]: + for filename in sys.argv[1:]: + flist.open(filename) + else: + flist.new() + if flist.inversedict: + root.mainloop() + +if __name__ == '__main__': + test() diff --git a/Lib/idlelib/FormatParagraph.py b/Lib/idlelib/FormatParagraph.py new file mode 100644 index 0000000..68fe6b1 --- /dev/null +++ b/Lib/idlelib/FormatParagraph.py @@ -0,0 +1,155 @@ +# Extension to format a paragraph + +# 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, arent handled :-) + +import string +import re + +class FormatParagraph: + + menudefs = [ + ('format', [ # /s/edit/format dscherer@cmu.edu + ('Format Paragraph', '<<format-paragraph>>'), + ]) + ] + + keydefs = { + '<<format-paragraph>>': ['<Alt-q>'], + } + + unix_keydefs = { + '<<format-paragraph>>': ['<Meta-q>'], + } + + def __init__(self, editwin): + self.editwin = editwin + + def close(self): + self.editwin = None + + def format_paragraph_event(self, event): + text = self.editwin.text + first, last = self.editwin.get_selection_indices() + if first and last: + data = text.get(first, last) + comment_header = '' + else: + first, last, comment_header, data = \ + find_paragraph(text, text.index("insert")) + if comment_header: + # Reformat the comment lines - convert to text sans header. + lines = string.split(data, "\n") + lines = map(lambda st, l=len(comment_header): st[l:], lines) + data = string.join(lines, "\n") + # Reformat to 70 chars or a 20 char width, whichever is greater. + format_width = max(70-len(comment_header), 20) + newdata = reformat_paragraph(data, format_width) + # re-split and re-insert the comment header. + newdata = string.split(newdata, "\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 isnt made of complete + # lines, but whatever!) Can't think of a clean soltution, + # so we hack away + block_suffix = "" + if not newdata[-1]: + block_suffix = "\n" + newdata = newdata[:-1] + builder = lambda item, prefix=comment_header: prefix+item + newdata = string.join(map(builder, newdata), '\n') + block_suffix + else: + # Just a normal text format + newdata = reformat_paragraph(data) + 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") + +def find_paragraph(text, mark): + lineno, col = map(int, string.split(mark, ".")) + line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno) + while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line): + lineno = lineno + 1 + line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno) + first_lineno = lineno + comment_header = get_comment_header(line) + comment_header_len = len(comment_header) + 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.0 lineend" % lineno) + last = "%d.0" % lineno + # Search back to beginning of paragraph + lineno = first_lineno - 1 + line = text.get("%d.0" % lineno, "%d.0 lineend" % 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.0 lineend" % lineno) + first = "%d.0" % (lineno+1) + return first, last, comment_header, text.get(first, last) + +def reformat_paragraph(data, limit=70): + lines = string.split(data, "\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(string.expandtabs(partial + word)) > limit and \ + partial != indent1: + new.append(string.rstrip(partial)) + partial = indent2 + partial = partial + word + " " + if j+1 < len(words) and words[j+1] != " ": + partial = partial + " " + i = i+1 + new.append(string.rstrip(partial)) + # XXX Should reformat remaining paragraphs as well + new.extend(lines[i:]) + return string.join(new, "\n") + +def is_all_white(line): + return re.match(r"^\s*$", line) is not None + +def get_indent(line): + return re.match(r"^(\s*)", line).group() + +def get_comment_header(line): + m = re.match(r"^(\s*#*)", line) + if m is None: return "" + return m.group(1) diff --git a/Lib/idlelib/FrameViewer.py b/Lib/idlelib/FrameViewer.py new file mode 100644 index 0000000..2ce0935 --- /dev/null +++ b/Lib/idlelib/FrameViewer.py @@ -0,0 +1,38 @@ +from repr import Repr +from Tkinter import * + +class FrameViewer: + + def __init__(self, root, frame): + self.root = root + self.frame = frame + self.top = Toplevel(self.root) + self.repr = Repr() + self.repr.maxstring = 60 + self.load_variables() + + def load_variables(self): + row = 0 + if self.frame.f_locals is not self.frame.f_globals: + l = Label(self.top, text="Local Variables", + borderwidth=2, relief="raised") + l.grid(row=row, column=0, columnspan=2, sticky="ew") + row = self.load_names(self.frame.f_locals, row+1) + l = Label(self.top, text="Global Variables", + borderwidth=2, relief="raised") + l.grid(row=row, column=0, columnspan=2, sticky="ew") + row = self.load_names(self.frame.f_globals, row+1) + + def load_names(self, dict, row): + names = dict.keys() + names.sort() + for name in names: + value = dict[name] + svalue = self.repr.repr(value) + l = Label(self.top, text=name) + l.grid(row=row, column=0, sticky="w") + l = Entry(self.top, width=60, borderwidth=0) + l.insert(0, svalue) + l.grid(row=row, column=1, sticky="w") + row = row+1 + return row diff --git a/Lib/idlelib/GrepDialog.py b/Lib/idlelib/GrepDialog.py new file mode 100644 index 0000000..61c77c3 --- /dev/null +++ b/Lib/idlelib/GrepDialog.py @@ -0,0 +1,135 @@ +import string +import os +import re +import fnmatch +import sys +from Tkinter import * +import tkMessageBox +import SearchEngine +from SearchDialogBase import SearchDialogBase + +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 + dialog.open(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, io=None): + SearchDialogBase.open(self, None) + 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) + + def create_other_buttons(self): + f = self.make_frame() + + 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 OutputWindow import OutputWindow + 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 %s in %s ..." % (`pat`, path) + hits = 0 + for fn in list: + try: + f = open(fn) + except IOError, msg: + print msg + continue + lineno = 0 + while 1: + block = f.readlines(100000) + if not block: + break + for line in block: + lineno = lineno + 1 + if line[-1:] == '\n': + line = line[:-1] + if prog.search(line): + sys.stdout.write("%s: %s: %s\n" % (fn, lineno, line)) + hits = hits + 1 + if hits: + if hits == 1: + s = "" + else: + s = "s" + print "Found", hits, "hit%s." % s + print "(Hint: right-click to open locations.)" + else: + print "No hits." + + def findfiles(self, dir, base, rec): + try: + names = os.listdir(dir or os.curdir) + except os.error, 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() diff --git a/Lib/idlelib/IOBinding.py b/Lib/idlelib/IOBinding.py new file mode 100644 index 0000000..07b04f3 --- /dev/null +++ b/Lib/idlelib/IOBinding.py @@ -0,0 +1,254 @@ +# changes by dscherer@cmu.edu +# - IOBinding.open() replaces the current window with the opened file, +# if the current window is both unmodified and unnamed +# - IOBinding.loadfile() interprets Windows, UNIX, and Macintosh +# end-of-line conventions, instead of relying on the standard library, +# which will only understand the local convention. + +import os +import tkFileDialog +import tkMessageBox +import re + +#$ event <<open-window-from-file>> +#$ win <Control-o> +#$ unix <Control-x><Control-f> + +#$ event <<save-window>> +#$ win <Control-s> +#$ unix <Control-x><Control-s> + +#$ event <<save-window-as-file>> +#$ win <Alt-s> +#$ unix <Control-x><Control-w> + +#$ event <<save-copy-of-window-as-file>> +#$ win <Alt-Shift-s> +#$ unix <Control-x><w> + + +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) + + 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) + # 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 + + def set_filename(self, filename): + self.filename = filename + self.set_saved(1) + if self.filename_change_hook: + self.filename_change_hook() + + def open(self, event): + if self.editwin.flist: + filename = self.askopenfile() + if filename: + # if the current window has no filename and hasn't been + # modified, we replace it's contents (no loss). Otherwise + # we open a new window. + if not self.filename and self.get_saved(): + self.editwin.flist.open(filename, self.loadfile) + else: + self.editwin.flist.open(filename) + else: + 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" + filename = self.askopenfile() + if filename: + self.loadfile(filename) + else: + self.text.focus_set() + return "break" + + def loadfile(self, filename): + try: + # open the file in binary mode so that we can handle + # end-of-line convention ourselves. + f = open(filename,'rb') + chars = f.read() + f.close() + except IOError, msg: + tkMessageBox.showerror("I/O Error", str(msg), master=self.text) + return 0 + + # We now convert all end-of-lines to '\n's + eol = r"(\r\n)|\n|\r" # \r\n (Windows), \n (UNIX), or \r (Mac) + chars = re.compile( eol ).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.see("insert") + return 1 + + def maybesave(self): + if self.get_saved(): + return "yes" + message = "Do you want to save %s before closing?" % ( + self.filename or "this untitled document") + m = tkMessageBox.Message( + title="Save On Close", + message=message, + icon=tkMessageBox.QUESTION, + type=tkMessageBox.YESNOCANCEL, + master=self.text) + reply = m.show() + if reply == "yes": + self.save(None) + if not self.get_saved(): + reply = "cancel" + 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(1) + 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) + self.text.focus_set() + return "break" + + def save_a_copy(self, event): + filename = self.asksavefile() + if filename: + self.writefile(filename) + self.text.focus_set() + return "break" + + def writefile(self, filename): + self.fixlastline() + try: + f = open(filename, "w") + chars = self.text.get("1.0", "end-1c") + f.write(chars) + f.close() + ## print "saved to", `filename` + return 1 + except IOError, msg: + tkMessageBox.showerror("I/O Error", str(msg), + master=self.text) + return 0 + + def fixlastline(self): + c = self.text.get("end-2c") + if c != '\n': + self.text.insert("end-1c", "\n") + + opendialog = None + savedialog = None + + filetypes = [ + ("Python and text files", "*.py *.pyw *.txt", "TEXT"), + ("All text files", "*", "TEXT"), + ("All files", "*"), + ] + + def askopenfile(self): + dir, base = self.defaultfilename("open") + if not self.opendialog: + self.opendialog = tkFileDialog.Open(master=self.text, + filetypes=self.filetypes) + return self.opendialog.show(initialdir=dir, initialfile=base) + + def defaultfilename(self, mode="open"): + if self.filename: + return os.path.split(self.filename) + 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(master=self.text, + filetypes=self.filetypes) + return self.savedialog.show(initialdir=dir, initialfile=base) + + +def test(): + from Tkinter import * + root = Tk() + class MyEditWin: + def __init__(self, text): + self.text = text + self.flist = None + self.text.bind("<Control-o>", self.open) + self.text.bind("<Control-s>", self.save) + self.text.bind("<Alt-s>", self.save_as) + self.text.bind("<Alt-z>", self.save_a_copy) + def get_saved(self): return 0 + def set_saved(self, flag): pass + def reset_undo(self): pass + def open(self, event): + self.text.event_generate("<<open-window-from-file>>") + def save(self, event): + self.text.event_generate("<<save-window>>") + def save_as(self, event): + self.text.event_generate("<<save-window-as-file>>") + def save_a_copy(self, event): + self.text.event_generate("<<save-copy-of-window-as-file>>") + text = Text(root) + text.pack() + text.focus_set() + editwin = MyEditWin(text) + io = IOBinding(editwin) + root.mainloop() + +if __name__ == "__main__": + test() diff --git a/Lib/idlelib/Icons/folder.gif b/Lib/idlelib/Icons/folder.gif Binary files differnew file mode 100644 index 0000000..effe8dc --- /dev/null +++ b/Lib/idlelib/Icons/folder.gif diff --git a/Lib/idlelib/Icons/minusnode.gif b/Lib/idlelib/Icons/minusnode.gif Binary files differnew file mode 100644 index 0000000..6cca2bf --- /dev/null +++ b/Lib/idlelib/Icons/minusnode.gif diff --git a/Lib/idlelib/Icons/openfolder.gif b/Lib/idlelib/Icons/openfolder.gif Binary files differnew file mode 100644 index 0000000..24aea1b --- /dev/null +++ b/Lib/idlelib/Icons/openfolder.gif diff --git a/Lib/idlelib/Icons/plusnode.gif b/Lib/idlelib/Icons/plusnode.gif Binary files differnew file mode 100644 index 0000000..13ace90 --- /dev/null +++ b/Lib/idlelib/Icons/plusnode.gif diff --git a/Lib/idlelib/Icons/python.gif b/Lib/idlelib/Icons/python.gif Binary files differnew file mode 100644 index 0000000..a7df778 --- /dev/null +++ b/Lib/idlelib/Icons/python.gif diff --git a/Lib/idlelib/Icons/tk.gif b/Lib/idlelib/Icons/tk.gif Binary files differnew file mode 100644 index 0000000..a603f5e --- /dev/null +++ b/Lib/idlelib/Icons/tk.gif diff --git a/Lib/idlelib/IdleConf.py b/Lib/idlelib/IdleConf.py new file mode 100644 index 0000000..36cad51 --- /dev/null +++ b/Lib/idlelib/IdleConf.py @@ -0,0 +1,113 @@ +"""Provides access to configuration information""" + +import os +import sys +from ConfigParser import ConfigParser, NoOptionError, NoSectionError + +class IdleConfParser(ConfigParser): + + # these conf sections do not define extensions! + builtin_sections = {} + for section in ('EditorWindow', 'Colors'): + builtin_sections[section] = section + + def getcolor(self, sec, name): + """Return a dictionary with foreground and background colors + + The return value is appropriate for passing to Tkinter in, e.g., + a tag_config call. + """ + fore = self.getdef(sec, name + "-foreground") + back = self.getdef(sec, name + "-background") + return {"foreground": fore, + "background": back} + + def getdef(self, sec, options, raw=0, vars=None, default=None): + """Get an option value for given section or return default""" + try: + return self.get(sec, options, raw, vars) + except (NoSectionError, NoOptionError): + return default + + def getsection(self, section): + """Return a SectionConfigParser object""" + return SectionConfigParser(section, self) + + def getextensions(self): + exts = [] + for sec in self.sections(): + if self.builtin_sections.has_key(sec): + continue + # enable is a bool, but it may not be defined + if self.getdef(sec, 'enable') != '0': + exts.append(sec) + return exts + + def reload(self): + global idleconf + idleconf = IdleConfParser() + load(_dir) # _dir is a global holding the last directory loaded + +class SectionConfigParser: + """A ConfigParser object specialized for one section + + This class has all the get methods that a regular ConfigParser does, + but without requiring a section argument. + """ + def __init__(self, section, config): + self.section = section + self.config = config + + def options(self): + return self.config.options(self.section) + + def get(self, options, raw=0, vars=None): + return self.config.get(self.section, options, raw, vars) + + def getdef(self, options, raw=0, vars=None, default=None): + return self.config.getdef(self.section, options, raw, vars, default) + + def getint(self, option): + return self.config.getint(self.section, option) + + def getfloat(self, option): + return self.config.getint(self.section, option) + + def getboolean(self, option): + return self.config.getint(self.section, option) + + def getcolor(self, option): + return self.config.getcolor(self.section, option) + +def load(dir): + """Load IDLE configuration files based on IDLE install in dir + + Attempts to load two config files: + dir/config.txt + dir/config-[win/mac/unix].txt + dir/config-%(sys.platform)s.txt + ~/.idle + """ + global _dir + _dir = dir + + if sys.platform[:3] == 'win': + genplatfile = os.path.join(dir, "config-win.txt") + # XXX don't know what the platform string is on a Mac + elif sys.platform[:3] == 'mac': + genplatfile = os.path.join(dir, "config-mac.txt") + else: + genplatfile = os.path.join(dir, "config-unix.txt") + + platfile = os.path.join(dir, "config-%s.txt" % sys.platform) + + try: + homedir = os.environ['HOME'] + except KeyError: + homedir = os.getcwd() + + idleconf.read((os.path.join(dir, "config.txt"), genplatfile, platfile, + os.path.join(homedir, ".idle"))) + +idleconf = IdleConfParser() + diff --git a/Lib/idlelib/IdleHistory.py b/Lib/idlelib/IdleHistory.py new file mode 100644 index 0000000..aa41b73 --- /dev/null +++ b/Lib/idlelib/IdleHistory.py @@ -0,0 +1,89 @@ +import string + +class History: + + def __init__(self, text, output_sep = "\n"): + self.text = text + self.history = [] + self.history_prefix = None + self.history_pointer = None + self.output_sep = output_sep + text.bind("<<history-previous>>", self.history_prev) + text.bind("<<history-next>>", self.history_next) + + def history_next(self, event): + self.history_do(0) + return "break" + + def history_prev(self, event): + self.history_do(1) + return "break" + + def _get_source(self, start, end): + # Get source code from start index to end index. Lines in the + # text control may be separated by sys.ps2 . + lines = string.split(self.text.get(start, end), self.output_sep) + return string.join(lines, "\n") + + def _put_source(self, where, source): + output = string.join(string.split(source, "\n"), self.output_sep) + self.text.insert(where, output) + + def history_do(self, reverse): + nhist = len(self.history) + pointer = self.history_pointer + prefix = self.history_prefix + if pointer is not None and prefix is not None: + if self.text.compare("insert", "!=", "end-1c") or \ + self._get_source("iomark", "end-1c") != self.history[pointer]: + pointer = prefix = None + if pointer is None or prefix is None: + prefix = self._get_source("iomark", "end-1c") + if reverse: + pointer = nhist + else: + pointer = -1 + nprefix = len(prefix) + while 1: + if reverse: + pointer = pointer - 1 + else: + pointer = pointer + 1 + if pointer < 0 or pointer >= nhist: + self.text.bell() + if self._get_source("iomark", "end-1c") != prefix: + self.text.delete("iomark", "end-1c") + self._put_source("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._put_source("iomark", item) + break + self.text.mark_set("insert", "end-1c") + self.text.see("insert") + self.text.tag_remove("sel", "1.0", "end") + self.history_pointer = pointer + self.history_prefix = prefix + + def history_store(self, source): + source = string.strip(source) + if len(source) > 2: + # avoid duplicates + try: + self.history.remove(source) + except ValueError: + pass + self.history.append(source) + self.history_pointer = None + self.history_prefix = None + + def recall(self, s): + s = string.strip(s) + self.text.tag_remove("sel", "1.0", "end") + self.text.delete("iomark", "end-1c") + self.text.mark_set("insert", "end-1c") + self.text.insert("insert", s) + self.text.see("insert") + diff --git a/Lib/idlelib/MultiScrolledLists.py b/Lib/idlelib/MultiScrolledLists.py new file mode 100644 index 0000000..32f6246 --- /dev/null +++ b/Lib/idlelib/MultiScrolledLists.py @@ -0,0 +1,138 @@ +# One or more ScrolledLists with HSeparators between them. +# There is a hierarchical relationship between them: +# the right list displays the substructure of the selected item +# in the left list. + +import string +from Tkinter import * +from WindowList import ListedToplevel +from Separator import HSeparator +from ScrolledList import ScrolledList + +class MultiScrolledLists: + + def __init__(self, root, nlists=2): + assert nlists >= 1 + self.root = root + self.nlists = nlists + self.path = [] + # create top + self.top = top = ListedToplevel(root) + top.protocol("WM_DELETE_WINDOW", self.close) + top.bind("<Escape>", self.close) + self.settitle() + # create frames and separators in between + self.frames = [] + self.separators = [] + last = top + for i in range(nlists-1): + sepa = HSeparator(last) + self.separators.append(sepa) + frame, last = sepa.parts() + self.frames.append(frame) + self.frames.append(last) + # create labels and lists + self.labels = [] + self.lists = [] + for i in range(nlists): + frame = self.frames[i] + label = Label(frame, text=self.subtitle(i), + relief="groove", borderwidth=2) + label.pack(fill="x") + self.labels.append(label) + list = ScrolledList(frame, width=self.width(i), + height=self.height(i)) + self.lists.append(list) + list.on_select = \ + lambda index, i=i, self=self: self.on_select(index, i) + list.on_double = \ + lambda index, i=i, self=self: self.on_double(index, i) + # fill leftmost list (rest get filled on demand) + self.fill(0) + # XXX one after_idle isn't enough; two are... + top.after_idle(self.call_pack_propagate_1) + + def call_pack_propagate_1(self): + self.top.after_idle(self.call_pack_propagate) + + def call_pack_propagate(self): + for frame in self.frames: + frame.pack_propagate(0) + + def close(self, event=None): + self.top.destroy() + + def settitle(self): + short = self.shorttitle() + long = self.longtitle() + if short and long: + title = short + " - " + long + elif short: + title = short + elif long: + title = long + else: + title = "Untitled" + icon = short or long or title + self.top.wm_title(title) + self.top.wm_iconname(icon) + + def longtitle(self): + # override this + return "Multi Scrolled Lists" + + def shorttitle(self): + # override this + return None + + def width(self, i): + # override this + return 20 + + def height(self, i): + # override this + return 10 + + def subtitle(self, i): + # override this + return "Column %d" % i + + def fill(self, i): + for k in range(i, self.nlists): + self.lists[k].clear() + self.labels[k].configure(text=self.subtitle(k)) + list = self.lists[i] + l = self.items(i) + for s in l: + list.append(s) + + def on_select(self, index, i): + item = self.lists[i].get(index) + del self.path[i:] + self.path.append(item) + if i+1 < self.nlists: + self.fill(i+1) + + def items(self, i): + # override this + l = [] + for k in range(10): + s = str(k) + if i > 0: + s = self.path[i-1] + "." + s + l.append(s) + return l + + def on_double(self, index, i): + pass + + +def main(): + root = Tk() + quit = Button(root, text="Exit", command=root.destroy) + quit.pack() + MultiScrolledLists(root, 4) + root.mainloop() + +if __name__ == "__main__": + main() diff --git a/Lib/idlelib/MultiStatusBar.py b/Lib/idlelib/MultiStatusBar.py new file mode 100644 index 0000000..c11db3e --- /dev/null +++ b/Lib/idlelib/MultiStatusBar.py @@ -0,0 +1,32 @@ +from Tkinter import * + +class MultiStatusBar(Frame): + + def __init__(self, master=None, **kw): + if master is None: + master = Tk() + apply(Frame.__init__, (self, master), kw) + self.labels = {} + + def set_label(self, name, text='', side=LEFT): + if not self.labels.has_key(name): + label = Label(self, bd=1, relief=SUNKEN, anchor=W) + label.pack(side=side) + self.labels[name] = label + else: + label = self.labels[name] + label.config(text=text) + +def _test(): + b = Frame() + c = Text(b) + c.pack(side=TOP) + a = MultiStatusBar(b) + a.set_label("one", "hello") + a.set_label("two", "world") + a.pack(side=BOTTOM, fill=X) + b.pack() + b.mainloop() + +if __name__ == '__main__': + _test() diff --git a/Lib/idlelib/NEWS.txt b/Lib/idlelib/NEWS.txt new file mode 100644 index 0000000..86cbc76 --- /dev/null +++ b/Lib/idlelib/NEWS.txt @@ -0,0 +1,130 @@ +(For a more detailed change log, see the file ChangeLog.) + +---------------------------------------------------------------------- + +New in IDLE 0.5 (2/15/2000) +------------------------- + +Tons of stuff, much of it contributed by Tim Peters and Mark Hammond: + +- Status bar, displaying current line/column (Moshe Zadka). + +- Better stack viewer, using tree widget. (XXX Only used by Stack +Viewer menu, not by the debugger.) + +- Format paragraph now recognizes Python block comments and reformats +them correctly (MH) + +- New version of pyclbr.py parses top-level functions and understands +much more of Python's syntax; this is reflected in the class and path +browsers (TP) + +- Much better auto-indent; knows how to indent the insides of +multi-line statements (TP) + +- Call tip window pops up when you type the name of a known function +followed by an open parenthesis. Hit ESC or click elsewhere in the +window to close the tip window (MH) + +- Comment out region now inserts ## to make it stand out more (TP) + +- New path and class browsers based on a tree widget that looks +familiar to Windows users + +- Reworked script running commands to be more intuitive: I/O now +always goes to the *Python Shell* window, and raw_input() works +correctly. You use F5 to import/reload a module: this adds the module +name to the __main__ namespace. You use Control-F5 to run a script: +this runs the script *in* the __main__ namespace. The latter also +sets sys.argv[] to the script name + +New in IDLE 0.4 (4/7/99) +------------------------ + +Most important change: a new menu entry "File -> Path browser", shows +a 4-column hierarchical browser which lets you browse sys.path, +directories, modules, and classes. Yes, it's a superset of the Class +browser menu entry. There's also a new internal module, +MultiScrolledLists.py, which provides the framework for this dialog. + +New in IDLE 0.3 (2/17/99) +------------------------- + +Most important changes: + +- Enabled support for running a module, with or without the debugger. +Output goes to a new window. Pressing F5 in a module is effectively a +reload of that module; Control-F5 loads it under the debugger. + +- Re-enable tearing off the Windows menu, and make a torn-off Windows +menu update itself whenever a window is opened or closed. + +- Menu items can now be have a checkbox (when the menu label starts +with "!"); use this for the Debugger and "Auto-open stack viewer" +(was: JIT stack viewer) menu items. + +- Added a Quit button to the Debugger API. + +- The current directory is explicitly inserted into sys.path. + +- Fix the debugger (when using Python 1.5.2b2) to use canonical +filenames for breakpoints, so these actually work. (There's still a +lot of work to be done to the management of breakpoints in the +debugger though.) + +- Closing a window that is still colorizing now actually works. + +- Allow dragging of the separator between the two list boxes in the +class browser. + +- Bind ESC to "close window" of the debugger, stack viewer and class +browser. It removes the selection highlighting in regular text +windows. (These are standard Windows conventions.) + +---------------------------------------------------------------------- + +New in IDLE 0.2 (1/8/99) +------------------------ + +Lots of changes; here are the highlights: + +General: + +- You can now write and configure your own IDLE extension modules; see +extend.txt. + + +File menu: + +The command to open the Python shell window is now in the File menu. + + +Edit menu: + +New Find dialog with more options; replace dialog; find in files dialog. + +Commands to tabify or untabify a region. + +Command to format a paragraph. + + +Debug menu: + +JIT (Just-In-Time) stack viewer toggle -- if set, the stack viewer +automaticall pops up when you get a traceback. + +Windows menu: + +Zoom height -- make the window full height. + + +Help menu: + +The help text now show up in a regular window so you can search and +even edit it if you like. + +---------------------------------------------------------------------- + +IDLE 0.1 was distributed with the Python 1.5.2b1 release on 12/22/98. + +====================================================================== diff --git a/Lib/idlelib/ObjectBrowser.py b/Lib/idlelib/ObjectBrowser.py new file mode 100644 index 0000000..94b59d6 --- /dev/null +++ b/Lib/idlelib/ObjectBrowser.py @@ -0,0 +1,151 @@ +# 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 + +from 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 1 + 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 1 + 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(`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 dispatch.has_key(t): + c = dispatch[t] + else: + c = ObjectTreeItem + return c(labeltext, object, setfunction) + +# Test script + +def test(): + import sys + from Tkinter import Toplevel + import PyShell + root = Toplevel(PyShell.root) + 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.expand() + +if __name__ == '__main__': + test() diff --git a/Lib/idlelib/OldStackViewer.py b/Lib/idlelib/OldStackViewer.py new file mode 100644 index 0000000..2fa4127 --- /dev/null +++ b/Lib/idlelib/OldStackViewer.py @@ -0,0 +1,276 @@ +import string +import sys +import os +from Tkinter import * +import linecache +from repr import Repr +from WindowList import ListedToplevel + +from ScrolledList import ScrolledList + + +class StackBrowser: + + def __init__(self, root, flist, stack=None): + self.top = top = ListedToplevel(root) + top.protocol("WM_DELETE_WINDOW", self.close) + top.bind("<Key-Escape>", self.close) + top.wm_title("Stack viewer") + top.wm_iconname("Stack") + # Create help label + self.helplabel = Label(top, + text="Click once to view variables; twice for source", + borderwidth=2, relief="groove") + self.helplabel.pack(fill="x") + # + self.sv = StackViewer(top, flist, self) + if stack is None: + stack = get_stack() + self.sv.load_stack(stack) + + def close(self, event=None): + self.top.destroy() + + localsframe = None + localsviewer = None + localsdict = None + globalsframe = None + globalsviewer = None + globalsdict = None + curframe = None + + def show_frame(self, (frame, lineno)): + if frame is self.curframe: + return + self.curframe = None + if frame.f_globals is not self.globalsdict: + self.show_globals(frame) + self.show_locals(frame) + self.curframe = frame + + def show_globals(self, frame): + title = "Global Variables" + if frame.f_globals.has_key("__name__"): + try: + name = str(frame.f_globals["__name__"]) + "" + except: + name = "" + if name: + title = title + " in module " + name + self.globalsdict = None + if self.globalsviewer: + self.globalsviewer.close() + self.globalsviewer = None + if not self.globalsframe: + self.globalsframe = Frame(self.top) + self.globalsdict = frame.f_globals + self.globalsviewer = NamespaceViewer( + self.globalsframe, + title, + self.globalsdict) + self.globalsframe.pack(fill="both", side="bottom") + + def show_locals(self, frame): + self.localsdict = None + if self.localsviewer: + self.localsviewer.close() + self.localsviewer = None + if frame.f_locals is not frame.f_globals: + title = "Local Variables" + code = frame.f_code + funcname = code.co_name + if funcname not in ("?", "", None): + title = title + " in " + funcname + if not self.localsframe: + self.localsframe = Frame(self.top) + self.localsdict = frame.f_locals + self.localsviewer = NamespaceViewer( + self.localsframe, + title, + self.localsdict) + self.localsframe.pack(fill="both", side="top") + else: + if self.localsframe: + self.localsframe.forget() + + +class StackViewer(ScrolledList): + + def __init__(self, master, flist, browser): + ScrolledList.__init__(self, master, width=80) + self.flist = flist + self.browser = browser + self.stack = [] + + def load_stack(self, stack, index=None): + self.stack = stack + self.clear() +## if len(stack) > 10: +## l["height"] = 10 +## self.topframe.pack(expand=1) +## else: +## l["height"] = len(stack) +## self.topframe.pack(expand=0) + 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 + sourceline = linecache.getline(filename, lineno) + 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): + if self.stack: + return ScrolledList.popup_event(self, event) + + def fill_menu(self): + 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): + if 0 <= index < len(self.stack): + self.browser.show_frame(self.stack[index]) + + def on_double(self, index): + 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.browser.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) + + +def get_stack(t=None, f=None): + if t is None: + t = sys.last_traceback + stack = [] + if t and t.tb_frame is f: + t = t.tb_next + while f is not None: + stack.append((f, f.f_lineno)) + if f is self.botframe: + break + f = f.f_back + stack.reverse() + while t is not None: + stack.append((t.tb_frame, t.tb_lineno)) + t = t.tb_next + return stack + + +def getexception(type=None, value=None): + if type is None: + 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 + + +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 + self.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): + 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) + l = Label(subframe, text=name) + l.grid(row=row, column=0, sticky="nw") + ## l = Label(subframe, text=svalue, justify="l", wraplength=300) + l = Entry(subframe, width=0, borderwidth=0) + l.insert(0, svalue) + ## l["state"] = "disabled" + 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/Lib/idlelib/OutputWindow.py b/Lib/idlelib/OutputWindow.py new file mode 100644 index 0000000..12280ad --- /dev/null +++ b/Lib/idlelib/OutputWindow.py @@ -0,0 +1,279 @@ +# changes by dscherer@cmu.edu +# - OutputWindow and OnDemandOutputWindow have been hastily +# extended to provide readline() support, an "iomark" separate +# from the "insert" cursor, and scrolling to clear the window. +# These changes are used by the ExecBinding module to provide +# standard input and output for user programs. Many of the new +# features are very similar to features of PyShell, which is a +# subclass of OutputWindow. Someone should make some sense of +# this. + +from Tkinter import * +from EditorWindow import EditorWindow +import re +import tkMessageBox + +from UndoDelegator import UndoDelegator + +class OutputUndoDelegator(UndoDelegator): + reading = 0 + # Forbid insert/delete before the I/O mark, in the blank lines after + # the output, or *anywhere* if we are not presently doing user input + def insert(self, index, chars, tags=None): + try: + if (self.delegate.compare(index, "<", "iomark") or + self.delegate.compare(index, ">", "endmark") or + (index!="iomark" and not self.reading)): + 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") or + self.delegate.compare(index1, ">", "endmark") or + (index2 and self.delegate.compare(index2, ">=", "endmark")) or + not self.reading): + self.delegate.bell() + return + except TclError: + pass + UndoDelegator.delete(self, index1, index2) + +class OutputWindow(EditorWindow): + """An editor window that can serve as an input and output file. + The input support has been rather hastily hacked in, and should + not be trusted. + """ + + UndoDelegator = OutputUndoDelegator + source_window = None + + def __init__(self, *args, **keywords): + if keywords.has_key('source_window'): + self.source_window = keywords['source_window'] + apply(EditorWindow.__init__, (self,) + args) + self.text.bind("<<goto-file-line>>", self.goto_file_line) + self.text.bind("<<newline-and-indent>>", self.enter_callback) + self.text.mark_set("iomark","1.0") + self.text.mark_gravity("iomark", LEFT) + self.text.mark_set("endmark","1.0") + + # Customize EditorWindow + + def ispythonsource(self, filename): + # No colorization needed + return 0 + + def short_title(self): + return "Output" + + def long_title(self): + return "" + + def maybesave(self): + # Override base class method -- don't ask any questions + if self.get_saved(): + return "yes" + else: + return "no" + + # Act as input file - incomplete + + def set_line_and_column(self, event=None): + index = self.text.index(INSERT) + if (self.text.compare(index, ">", "endmark")): + self.text.mark_set("insert", "endmark") + self.text.see("insert") + EditorWindow.set_line_and_column(self) + + reading = 0 + canceled = 0 + endoffile = 0 + + def readline(self): + save = self.reading + try: + self.reading = self.undo.reading = 1 + self.text.mark_set("insert", "iomark") + self.text.see("insert") + self.top.mainloop() + finally: + self.reading = self.undo.reading = save + line = self.text.get("input", "iomark") + if self.canceled: + self.canceled = 0 + raise KeyboardInterrupt + if self.endoffile: + self.endoffile = 0 + return "" + return line or '\n' + + def close(self): + self.interrupt() + return EditorWindow.close(self) + + def interrupt(self): + if self.reading: + self.endoffile = 1 + self.top.quit() + + def enter_callback(self, event): + if self.reading and self.text.compare("insert", ">=", "iomark"): + self.text.mark_set("input", "iomark") + self.text.mark_set("iomark", "insert") + self.write('\n',"iomark") + self.text.tag_add("stdin", "input", "iomark") + self.text.update_idletasks() + self.top.quit() # Break out of recursive mainloop() in raw_input() + + return "break" + + # Act as output file + + def write(self, s, tags=(), mark="iomark"): + self.text.mark_gravity(mark, RIGHT) + self.text.insert(mark, str(s), tags) + self.text.mark_gravity(mark, LEFT) + self.text.see(mark) + self.text.update() + + def writelines(self, l): + map(self.write, l) + + def flush(self): + pass + + # Our own right-button menu + + rmenu_specs = [ + ("Go to file/line", "<<goto-file-line>>"), + ] + + file_line_pats = [ + r'file "([^"]*)", line (\d+)', + r'([^\s]+)\((\d+)\)', + r'([^\s]+):\s*(\d+):', + ] + + 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.", + master=self.text) + return + filename, lineno = result + edit = self.untitled(filename) or self.flist.open(filename) + edit.gotoline(lineno) + edit.wakeup() + + def untitled(self, filename): + if filename!='Untitled' or not self.source_window or self.source_window.io.filename: + return None + return self.source_window + + def _file_line_helper(self, line): + for prog in self.file_line_progs: + m = prog.search(line) + if m: + break + else: + return None + filename, lineno = m.group(1, 2) + if not self.untitled(filename): + try: + f = open(filename, "r") + f.close() + except IOError: + return None + try: + return filename, int(lineno) + except TypeError: + return None + +# This classes now used by ExecBinding.py: + +class OnDemandOutputWindow: + source_window = None + + tagdefs = { + # XXX Should use IdlePrefs.ColorPrefs + "stdin": {"foreground": "black"}, + "stdout": {"foreground": "blue"}, + "stderr": {"foreground": "red"}, + } + + def __init__(self, flist): + self.flist = flist + self.owin = None + self.title = "Output" + self.close_hook = None + self.old_close = None + + def owclose(self): + if self.close_hook: + self.close_hook() + if self.old_close: + self.old_close() + + def set_title(self, title): + self.title = title + if self.owin and self.owin.text: + self.owin.saved_change_hook() + + def write(self, s, tags=(), mark="iomark"): + if not self.owin or not self.owin.text: + self.setup() + self.owin.write(s, tags, mark) + + def readline(self): + if not self.owin or not self.owin.text: + self.setup() + return self.owin.readline() + + def scroll_clear(self): + if self.owin and self.owin.text: + lineno = self.owin.getlineno("endmark") + self.owin.text.mark_set("insert","endmark") + self.owin.text.yview(float(lineno)) + self.owin.wakeup() + + def setup(self): + self.owin = owin = OutputWindow(self.flist, source_window = self.source_window) + owin.short_title = lambda self=self: self.title + text = owin.text + + self.old_close = owin.close_hook + owin.close_hook = self.owclose + + # xxx Bad hack: 50 blank lines at the bottom so that + # we can scroll the top of the window to the output + # cursor in scroll_clear(). There must be a better way... + owin.text.mark_gravity('endmark', LEFT) + owin.text.insert('iomark', '\n'*50) + owin.text.mark_gravity('endmark', RIGHT) + + for tag, cnf in self.tagdefs.items(): + if cnf: + apply(text.tag_configure, (tag,), cnf) + text.tag_raise('sel') diff --git a/Lib/idlelib/ParenMatch.py b/Lib/idlelib/ParenMatch.py new file mode 100644 index 0000000..a607e49 --- /dev/null +++ b/Lib/idlelib/ParenMatch.py @@ -0,0 +1,192 @@ +"""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. + +WARNING: This extension will fight with the CallTips extension, +because they both are interested in the KeyRelease-parenright event. +We'll have to fix IDLE to do something reasonable when two or more +extensions what to capture the same event. +""" + +import string + +import PyParse +from AutoIndent import AutoIndent, index2line +from IdleConf import idleconf + +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: + - fix interaction with CallTips + - 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 = [] + + keydefs = { + '<<flash-open-paren>>' : ('<KeyRelease-parenright>', + '<KeyRelease-bracketright>', + '<KeyRelease-braceright>'), + '<<check-restore>>' : ('<KeyPress>',), + } + + windows_keydefs = {} + unix_keydefs = {} + + iconf = idleconf.getsection('ParenMatch') + STYLE = iconf.getdef('style', 'default') + FLASH_DELAY = iconf.getint('flash-delay') + HILITE_CONFIG = iconf.getcolor('hilite') + BELL = iconf.getboolean('bell') + del iconf + + def __init__(self, editwin): + self.editwin = editwin + self.text = editwin.text + self.finder = LastOpenBracketFinder(editwin) + self.counter = 0 + self._restore = None + self.set_style(self.STYLE) + + 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_open_paren_event(self, event): + index = self.finder.find(keysym_type(event.keysym)) + if index is None: + self.warn_mismatched() + return + self._restore = 1 + self.create_tag(index) + self.set_timeout() + + def check_restore_event(self, event=None): + if self._restore: + self.text.tag_delete("paren") + self._restore = None + + def handle_restore_timer(self, timer_count): + if timer_count + 1 == self.counter: + self.check_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, index): + """Highlight the single paren that matches""" + self.text.tag_add("paren", index) + self.text.tag_config("paren", self.HILITE_CONFIG) + + def create_tag_expression(self, index): + """Highlight the entire expression""" + self.text.tag_add("paren", index, "insert") + 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""" + pass + + 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.editwin.text_frame.after(self.FLASH_DELAY, + lambda self=self, c=self.counter: \ + self.handle_restore_timer(c)) + self.counter = self.counter + 1 + +def keysym_type(ks): + # Not all possible chars or keysyms are checked because of the + # limited context in which the function is used. + if ks == "parenright" or ks == "(": + return "paren" + if ks == "bracketright" or ks == "[": + return "bracket" + if ks == "braceright" or ks == "{": + return "brace" + +class LastOpenBracketFinder: + num_context_lines = AutoIndent.num_context_lines + indentwidth = AutoIndent.indentwidth + tabwidth = AutoIndent.tabwidth + context_use_ps1 = AutoIndent.context_use_ps1 + + def __init__(self, editwin): + self.editwin = editwin + self.text = editwin.text + + def _find_offset_in_buf(self, lno): + y = PyParse.Parser(self.indentwidth, self.tabwidth) + for context in self.num_context_lines: + startat = max(lno - context, 1) + startatindex = `startat` + ".0" + # rawtext needs to contain everything up to the last + # character, which was the close paren. the parser also + # requires that the last line ends with "\n" + rawtext = self.text.get(startatindex, "insert")[:-1] + "\n" + 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) + i = y.get_last_open_bracket_pos() + return i, y.str + + def find(self, right_keysym_type): + """Return the location of the last open paren""" + lno = index2line(self.text.index("insert")) + i, buf = self._find_offset_in_buf(lno) + if i is None \ + or keysym_type(buf[i]) != right_keysym_type: + return None + lines_back = string.count(buf[i:], "\n") - 1 + # subtract one for the "\n" added to please the parser + upto_open = buf[:i] + j = string.rfind(upto_open, "\n") + 1 # offset of column 0 of line + offset = i - j + return "%d.%d" % (lno - lines_back, offset) + + def _build_char_in_string_func(self, startindex): + def inner(offset, startindex=startindex, + icis=self.editwin.is_char_in_string): + return icis(startindex + "%dc" % offset) + return inner + diff --git a/Lib/idlelib/PathBrowser.py b/Lib/idlelib/PathBrowser.py new file mode 100644 index 0000000..7ea2410 --- /dev/null +++ b/Lib/idlelib/PathBrowser.py @@ -0,0 +1,95 @@ +import os +import sys +import imp + +from TreeWidget import TreeItem +from ClassBrowser import ClassBrowser, ModuleBrowserTreeItem + +class PathBrowser(ClassBrowser): + + def __init__(self, flist): + self.init(flist) + + def settitle(self): + 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 0 + 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 not modules.has_key(mod_name): + modules[mod_name] = None + sorted.append((normed_name, name)) + allnames.remove(name) + sorted.sort() + return sorted + +def main(): + import PyShell + PathBrowser(PyShell.flist) + if sys.stdin is sys.__stdin__: + mainloop() + +if __name__ == "__main__": + main() diff --git a/Lib/idlelib/Percolator.py b/Lib/idlelib/Percolator.py new file mode 100644 index 0000000..9def5f4 --- /dev/null +++ b/Lib/idlelib/Percolator.py @@ -0,0 +1,85 @@ +from WidgetRedirector import WidgetRedirector +from 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 main(): + class Tracer(Delegator): + def __init__(self, name): + self.name = name + Delegator.__init__(self, None) + def insert(self, *args): + print self.name, ": insert", args + apply(self.delegate.insert, args) + def delete(self, *args): + print self.name, ": delete", args + apply(self.delegate.delete, args) + from Tkinter import * + root = Tk() + root.wm_protocol("WM_DELETE_WINDOW", root.quit) + text = Text() + text.pack() + text.focus_set() + p = Percolator(text) + t1 = Tracer("t1") + t2 = Tracer("t2") + p.insertfilter(t1) + p.insertfilter(t2) + root.mainloop() + p.removefilter(t2) + root.mainloop() + p.insertfilter(t2) + p.removefilter(t1) + root.mainloop() + +if __name__ == "__main__": + main() diff --git a/Lib/idlelib/PyParse.py b/Lib/idlelib/PyParse.py new file mode 100644 index 0000000..23b995c --- /dev/null +++ b/Lib/idlelib/PyParse.py @@ -0,0 +1,569 @@ +import string +import re +import sys + +# Reason last stmt is continued (or C_NONE if it's not). +C_NONE, C_BACKSLASH, C_STRING, C_BRACKET = range(4) + +if 0: # for throwaway debugging output + def dump(*stuff): + sys.__stdout__.write(string.join(map(str, stuff), " ") + "\n") + +# Find what looks like the start of a popular stmt. + +_synchre = re.compile(r""" + ^ + [ \t]* + (?: if + | for + | while + | else + | def + | return + | assert + | break + | class + | continue + | elif + | try + | except + | raise + | import + ) + \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 = string.join(_tran, '') +del ch + +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' + 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. + # + # Ack, hack: in the shell window this kills us, because there's + # no way to tell the differences between output, >>> etc and + # user input. Indeed, IDLE's first output line makes the rest + # look like it's in an unclosed paren!: + # Python 1.5.2 (#0, Apr 13 1999, ... + + def find_good_parse_start(self, use_ps1, is_char_in_string=None, + _rfind=string.rfind, + _synchre=_synchre): + str, pos = self.str, None + if use_ps1: + # shell window + ps1 = '\n' + sys.ps1 + i = _rfind(str, ps1) + if i >= 0: + pos = i + len(ps1) + # make it look like there's a newline instead + # of ps1 at the start -- hacking here once avoids + # repeated hackery later + self.str = str[:pos-1] + '\n' + str[pos:] + return pos + + # File window -- real work. + 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 = _rfind(str, ":\n", 0, limit) + if i < 0: + break + i = _rfind(str, '\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, _replace=string.replace, _find=string.find): + 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 = string.translate(str, _tran) + str = _replace(str, 'xxxxxxxx', 'x') + str = _replace(str, 'xxxx', 'x') + str = _replace(str, 'xx', 'x') + str = _replace(str, 'xx', 'x') + str = _replace(str, '\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 + 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 + continuation = C_STRING + continue # with outer loop + + if ch == '#': + # consume the comment + i = _find(str, '\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 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.lastch + # last non-whitespace character before optional trailing + # comment + # self.lastopenbracketpos + # if continuation is C_BRACKET, index of last open bracket + + def _study2(self, _rfind=string.rfind, _find=string.find, + _ws=string.whitespace): + 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 = _rfind(str, '\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 + while p < q: + # suck up all except ()[]{}'"#\\ + m = _chew_ordinaryre(str, p, q) + if m: + # we skipped at least one boring char + p = m.end() + # back up over totally boring whitespace + i = p-1 # index of last boring char + while i >= 0 and str[i] in " \t\n": + i = i-1 + if i >= 0: + lastch = str[i] + if p >= q: + break + + ch = str[p] + + if ch in "([{": + push_stack(p) + lastch = ch + p = p+1 + continue + + if ch in ")]}": + if stack: + del stack[-1] + lastch = ch + p = p+1 + 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. + lastch = ch + p = _match_stringre(str, p, q).end() + continue + + if ch == '#': + # consume comment and trailing newline + p = _find(str, '\n', p, q) + 1 + assert p > 0 + 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] + + # Assuming continuation is C_BRACKET, return the number + # of spaces the next line should be indented. + + def compute_bracket_indent(self, _find=string.find): + self._study2() + assert self.continuation == C_BRACKET + j = self.lastopenbracketpos + str = self.str + n = len(str) + origi = i = string.rfind(str, '\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 = _find(str, '\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(string.expandtabs(str[i:j], + 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 = string.find(str, '\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(string.expandtabs(str[self.stmt_start : + i], + 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 diff --git a/Lib/idlelib/PyShell.py b/Lib/idlelib/PyShell.py new file mode 100644 index 0000000..7ae7130 --- /dev/null +++ b/Lib/idlelib/PyShell.py @@ -0,0 +1,860 @@ +#! /usr/bin/env python + +# changes by dscherer@cmu.edu + +# the main() function has been replaced by a whole class, in order to +# address the constraint that only one process can sit on the port +# hard-coded into the loader. + +# It attempts to load the RPC protocol server and publish itself. If +# that fails, it assumes that some other copy of IDLE is already running +# on the port and attempts to contact it. It then uses the RPC mechanism +# to ask that copy to do whatever it was instructed (via the command +# line) to do. (Think netscape -remote). The handling of command line +# arguments for remotes is still very incomplete. + +# default behavior (no command line options) is to NOT start the Python +# Shell. If files are specified, they are opened, otherwise a single +# blank editor window opens. + +# If any command line -options are specified, a shell does appear. This +# is necessary to make the current semantics of the options make sense. + +import os +import spawn +import sys +import string +import getopt +import re +import protocol + +import linecache +from code import InteractiveInterpreter + +from Tkinter import * +import tkMessageBox + +from EditorWindow import EditorWindow, fixwordbreaks +from FileList import FileList +from ColorDelegator import ColorDelegator +from UndoDelegator import UndoDelegator +from OutputWindow import OutputWindow, OnDemandOutputWindow +from IdleConf import idleconf +import idlever + +# We need to patch linecache.checkcache, because we don't want it +# to throw away our <pyshell#...> entries. +# Rather than repeating its code here, we save those entries, +# then call the original function, and then restore the saved entries. +def linecache_checkcache(orig_checkcache=linecache.checkcache): + cache = linecache.cache + save = {} + for filename in cache.keys(): + if filename[:1] + filename[-1:] == '<>': + save[filename] = cache[filename] + orig_checkcache() + cache.update(save) +linecache.checkcache = linecache_checkcache + + +# Note: <<newline-and-indent>> event is defined in AutoIndent.py + +#$ event <<plain-newline-and-indent>> +#$ win <Control-j> +#$ unix <Control-j> + +#$ event <<beginning-of-line>> +#$ win <Control-a> +#$ win <Home> +#$ unix <Control-a> +#$ unix <Home> + +#$ event <<history-next>> +#$ win <Alt-n> +#$ unix <Alt-n> + +#$ event <<history-previous>> +#$ win <Alt-p> +#$ unix <Alt-p> + +#$ event <<interrupt-execution>> +#$ win <Control-c> +#$ unix <Control-c> + +#$ event <<end-of-file>> +#$ win <Control-d> +#$ unix <Control-d> + +#$ event <<open-stack-viewer>> + +#$ event <<toggle-debugger>> + + +class PyShellEditorWindow(EditorWindow): + + # Regular text edit window when a shell is present + # XXX ought to merge with regular editor window + + def __init__(self, *args): + apply(EditorWindow.__init__, (self,) + args) + self.text.bind("<<set-breakpoint-here>>", self.set_breakpoint_here) + self.text.bind("<<open-python-shell>>", self.flist.open_shell) + + rmenu_specs = [ + ("Set breakpoint here", "<<set-breakpoint-here>>"), + ] + + def set_breakpoint_here(self, event=None): + if not self.flist.pyshell or not self.flist.pyshell.interp.debugger: + self.text.bell() + return + self.flist.pyshell.interp.debugger.set_breakpoint_here(self) + + +class PyShellFileList(FileList): + + # File list when a shell is present + + EditorWindow = PyShellEditorWindow + + pyshell = None + + def open_shell(self, event=None): + if self.pyshell: + self.pyshell.wakeup() + else: + self.pyshell = PyShell(self) + self.pyshell.begin() + return self.pyshell + + +class ModifiedColorDelegator(ColorDelegator): + + # Colorizer for the shell window itself + + def recolorize_main(self): + self.tag_remove("TODO", "1.0", "iomark") + self.tag_add("SYNC", "1.0", "iomark") + ColorDelegator.recolorize_main(self) + + tagdefs = ColorDelegator.tagdefs.copy() + cconf = idleconf.getsection('Colors') + + tagdefs.update({ + "stdin": cconf.getcolor("stdin"), + "stdout": cconf.getcolor("stdout"), + "stderr": cconf.getcolor("stderr"), + "console": cconf.getcolor("console"), + "ERROR": cconf.getcolor("ERROR"), + None: cconf.getcolor("normal"), + }) + + +class ModifiedUndoDelegator(UndoDelegator): + + # 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 ModifiedInterpreter(InteractiveInterpreter): + + def __init__(self, tkconsole): + self.tkconsole = tkconsole + locals = sys.modules['__main__'].__dict__ + InteractiveInterpreter.__init__(self, locals=locals) + + 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") + except (OverflowError, SyntaxError): + self.tkconsole.resetoutput() + InteractiveInterpreter.showsyntaxerror(self, filename) + else: + self.runcode(code) + + def runsource(self, source): + # Extend base class to stuff the source in the line cache first + filename = self.stuffsource(source) + self.more = 0 + return InteractiveInterpreter.runsource(self, source, filename) + + def stuffsource(self, source): + # Stuff source in the filename cache + filename = "<pyshell#%d>" % self.gid + self.gid = self.gid + 1 + lines = string.split(source, "\n") + linecache.cache[filename] = len(source)+1, 0, lines, filename + return filename + + def showsyntaxerror(self, filename=None): + # Extend base class to color the offending position + # (instead of printing it and pointing at it with a caret) + text = self.tkconsole.text + stuff = self.unpackerror() + if not stuff: + self.tkconsole.resetoutput() + InteractiveInterpreter.showsyntaxerror(self, filename) + return + 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 string.letters + string.digits + "_": + text.tag_add("ERROR", pos + " wordstart", pos) + self.tkconsole.resetoutput() + self.write("SyntaxError: %s\n" % str(msg)) + + def unpackerror(self): + type, value, tb = sys.exc_info() + ok = type is SyntaxError + if ok: + try: + msg, (dummy_filename, lineno, offset, line) = value + except: + ok = 0 + if ok: + return msg, lineno, offset, line + else: + return None + + def showtraceback(self): + # Extend base class method to reset output properly + text = self.tkconsole.text + self.tkconsole.resetoutput() + self.checklinecache() + InteractiveInterpreter.showtraceback(self) + + def checklinecache(self): + c = linecache.cache + for key in c.keys(): + if key[:1] + key[-1:] != "<>": + del c[key] + + debugger = None + + def setdebugger(self, debugger): + self.debugger = debugger + + def getdebugger(self): + return self.debugger + + def runcode(self, code): + # Override base class method + debugger = self.debugger + try: + self.tkconsole.beginexecuting() + try: + if debugger: + debugger.run(code, self.locals) + else: + exec code in self.locals + except SystemExit: + if tkMessageBox.askyesno( + "Exit?", + "Do you want to exit altogether?", + default="yes", + master=self.tkconsole.text): + raise + else: + self.showtraceback() + if self.tkconsole.getvar("<<toggle-jit-stack-viewer>>"): + self.tkconsole.open_stack_viewer() + except: + self.showtraceback() + if self.tkconsole.getvar("<<toggle-jit-stack-viewer>>"): + self.tkconsole.open_stack_viewer() + + finally: + self.tkconsole.endexecuting() + + def write(self, s): + # Override base class write + self.tkconsole.console.write(s) + + +class PyShell(OutputWindow): + + shell_title = "Python Shell" + + # Override classes + ColorDelegator = ModifiedColorDelegator + UndoDelegator = ModifiedUndoDelegator + + # Override menu bar specs + menu_specs = PyShellEditorWindow.menu_specs[:] + menu_specs.insert(len(menu_specs)-2, ("debug", "_Debug")) + + # New classes + from IdleHistory import History + + def __init__(self, flist=None): + self.interp = ModifiedInterpreter(self) + if flist is None: + root = Tk() + fixwordbreaks(root) + root.withdraw() + flist = PyShellFileList(root) + + OutputWindow.__init__(self, flist, None, None) + + import __builtin__ + __builtin__.quit = __builtin__.exit = "To exit, type Ctrl-D." + + self.auto = self.extensions["AutoIndent"] # Required extension + self.auto.config(usetabs=1, indentwidth=8, context_use_ps1=1) + + 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("<<beginning-of-line>>", self.home_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("<<open-python-shell>>", self.flist.open_shell) + text.bind("<<toggle-jit-stack-viewer>>", self.toggle_jit_stack_viewer) + + self.save_stdout = sys.stdout + self.save_stderr = sys.stderr + self.save_stdin = sys.stdin + sys.stdout = PseudoFile(self, "stdout") + sys.stderr = PseudoFile(self, "stderr") + sys.stdin = self + self.console = PseudoFile(self, "console") + + self.history = self.History(self.text) + + reading = 0 + executing = 0 + canceled = 0 + endoffile = 0 + + def toggle_debugger(self, event=None): + if self.executing: + tkMessageBox.showerror("Don't debug now", + "You can only toggle the debugger when idle", + master=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() + self.resetoutput() + self.console.write("[DEBUG OFF]\n") + sys.ps1 = ">>> " + self.showprompt() + self.set_debugger_indicator() + + def open_debugger(self): + import Debugger + self.interp.setdebugger(Debugger.Debugger(self)) + sys.ps1 = "[DEBUG ON]\n>>> " + self.showprompt() + self.set_debugger_indicator() + + def beginexecuting(self): + # Helper for ModifiedInterpreter + self.resetoutput() + self.executing = 1 + ##self._cancel_check = self.cancel_check + ##sys.settrace(self._cancel_check) + + def endexecuting(self): + # Helper for ModifiedInterpreter + ##sys.settrace(None) + ##self._cancel_check = None + self.executing = 0 + self.canceled = 0 + + def close(self): + # Extend base class method + if self.executing: + # XXX Need to ask a question here + if not tkMessageBox.askokcancel( + "Kill?", + "The program is still running; do you want to kill it?", + default="ok", + master=self.text): + return "cancel" + self.canceled = 1 + if self.reading: + self.top.quit() + return "cancel" + return PyShellEditorWindow.close(self) + + def _close(self): + self.close_debugger() + # 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.auto = None + self.flist.pyshell = None + self.history = None + OutputWindow._close(self) # Really EditorWindow._close + + def ispythonsource(self, filename): + # Override this so EditorWindow never removes the colorizer + return 1 + + def short_title(self): + return self.shell_title + + def begin(self): + self.resetoutput() + self.write("Python %s on %s\n%s\nIDLE %s -- press F1 for help\n" % + (sys.version, sys.platform, sys.copyright, + idlever.IDLE_VERSION)) + try: + sys.ps1 + except AttributeError: + sys.ps1 = ">>> " + self.showprompt() + import Tkinter + Tkinter._default_root = None + + def interact(self): + self.begin() + self.top.mainloop() + + def readline(self): + save = self.reading + try: + self.reading = 1 + self.top.mainloop() + finally: + self.reading = save + line = self.text.get("iomark", "end-1c") + self.resetoutput() + if self.canceled: + self.canceled = 0 + raise KeyboardInterrupt + if self.endoffile: + self.endoffile = 0 + return "" + return line + + def isatty(self): + return 1 + + def cancel_callback(self, event): + 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.write("KeyboardInterrupt\n") + self.showprompt() + return "break" + self.endoffile = 0 + self.canceled = 1 + if self.reading: + self.top.quit() + 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: +## if not tkMessageBox.askokcancel( +## "Exit?", +## "Are you sure you want to exit?", +## default="ok", master=self.text): +## return "break" + self.resetoutput() + self.close() + else: + self.canceled = 0 + self.endoffile = 1 + self.top.quit() + return "break" + + def home_callback(self, event): + if event.state != 0 and event.keysym == "Home": + return # <Modifier-Home>; fall back to class binding + if self.text.compare("iomark", "<=", "insert") and \ + self.text.compare("insert linestart", "<=", "iomark"): + self.text.mark_set("insert", "iomark") + self.text.tag_remove("sel", "1.0", "end") + self.text.see("insert") + 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.auto.auto_indent(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) + 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])) + 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])) + return "break" + # No stdin mark -- just get the current line + self.recall(self.text.get("insert linestart", "insert lineend")) + return "break" + # 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 string.strip(s): + 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.auto.auto_indent(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.auto.auto_indent(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): + if self.history: + self.history.recall(s) + + 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] + more = self.interp.runsource(line) + if not more: + self.showprompt() + + def cancel_check(self, frame, what, args, + dooneevent=tkinter.dooneevent, + dontwait=tkinter.DONT_WAIT): + # Hack -- use the debugger hooks to be able to handle events + # and interrupt execution at any time. + # This slows execution down quite a bit, so you may want to + # disable this (by not calling settrace() in runcode() above) + # for full-bore (uninterruptable) speed. + # XXX This should become a user option. + if self.canceled: + return + dooneevent(dontwait) + if self.canceled: + self.canceled = 0 + raise KeyboardInterrupt + return self._cancel_check + + def open_stack_viewer(self, event=None): + try: + sys.last_traceback + except: + tkMessageBox.showerror("No stack trace", + "There is no stack trace yet.\n" + "(sys.last_traceback is not defined)", + master=self.text) + return + from StackViewer import StackBrowser + sv = StackBrowser(self.root, self.flist) + + def showprompt(self): + self.resetoutput() + try: + s = str(sys.ps1) + except: + s = "" + self.console.write(s) + self.text.mark_set("insert", "end-1c") + + def resetoutput(self): + source = self.text.get("iomark", "end-1c") + if self.history: + self.history.history_store(source) + if self.text.get("end-2c") != "\n": + self.text.insert("end-1c", "\n") + self.text.mark_set("iomark", "end-1c") + sys.stdout.softspace = 0 + + def write(self, s, tags=()): + self.text.mark_gravity("iomark", "right") + OutputWindow.write(self, s, tags, "iomark") + self.text.mark_gravity("iomark", "left") + if self.canceled: + self.canceled = 0 + raise KeyboardInterrupt + +class PseudoFile: + + def __init__(self, shell, tags): + self.shell = shell + self.tags = tags + + def write(self, s): + self.shell.write(s, self.tags) + + def writelines(self, l): + map(self.write, l) + + def flush(self): + pass + + def isatty(self): + return 1 + +usage_msg = """\ +usage: idle.py [-c command] [-d] [-e] [-s] [-t title] [arg] ... + +-c command run this command +-d enable debugger +-e edit mode; arguments are files to be edited +-s run $IDLESTARTUP or $PYTHONSTARTUP before anything else +-t title set title of shell window + +When neither -c nor -e is used, and there are arguments, and the first +argument is not '-', the first argument is run as a script. Remaining +arguments are arguments to the script or to the command run by -c. +""" + +class usageError: + def __init__(self, string): self.string = string + def __repr__(self): return self.string + +class main: + def __init__(self): + try: + self.server = protocol.Server(connection_hook = self.address_ok) + protocol.publish( 'IDLE', self.connect ) + self.main( sys.argv[1:] ) + return + except protocol.connectionLost: + try: + client = protocol.Client() + IDLE = client.getobject('IDLE') + if IDLE: + try: + IDLE.remote( sys.argv[1:] ) + except usageError, msg: + sys.stderr.write("Error: %s\n" % str(msg)) + sys.stderr.write(usage_msg) + return + except protocol.connectionLost: + pass + + # xxx Should scream via Tk() + print "Something already has our socket, but it won't open a window for me!" + print "Unable to proceed." + + def idle(self): + spawn.kill_zombies() + self.server.rpc_loop() + root.after(25, self.idle) + + # We permit connections from localhost only + def address_ok(self, addr): + return addr[0] == '127.0.0.1' + + def connect(self, client, addr): + return self + + def remote( self, argv ): + # xxx Should make this behavior match the behavior in main, or redo + # command line options entirely. + + try: + opts, args = getopt.getopt(argv, "c:deist:") + except getopt.error, msg: + raise usageError(msg) + + for filename in args: + flist.open(filename) + if not args: + flist.new() + + def main( self, argv ): + cmd = None + edit = 0 + noshell = 1 + + debug = 0 + startup = 0 + + try: + opts, args = getopt.getopt(argv, "c:deist:") + except getopt.error, msg: + sys.stderr.write("Error: %s\n" % str(msg)) + sys.stderr.write(usage_msg) + sys.exit(2) + + for o, a in opts: + noshell = 0 + if o == '-c': + cmd = a + if o == '-d': + debug = 1 + if o == '-e': + edit = 1 + if o == '-s': + startup = 1 + if o == '-t': + PyShell.shell_title = a + + if noshell: edit=1 + + if not edit: + if cmd: + sys.argv = ["-c"] + args + else: + sys.argv = args or [""] + + for i in range(len(sys.path)): + sys.path[i] = os.path.abspath(sys.path[i]) + + pathx = [] + if edit: + for filename in args: + pathx.append(os.path.dirname(filename)) + elif args and args[0] != "-": + pathx.append(os.path.dirname(args[0])) + else: + pathx.append(os.curdir) + for dir in pathx: + dir = os.path.abspath(dir) + if not dir in sys.path: + sys.path.insert(0, dir) + + global flist, root + root = Tk() + fixwordbreaks(root) + root.withdraw() + flist = PyShellFileList(root) + + if edit: + for filename in args: + flist.open(filename) + if not args: + flist.new() + + #dbg=OnDemandOutputWindow(flist) + #dbg.set_title('Internal IDLE Problem') + #sys.stdout = PseudoFile(dbg,['stdout']) + #sys.stderr = PseudoFile(dbg,['stderr']) + + if noshell: + flist.pyshell = None + else: + shell = PyShell(flist) + interp = shell.interp + flist.pyshell = shell + + if startup: + filename = os.environ.get("IDLESTARTUP") or \ + os.environ.get("PYTHONSTARTUP") + if filename and os.path.isfile(filename): + interp.execfile(filename) + + if debug: + shell.open_debugger() + if cmd: + interp.execsource(cmd) + elif not edit and args and args[0] != "-": + interp.execfile(args[0]) + + shell.begin() + + self.idle() + root.mainloop() + root.destroy() + + +if __name__ == "__main__": + main() diff --git a/Lib/idlelib/README.txt b/Lib/idlelib/README.txt new file mode 100644 index 0000000..1b065ae --- /dev/null +++ b/Lib/idlelib/README.txt @@ -0,0 +1,121 @@ +EXPERIMENTAL LOADER IDLE 2000-05-29 +----------------------------------- + + David Scherer <dscherer@cmu.edu> + +This is a modification of the CVS version of IDLE 0.5, updated as of +2000-03-09. It is alpha software and might be unstable. If it breaks, +you get to keep both pieces. + +If you have problems or suggestions, you should either contact me or +post to the list at http://www.python.org/mailman/listinfo/idle-dev +(making it clear that you are using this modified version of IDLE). + +Changes: + + The ExecBinding module, a replacement for ScriptBinding, executes + programs in a separate process, piping standard I/O through an RPC + mechanism to an OnDemandOutputWindow in IDLE. It supports executing + unnamed programs (through a temporary file). It does not yet support + debugging. + + When running programs with ExecBinding, tracebacks will be clipped + to exclude system modules. If, however, a system module calls back + into the user program, that part of the traceback will be shown. + + The OnDemandOutputWindow class has been improved. In particular, + it now supports a readline() function used to implement user input, + and a scroll_clear() operation which is used to hide the output of + a previous run by scrolling it out of the window. + + Startup behavior has been changed. By default IDLE starts up with + just a blank editor window, rather than an interactive window. Opening + a file in such a blank window replaces the (nonexistent) contents of + that window instead of creating another window. Because of the need to + have a well-known port for the ExecBinding protocol, only one copy of + IDLE can be running. Additional invocations use the RPC mechanism to + report their command line arguments to the copy already running. + + The menus have been reorganized. In particular, the excessively large + 'edit' menu has been split up into 'edit', 'format', and 'run'. + + 'Python Documentation' now works on Windows, if the win32api module is + present. + + A few key bindings have been changed: F1 now loads Python Documentation + instead of the IDLE help; shift-TAB is now a synonym for unindent. + +New modules: + ExecBinding.py Executes program through loader + loader.py Bootstraps user program + protocol.py RPC protocol + Remote.py User-process interpreter + spawn.py OS-specific code to start programs + +Files modified: + autoindent.py ( bindings tweaked ) + bindings.py ( menus reorganized ) + config.txt ( execbinding enabled ) + editorwindow.py ( new menus, fixed 'Python Documentation' ) + filelist.py ( hook for "open in same window" ) + formatparagraph.py ( bindings tweaked ) + idle.bat ( removed absolute pathname ) + idle.pyw ( weird bug due to import with same name? ) + iobinding.py ( open in same window, EOL convention ) + keydefs.py ( bindings tweaked ) + outputwindow.py ( readline, scroll_clear, etc ) + pyshell.py ( changed startup behavior ) + readme.txt ( <Recursion on file with id=1234567> ) + +IDLE 0.5 - February 2000 +------------------------ + +This is an early release of IDLE, my own attempt at a Tkinter-based +IDE for Python. + +For news about this release, see the file NEWS.txt. (For a more +detailed change log, see the file ChangeLog.) + +FEATURES + +IDLE has the following features: + +- coded in 100% pure Python, using the Tkinter GUI toolkit (i.e. Tcl/Tk) + +- cross-platform: works on Windows and Unix (on the Mac, there are +currently problems with Tcl/Tk) + +- multi-window text editor with multiple undo, Python colorizing +and many other features, e.g. smart indent and call tips + +- Python shell window (a.k.a. interactive interpreter) + +- debugger (not complete, but you can set breakpoints, view and step) + +USAGE + +The main program is in the file "idle.py"; on Unix, you should be able +to run it by typing "./idle.py" to your shell. On Windows, you can +run it by double-clicking it; you can use idle.pyw to avoid popping up +a DOS console. If you want to pass command line arguments on Windows, +use the batch file idle.bat. + +Command line arguments: files passed on the command line are executed, +not opened for editing, unless you give the -e command line option. +Try "./idle.py -h" to see other command line options. + +IDLE requires Python 1.5.2, so it is currently only usable with a +Python 1.5.2 distribution. (An older version of IDLE is distributed +with Python 1.5.2; you can drop this version on top of it.) + +COPYRIGHT + +IDLE is covered by the standard Python copyright notice +(http://www.python.org/doc/Copyright.html). + +FEEDBACK + +(removed, since Guido probably doesn't want complaints about my +changes) + +--Guido van Rossum (home page: http://www.python.org/~guido/) diff --git a/Lib/idlelib/Remote.py b/Lib/idlelib/Remote.py new file mode 100644 index 0000000..facba78 --- /dev/null +++ b/Lib/idlelib/Remote.py @@ -0,0 +1,101 @@ +"""Remote + This module is imported by the loader and serves to control + the execution of the user program. It presently executes files + and reports exceptions to IDLE. It could be extended to provide + other services, such as interactive mode and debugging. To that + end, it could be a subclass of e.g. InteractiveInterpreter. + + Two other classes, pseudoIn and pseudoOut, are file emulators also + used by loader. +""" +import sys, os +import traceback + +class Remote: + def __init__(self, main, master): + self.main = main + self.master = master + self.this_file = self.canonic( self.__init__.im_func.func_code.co_filename ) + + def canonic(self, path): + return os.path.normcase(os.path.abspath(path)) + + def mainloop(self): + while 1: + args = self.master.get_command() + + try: + f = getattr(self,args[0]) + apply(f,args[1:]) + except: + if not self.report_exception(): raise + + def finish(self): + sys.exit() + + def run(self, *argv): + sys.argv = argv + + path = self.canonic( argv[0] ) + dir = self.dir = os.path.dirname(path) + os.chdir(dir) + + sys.path[0] = dir + + usercode = open(path) + exec usercode in self.main + + def report_exception(self): + try: + type, value, tb = sys.exc_info() + sys.last_type = type + sys.last_value = value + sys.last_traceback = tb + + tblist = traceback.extract_tb(tb) + + # Look through the traceback, canonicalizing filenames and + # eliminating leading and trailing system modules. + first = last = 1 + for i in range(len(tblist)): + filename, lineno, name, line = tblist[i] + filename = self.canonic(filename) + tblist[i] = filename, lineno, name, line + + dir = os.path.dirname(filename) + if filename == self.this_file: + first = i+1 + elif dir==self.dir: + last = i+1 + + # Canonicalize the filename in a syntax error, too: + if type is SyntaxError: + try: + msg, (filename, lineno, offset, line) = value + filename = self.canonic(filename) + value = msg, (filename, lineno, offset, line) + except: + pass + + return self.master.program_exception( type, value, tblist, first, last ) + finally: + # avoid any circular reference through the traceback + del tb + +class pseudoIn: + def __init__(self, readline): + self.readline = readline + def isatty(): + return 1 + +class pseudoOut: + def __init__(self, func, **kw): + self.func = func + self.kw = kw + def write(self, *args): + return apply( self.func, args, self.kw ) + def writelines(self, l): + map(self.write, l) + def flush(self): + pass + diff --git a/Lib/idlelib/ReplaceDialog.py b/Lib/idlelib/ReplaceDialog.py new file mode 100644 index 0000000..e29d4d6 --- /dev/null +++ b/Lib/idlelib/ReplaceDialog.py @@ -0,0 +1,172 @@ +import string +import os +import re +import fnmatch +from Tkinter import * +import tkMessageBox +import SearchEngine +from SearchDialogBase import SearchDialogBase + +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) + + 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): + self.do_replace() + self.do_find(0) + + 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 = re.pcre_expand(m, repl) + 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 0 + text = self.text + res = self.engine.search_text(text, None, ok) + if not res: + text.bell() + return 0 + 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 1 + + def do_replace(self): + prog = self.engine.getprog() + if not prog: + return 0 + 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 0 + new = re.pcre_expand(m, self.replvar.get()) + 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 1 + + 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") diff --git a/Lib/idlelib/ScriptBinding.py b/Lib/idlelib/ScriptBinding.py new file mode 100644 index 0000000..aa46c68 --- /dev/null +++ b/Lib/idlelib/ScriptBinding.py @@ -0,0 +1,169 @@ +"""Extension to execute code outside the Python shell window. + +This adds the following commands (to the Edit menu, until there's a +separate Python menu): + +- Check module (Alt-F5) does a full syntax check of the current module. +It also runs the tabnanny to catch any inconsistent tabs. + +- Import module (F5) is equivalent to either import or reload of the +current module. The window must have been saved previously. The +module is added to sys.modules, and is also added to the __main__ +namespace. Output goes to the shell window. + +- Run module (Control-F5) does the same but executes the module's +code in the __main__ namespace. + +""" + +import sys +import os +import imp +import tkMessageBox + +indent_message = """Error: Inconsistent indentation detected! + +This means that either: + +(1) your indentation is outright incorrect (easy to fix), or + +(2) your indentation mixes tabs and spaces in a way that depends on \ +how many spaces a tab is worth. + +To fix case 2, change all tabs to spaces by using Select All followed \ +by Untabify Region (both in the Edit menu).""" + +class ScriptBinding: + + keydefs = { + '<<check-module>>': ['<Alt-F5>', '<Meta-F5>'], + '<<import-module>>': ['<F5>'], + '<<run-script>>': ['<Control-F5>'], + } + + menudefs = [ + ('edit', [None, + ('Check module', '<<check-module>>'), + ('Import module', '<<import-module>>'), + ('Run script', '<<run-script>>'), + ] + ), + ] + + 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.flist.root + + def check_module_event(self, event): + filename = self.getfilename() + if not filename: + return + if not self.tabnanny(filename): + return + if not self.checksyntax(filename): + return + + def tabnanny(self, filename): + import tabnanny + import tokenize + tabnanny.reset_globals() + f = open(filename, 'r') + try: + tokenize.tokenize(f.readline, tabnanny.tokeneater) + except tokenize.TokenError, msg: + self.errorbox("Token error", + "Token error:\n%s" % str(msg)) + return 0 + except tabnanny.NannyNag, nag: + # The error messages from tabnanny are too confusing... + self.editwin.gotoline(nag.get_lineno()) + self.errorbox("Tab/space error", indent_message) + return 0 + return 1 + + def checksyntax(self, filename): + f = open(filename, 'r') + source = f.read() + f.close() + if '\r' in source: + import re + source = re.sub(r"\r\n", "\n", source) + if source and source[-1] != '\n': + source = source + '\n' + try: + compile(source, filename, "exec") + except (SyntaxError, OverflowError), err: + try: + msg, (errorfilename, lineno, offset, line) = err + if not errorfilename: + err.args = msg, (filename, lineno, offset, line) + err.filename = filename + except: + lineno = None + msg = "*** " + str(err) + if lineno: + self.editwin.gotoline(lineno) + self.errorbox("Syntax error", + "There's an error in your program:\n" + msg) + return 1 + + def import_module_event(self, event): + filename = self.getfilename() + if not filename: + return + + modname, ext = os.path.splitext(os.path.basename(filename)) + if sys.modules.has_key(modname): + mod = sys.modules[modname] + else: + mod = imp.new_module(modname) + sys.modules[modname] = mod + mod.__file__ = filename + setattr(sys.modules['__main__'], modname, mod) + + dir = os.path.dirname(filename) + dir = os.path.normpath(os.path.abspath(dir)) + if dir not in sys.path: + sys.path.insert(0, dir) + + flist = self.editwin.flist + shell = flist.open_shell() + interp = shell.interp + interp.runcode("reload(%s)" % modname) + + def run_script_event(self, event): + filename = self.getfilename() + if not filename: + return + + flist = self.editwin.flist + shell = flist.open_shell() + interp = shell.interp + if (not sys.argv or + os.path.basename(sys.argv[0]) != os.path.basename(filename)): + # XXX Too often this discards arguments the user just set... + sys.argv = [filename] + interp.execfile(filename) + + def getfilename(self): + # Logic to make sure we have a saved filename + # XXX Better logic would offer to save! + if not self.editwin.get_saved(): + self.errorbox("Not saved", + "Please save first!") + self.editwin.text.focus_set() + return + filename = self.editwin.io.filename + if not filename: + self.errorbox("No file name", + "This window has no file name") + return + return filename + + def errorbox(self, title, message): + # XXX This should really be a function of EditorWindow... + tkMessageBox.showerror(title, message, master=self.editwin.text) + self.editwin.text.focus_set() diff --git a/Lib/idlelib/ScrolledList.py b/Lib/idlelib/ScrolledList.py new file mode 100644 index 0000000..7fb1c20 --- /dev/null +++ b/Lib/idlelib/ScrolledList.py @@ -0,0 +1,139 @@ +from Tkinter import * + +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) + 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 test(): + root = Tk() + root.protocol("WM_DELETE_WINDOW", root.destroy) + class MyScrolledList(ScrolledList): + def fill_menu(self): self.menu.add_command(label="pass") + def on_select(self, index): print "select", self.get(index) + def on_double(self, index): print "double", self.get(index) + s = MyScrolledList(root) + for i in range(30): + s.append("item %02d" % i) + return root + +def main(): + root = test() + root.mainloop() + +if __name__ == '__main__': + main() diff --git a/Lib/idlelib/SearchBinding.py b/Lib/idlelib/SearchBinding.py new file mode 100644 index 0000000..5943e3b --- /dev/null +++ b/Lib/idlelib/SearchBinding.py @@ -0,0 +1,97 @@ +import tkSimpleDialog + +###$ event <<find>> +###$ win <Control-f> +###$ unix <Control-u><Control-u><Control-s> + +###$ event <<find-again>> +###$ win <Control-g> +###$ win <F3> +###$ unix <Control-u><Control-s> + +###$ event <<find-selection>> +###$ win <Control-F3> +###$ unix <Control-s> + +###$ event <<find-in-files>> +###$ win <Alt-F3> + +###$ event <<replace>> +###$ win <Control-h> + +###$ event <<goto-line>> +###$ win <Alt-g> +###$ unix <Alt-g> + +class SearchBinding: + + windows_keydefs = { + '<<find-again>>': ['<Control-g>', '<F3>'], + '<<find-in-files>>': ['<Alt-F3>'], + '<<find-selection>>': ['<Control-F3>'], + '<<find>>': ['<Control-f>'], + '<<replace>>': ['<Control-h>'], + '<<goto-line>>': ['<Alt-g>'], + } + + unix_keydefs = { + '<<find-again>>': ['<Control-u><Control-s>'], + '<<find-in-files>>': ['<Alt-s>', '<Meta-s>'], + '<<find-selection>>': ['<Control-s>'], + '<<find>>': ['<Control-u><Control-u><Control-s>'], + '<<replace>>': ['<Control-r>'], + '<<goto-line>>': ['<Alt-g>', '<Meta-g>'], + } + + menudefs = [ + ('edit', [ + 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>>'), + ]), + ] + + def __init__(self, editwin): + self.editwin = editwin + + def find_event(self, event): + import SearchDialog + SearchDialog.find(self.editwin.text) + return "break" + + def find_again_event(self, event): + import SearchDialog + SearchDialog.find_again(self.editwin.text) + return "break" + + def find_selection_event(self, event): + import SearchDialog + SearchDialog.find_selection(self.editwin.text) + return "break" + + def find_in_files_event(self, event): + import GrepDialog + GrepDialog.grep(self.editwin.text, self.editwin.io, self.editwin.flist) + return "break" + + def replace_event(self, event): + import ReplaceDialog + ReplaceDialog.replace(self.editwin.text) + return "break" + + def goto_line_event(self, event): + text = self.editwin.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") diff --git a/Lib/idlelib/SearchDialog.py b/Lib/idlelib/SearchDialog.py new file mode 100644 index 0000000..0f0cb18 --- /dev/null +++ b/Lib/idlelib/SearchDialog.py @@ -0,0 +1,67 @@ +from Tkinter import * +import SearchEngine +from 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): + return _setup(text).open(text) + +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): + f = SearchDialogBase.create_widgets(self) + self.make_button("Find", self.default_command, 1) + + def default_command(self, event=None): + if not self.engine.getprog(): + return + if self.find_again(self.text): + self.close() + + def find_again(self, text): + if not self.engine.getpat(): + self.open(text) + return 0 + if not self.engine.getprog(): + return 0 + 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 0 + 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 1 + else: + text.bell() + return 0 + + def find_selection(self, text): + pat = text.get("sel.first", "sel.last") + if pat: + self.engine.setcookedpat(pat) + return self.find_again(text) diff --git a/Lib/idlelib/SearchDialogBase.py b/Lib/idlelib/SearchDialogBase.py new file mode 100644 index 0000000..faf5269 --- /dev/null +++ b/Lib/idlelib/SearchDialogBase.py @@ -0,0 +1,129 @@ +import string +from Tkinter import * + +class SearchDialogBase: + + title = "Search Dialog" + icon = "Search" + needwrapbutton = 1 + + def __init__(self, root, engine): + self.root = root + self.engine = engine + self.top = None + + def open(self, text): + self.text = text + if not self.top: + self.create_widgets() + else: + self.top.deiconify() + self.top.tkraise() + self.ent.focus_set() + self.ent.selection_range(0, "end") + self.ent.icursor(0) + self.top.grab_set() + + def close(self, event=None): + if self.top: + self.top.grab_release() + self.top.withdraw() + + def create_widgets(self): + 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, weight=0) + self.top.grid_columnconfigure(1, weight=100) + + self.create_entries() + self.create_option_buttons() + self.create_other_buttons() + return self.create_command_buttons() + + def make_entry(self, label, var): + l = Label(self.top, text=label) + l.grid(row=self.row, col=0, sticky="w") + e = Entry(self.top, textvariable=var, exportselection=0) + e.grid(row=self.row, col=1, sticky="we") + self.row = self.row + 1 + return e + + def make_frame(self): + f = Frame(self.top) + f.grid(row=self.row, col=0, columnspan=2, sticky="we") + self.row = self.row + 1 + return f + + def make_button(self, label, command, isdef=0, side="left"): + b = Button(self.buttonframe, + text=label, command=command, + default=isdef and "active" or "normal") + b.pack(side=side) + return b + + def create_entries(self): + self.ent = self.make_entry("Find:", self.engine.patvar) + + def create_option_buttons(self): + f = self.make_frame() + + btn = Checkbutton(f, anchor="w", + variable=self.engine.revar, + text="Regular expression") + btn.pack(side="left", fill="both") + if self.engine.isre(): + btn.select() + + btn = Checkbutton(f, anchor="w", + variable=self.engine.casevar, + text="Match case") + btn.pack(side="left", fill="both") + if self.engine.iscase(): + btn.select() + + btn = Checkbutton(f, anchor="w", + variable=self.engine.wordvar, + text="Whole word") + btn.pack(side="left", fill="both") + if self.engine.isword(): + btn.select() + + if self.needwrapbutton: + btn = Checkbutton(f, anchor="w", + variable=self.engine.wrapvar, + text="Wrap around") + btn.pack(side="left", fill="both") + if self.engine.iswrap(): + btn.select() + + def create_other_buttons(self): + f = self.make_frame() + + lbl = Label(f, text="Direction: ") + lbl.pack(side="left") + + btn = Radiobutton(f, anchor="w", + variable=self.engine.backvar, value=1, + text="Up") + btn.pack(side="left", fill="both") + if self.engine.isback(): + btn.select() + + btn = Radiobutton(f, anchor="w", + variable=self.engine.backvar, value=0, + text="Down") + btn.pack(side="left", fill="both") + if not self.engine.isback(): + btn.select() + + def create_command_buttons(self): + f = self.buttonframe = self.make_frame() + b = self.make_button("close", self.close, side="right") + b.lower() diff --git a/Lib/idlelib/SearchEngine.py b/Lib/idlelib/SearchEngine.py new file mode 100644 index 0000000..e379751 --- /dev/null +++ b/Lib/idlelib/SearchEngine.py @@ -0,0 +1,221 @@ +import string +import re +from Tkinter import * +import tkMessageBox + +def get(root): + if not hasattr(root, "_searchengine"): + root._searchengine = SearchEngine(root) + # XXX This will never garbage-collect -- who cares + return root._searchengine + +class SearchEngine: + + def __init__(self, root): + self.root = root + # State shared by search, replace, and grep; + # the search dialogs bind these to UI elements. + self.patvar = StringVar(root) # search pattern + self.revar = BooleanVar(root) # regular expression? + self.casevar = BooleanVar(root) # match case? + self.wordvar = BooleanVar(root) # match whole word? + self.wrapvar = BooleanVar(root) # wrap around buffer? + self.wrapvar.set(1) # (on by default) + self.backvar = BooleanVar(root) # 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 getcookedpat(self): + pat = self.getpat() + if not self.isre(): + pat = re.escape(pat) + if self.isword(): + pat = r"\b%s\b" % pat + return pat + + def getprog(self): + 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, what: + try: + msg, col = what + except: + msg = str(what) + col = -1 + self.report_error(pat, msg, col) + return None + return prog + + def report_error(self, pat, msg, col=-1): + # Derived class could overrid this with something fancier + msg = "Error: " + str(msg) + if pat: + msg = msg + "\np\Pattern: " + str(pat) + if col >= 0: + msg = msg + "\nOffset: " + str(col) + tkMessageBox.showerror("Regular expression error", + msg, master=self.root) + + def setcookedpat(self, pat): + if self.isre(): + pat = re.escape(pat) + self.setpat(pat) + + def search_text(self, text, prog=None, ok=0): + """Search a text widget for the pattern. + + If prog is given, it should be the precompiled pattern. + Return a tuple (lineno, matchobj); None if not found. + + This obeys the wrap and direction (back) settings. + + The search starts at the selection (if there is one) or + at the insert mark (otherwise). If the search is forward, + it starts at the right of the selection; for a backward + search, it starts at the left end. An empty match exactly + at either end of the selection (or at the insert mark if + there is no selection) is ignored unless the ok flag is true + -- this is done to guarantee progress. + + If the search is allowed to wrap around, it will return the + original selection if (and only if) it is the only match. + + """ + 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, string.split(pos, ".")) + chars = text.get("%d.0" % line, "%d.0" % (line+1)) + col = len(chars) - 1 + return None + +# Helper to search backwards in a string. +# (Optimized for the case where the pattern isn't found.) + +def search_reverse(prog, chars, col): + m = prog.search(chars) + if not m: + return None + found = None + i, j = m.span() + 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 + +# Helper to get selection end points, defaulting to insert mark. +# Return a tuple of indices ("line.col" strings). + +def get_selection(text): + 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 + +# Helper to parse a text index into a (line, col) tuple. + +def get_line_col(index): + line, col = map(int, string.split(index, ".")) # Fails on invalid index + return line, col diff --git a/Lib/idlelib/Separator.py b/Lib/idlelib/Separator.py new file mode 100644 index 0000000..2645e57 --- /dev/null +++ b/Lib/idlelib/Separator.py @@ -0,0 +1,92 @@ +from Tkinter import * + +class Separator: + + def __init__(self, master, orient, min=10, thickness=5, bg=None): + self.min = max(1, min) + self.thickness = max(1, thickness) + if orient in ("h", "horizontal"): + self.side = "left" + self.dim = "width" + self.dir = "x" + self.cursor = "sb_h_double_arrow" + elif orient in ("v", "vertical"): + self.side = "top" + self.dim = "height" + self.dir = "y" + self.cursor = "sb_v_double_arrow" + else: + raise ValueError, "Separator: orient should be h or v" + self.winfo_dim = "winfo_" + self.dim + self.master = master = Frame(master) + master.pack(expand=1, fill="both") + self.f1 = Frame(master) + self.f1.pack(expand=1, fill="both", side=self.side) + self.div = Frame(master, cursor=self.cursor) + self.div[self.dim] = self.thickness + self.div.pack(fill="both", side=self.side) + self.f2 = Frame(master) + self.f2.pack(expand=1, fill="both", side=self.side) + self.div.bind("<ButtonPress-1>", self.divider_press) + if bg: + ##self.f1["bg"] = bg + ##self.f2["bg"] = bg + self.div["bg"] = bg + + def parts(self): + return self.f1, self.f2 + + def divider_press(self, event): + self.press_event = event + self.f1.pack_propagate(0) + self.f2.pack_propagate(0) + for f in self.f1, self.f2: + for dim in "width", "height": + f[dim] = getattr(f, "winfo_"+dim)() + self.div.bind("<Motion>", self.div_motion) + self.div.bind("<ButtonRelease-1>", self.div_release) + self.div.grab_set() + + def div_motion(self, event): + delta = getattr(event, self.dir) - getattr(self.press_event, self.dir) + if delta: + dim1 = getattr(self.f1, self.winfo_dim)() + dim2 = getattr(self.f2, self.winfo_dim)() + delta = max(delta, self.min-dim1) + delta = min(delta, dim2-self.min) + dim1 = dim1 + delta + dim2 = dim2 - delta + self.f1[self.dim] = dim1 + self.f2[self.dim] = dim2 + + def div_release(self, event): + self.div_motion(event) + self.div.unbind("<Motion>") + self.div.grab_release() + +class VSeparator(Separator): + + def __init__(self, master, min=10, thickness=5, bg=None): + Separator.__init__(self, master, "v", min, thickness, bg) + +class HSeparator(Separator): + + def __init__(self, master, min=10, thickness=5, bg=None): + Separator.__init__(self, master, "h", min, thickness, bg) + +def main(): + root = Tk() + tlist = [] + outer = HSeparator(root, bg="red") + for part in outer.parts(): + inner = VSeparator(part, bg="blue") + for f in inner.parts(): + t = Text(f, width=40, height=10, borderwidth=0) + t.pack(fill="both", expand=1) + tlist.append(t) + tlist[0].insert("1.0", "Make your own Mondrian!") + tlist[1].insert("1.0", "Move the colored dividers...") + root.mainloop() + +if __name__ == '__main__': + main() diff --git a/Lib/idlelib/StackViewer.py b/Lib/idlelib/StackViewer.py new file mode 100644 index 0000000..5b3c87a --- /dev/null +++ b/Lib/idlelib/StackViewer.py @@ -0,0 +1,135 @@ +import string +from Tkinter import * +import linecache + +from TreeWidget import TreeNode, TreeItem, ScrolledCanvas +from ObjectBrowser import ObjectTreeItem, make_objecttreeitem +from OldStackViewer import StackViewer, NamespaceViewer + +def StackBrowser(root, flist=None, stack=None): + top = Toplevel(root) + sc = ScrolledCanvas(top, bg="white", highlightthickness=0) + sc.frame.pack(expand=1, fill="both") + item = StackTreeItem(flist) + node = TreeNode(sc.canvas, None, item) + node.expand() + +class StackTreeItem(TreeItem): + + def __init__(self, flist=None): + self.flist = flist + self.stack = get_stack() + self.text = get_exception() + + 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 = 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 + 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 + edit = self.flist.open(filename) + edit.gotoline(lineno) + +class VariablesTreeItem(ObjectTreeItem): + + def GetText(self): + return self.labeltext + + def GetLabelText(self): + return None + + def IsExpandable(self): + return len(self.object) > 0 + + def keys(self): + return self.object.keys() + + 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(key + " =", value, setfunction) + sublist.append(item) + return sublist + +def get_stack(t=None, f=None): + if t is None: + t = sys.last_traceback + stack = [] + if t and t.tb_frame is f: + t = t.tb_next + while f is not None: + stack.append((f, f.f_lineno)) + if f is self.botframe: + break + f = f.f_back + stack.reverse() + while t is not None: + stack.append((t.tb_frame, t.tb_lineno)) + t = t.tb_next + return stack + +def get_exception(type=None, value=None): + if type is None: + 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 + +if __name__ == "__main__": + root = Tk() + root.withdraw() + StackBrowser(root) diff --git a/Lib/idlelib/TODO.txt b/Lib/idlelib/TODO.txt new file mode 100644 index 0000000..9701a87 --- /dev/null +++ b/Lib/idlelib/TODO.txt @@ -0,0 +1,205 @@ + +TO DO: + +- improve debugger: + - manage breakpoints globally, allow bp deletion, tbreak, cbreak etc. + - real object browser + - help on how to use it (a simple help button will do wonders) + - performance? (updates of large sets of locals are slow) + - better integration of "debug module" + - debugger should be global resource (attached to flist, not to shell) + - fix the stupid bug where you need to step twice + - display class name in stack viewer entries for methods + - suppress tracing through IDLE internals (e.g. print) + - add a button to suppress through a specific module or class or method +- insert the initial current directory into sys.path +- default directory attribute for each window instead of only for windows + that have an associated filename +- command expansion from keywords, module contents, other buffers, etc. +- "Recent documents" menu item +- Filter region command +- Optional horizontal scroll bar +- more Emacsisms: + - ^K should cut to buffer + - M-[, M-] to move by paragraphs + - incremental search? +- search should indicate wrap-around in some way +- restructure state sensitive code to avoid testing flags all the time +- persistent user state (e.g. window and cursor positions, bindings) +- make backups when saving +- check file mtimes at various points +- Pluggable interface with RCS/CVS/Perforce/Clearcase +- better help? +- don't open second class browser on same module (nor second path browser) +- unify class and path browsers +- Need to define a standard way whereby one can determine one is running + inside IDLE (needed for Tk mainloop, also handy for $PYTHONSTARTUP) +- Add more utility methods for use by extensions (a la get_selection) +- Way to run command in totally separate interpreter (fork+os.system?) +- Way to find definition of fully-qualified name: + In other words, select "UserDict.UserDict", hit some magic key and + it loads up UserDict.py and finds the first def or class for UserDict. +- need a way to force colorization on/off +- need a way to force auto-indent on/off + +Details: + +- when there's a selection, left/right arrow should go to either + end of the selection +- ^O (on Unix -- open-line) should honor autoindent +- after paste, show end of pasted text +- on Windows, should turn short filename to long filename (not only in argv!) + (shouldn't this be done -- or undone -- by ntpath.normpath?) +- new autoindent after colon even indents when the colon is in a comment! +- sometimes forward slashes in pathname remain +- sometimes star in window name remains in Windows menu +- With unix bindings, ESC by itself is ignored +- Sometimes for no apparent reason a selection from the cursor to the + end of the command buffer appears, which is hard to get rid of + because it stays when you are typing! +- The Line/Col in the status bar can be wrong initially in PyShell + +Structural problems: + +- too much knowledge in FileList about EditorWindow (for example) +- should add some primitives for accessing the selection etc. + to repeat cumbersome code over and over + +====================================================================== + +Jeff Bauer suggests: + +- Open Module doesn't appear to handle hierarchical packages. +- Class browser should also allow hierarchical packages. +- Open and Open Module could benefit from a history, + either command line style, or Microsoft recent-file + style. +- Add a Smalltalk-style inspector (i.e. Tkinspect) + +The last suggestion is already a reality, but not yet +integrated into IDLE. I use a module called inspector.py, +that used to be available from python.org(?) It no longer +appears to be in the contributed section, and the source +has no author attribution. + +In any case, the code is useful for visually navigating +an object's attributes, including its container hierarchy. + + >>> from inspector import Tkinspect + >>> Tkinspect(None, myObject) + +Tkinspect could probably be extended and refined to +integrate better into IDLE. + +====================================================================== + +Comparison to PTUI +------------------ + ++ PTUI's help is better (HTML!) + ++ PTUI can attach a shell to any module + ++ PTUI has some more I/O commands: + open multiple + append + examine (what's that?) + +====================================================================== + +Notes after trying to run Grail +------------------------------- + +- Grail does stuff to sys.path based on sys.argv[0]; you must set +sys.argv[0] to something decent first (it is normally set to the path of +the idle script). + +- Grail must be exec'ed in __main__ because that's imported by some +other parts of Grail. + +- Grail uses a module called History and so does idle :-( + +====================================================================== + +Robin Friedrich's items: + +Things I'd like to see: + - I'd like support for shift-click extending the selection. There's a + bug now that it doesn't work the first time you try it. + - Printing is needed. How hard can that be on Windows? + - The python-mode trick of autoindenting a line with <tab> is neat and + very handy. + - (someday) a spellchecker for docstrings and comments. + - a pagedown/up command key which moves to next class/def statement (top + level) + - split window capability + - DnD text relocation/copying + +Things I don't want to see. + - line numbers... will probably slow things down way too much. + - Please use another icon for the tree browser leaf. The small snake + isn't cutting it. + +---------------------------------------------------------------------- + +- Customizable views (multi-window or multi-pane). (Markus Gritsch) + +- Being able to double click (maybe double right click) on a callable +object in the editor which shows the source of the object, if +possible. (Gerrit Holl) + +- Hooks into the guts, like in Emacs. (Mike Romberg) + +- Sharing the editor with a remote tutor. (Martijn Faassen) + +- Multiple views on the same file. (Tony J Ibbs) + +- Store breakpoints in a global (per-project) database (GvR); Dirk +Heise adds: save some space-trimmed context and search around when +reopening a file that might have been edited by someone else. + +- Capture menu events in extensions without changing the IDLE source. +(Matthias Barmeier) + +- Use overlapping panels (a "notebook" in MFC terms I think) for info +that doesn't need to be accessible simultaneously (e.g. HTML source +and output). Use multi-pane windows for info that does need to be +shown together (e.g. class browser and source). (Albert Brandl) + +- A project should invisibly track all symbols, for instant search, +replace and cross-ref. Projects should be allowed to span multiple +directories, hosts, etc. Project management files are placed in a +directory you specify. A global mapping between project names and +project directories should exist [not so sure --GvR]. (Tim Peters) + +- Merge attr-tips and auto-expand. (Mark Hammond, Tim Peters) + +- Python Shell should behave more like a "shell window" as users know +it -- i.e. you can only edit the current command, and the cursor can't +escape from the command area. (Albert Brandl) + +- Set X11 class to "idle/Idle", set icon and title to something +beginning with "idle" -- for window manangers. (Randall Hopper) + +- Config files editable through a preferences dialog. (me) + +- Config files still editable outside the preferences dialog. +(Randall Hopper) + +- When you're editing a command in PyShell, and there are only blank +lines below the cursor, hitting Return should ignore or delete those +blank lines rather than deciding you're not on the last line. (me) + +- Run command (F5 c.s.) should be more like Pythonwin's Run -- a +dialog with options to give command line arguments, run the debugger, +etc. (me) + +- Shouldn't be able to delete part of the prompt (or any text before +it) in the PyShell. (Martijn Faassen) + +- Emacs style auto-fill (also smart about comments and strings). +(Jeremy Hylton) + +- Output of Run Script should go to a separate output window, not to +the shell window. Output of separate runs should all go to the same +window but clearly delimited. (David Scherer) diff --git a/Lib/idlelib/ToolTip.py b/Lib/idlelib/ToolTip.py new file mode 100644 index 0000000..691f510 --- /dev/null +++ b/Lib/idlelib/ToolTip.py @@ -0,0 +1,87 @@ +# Ideas gleaned from PySol + +import os +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 main(): + # Test code + root = Tk() + b = Button(root, text="Hello", command=root.destroy) + b.pack() + root.update() + tip = ListboxToolTip(b, ["Hello", "world"]) + + # root.mainloop() # not in idle + +main() diff --git a/Lib/idlelib/TreeWidget.py b/Lib/idlelib/TreeWidget.py new file mode 100644 index 0000000..60eefdc --- /dev/null +++ b/Lib/idlelib/TreeWidget.py @@ -0,0 +1,471 @@ +# 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 +import sys +import string +from Tkinter import * +import imp + +import ZoomHeight + +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 (%s)" % `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 = 0 + 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 = 1 + self.canvas.delete(self.image_id) + self.drawicon() + self.drawtext() + + def deselect(self, event=None): + if not self.selected: + return + self.selected = 0 + 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! + self.x, self.y = x, y + self.drawicon() + self.drawtext() + if self.state != 'expanded': + return y+17 + # 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 = TreeNode(self.canvas, self, item) + self.children.append(child) + cx = x+20 + cy = y+17 + 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-1 + 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: + label = self.label + except AttributeError: + # padding carefully selected (on Windows) to match Entry widget: + self.label = Label(self.canvas, text=text, bd=0, padx=2, pady=2) + if self.selected: + self.label.configure(fg="white", bg="darkblue") + else: + self.label.configure(fg="black", bg="white") + 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): + 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(lambda a, b: cmp(os.path.normcase(a), os.path.normcase(b))) + 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 not opts.has_key('yscrollincrement'): + opts['yscrollincrement'] = 17 + self.master = master + self.frame = Frame(master) + self.frame.rowconfigure(0, weight=1) + self.frame.columnconfigure(0, weight=1) + self.canvas = apply(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-F2>", 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" + + +# Testing functions + +def test(): + import PyShell + root = Toplevel(PyShell.root) + 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 = FileTreeItem("C:/windows/desktop") + node = TreeNode(sc.canvas, None, item) + node.expand() + +def test2(): + # test w/o scrolling canvas + root = Tk() + root.configure(bd=0) + canvas = Canvas(root, bg="white", highlightthickness=0) + canvas.pack(expand=1, fill="both") + item = FileTreeItem(os.curdir) + node = TreeNode(canvas, None, item) + node.update() + canvas.focus_set() + +if __name__ == '__main__': + test() diff --git a/Lib/idlelib/UndoDelegator.py b/Lib/idlelib/UndoDelegator.py new file mode 100644 index 0000000..ec7af81 --- /dev/null +++ b/Lib/idlelib/UndoDelegator.py @@ -0,0 +1,352 @@ +import sys +import string +from Tkinter import * +from 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 = 0 + 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=1): + 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 = 1 + 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 = 0 + 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 = 0 + 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 + `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 0 + if self.index2 != cmd.index1: + return 0 + if self.tags != cmd.tags: + return 0 + if len(cmd.chars) != 1: + return 0 + if self.chars and \ + self.classify(self.chars[-1]) != self.classify(cmd.chars): + return 0 + self.index2 = cmd.index2 + self.chars = self.chars + cmd.chars + return 1 + + alphanumeric = string.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(" " + `cmd`) + return s + "(\n" + string.join(strs, ",\n") + "\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 main(): + from Percolator import Percolator + root = Tk() + root.wm_protocol("WM_DELETE_WINDOW", root.quit) + text = Text() + text.pack() + text.focus_set() + p = Percolator(text) + d = UndoDelegator() + p.insertfilter(d) + root.mainloop() + +if __name__ == "__main__": + main() diff --git a/Lib/idlelib/WidgetRedirector.py b/Lib/idlelib/WidgetRedirector.py new file mode 100644 index 0000000..b49ccf1 --- /dev/null +++ b/Lib/idlelib/WidgetRedirector.py @@ -0,0 +1,92 @@ +from Tkinter import * + + +class WidgetRedirector: + + """Support for redirecting arbitrary widget subcommands.""" + + def __init__(self, widget): + self.dict = {} + self.widget = widget + self.tk = tk = widget.tk + w = widget._w + self.orig = w + "_orig" + tk.call("rename", w, self.orig) + tk.createcommand(w, self.dispatch) + + def __repr__(self): + return "WidgetRedirector(%s<%s>)" % (self.widget.__class__.__name__, + self.widget._w) + + def close(self): + for name in self.dict.keys(): + self.unregister(name) + widget = self.widget; del self.widget + orig = self.orig; del self.orig + tk = widget.tk + w = widget._w + tk.deletecommand(w) + tk.call("rename", orig, w) + + def register(self, name, function): + if self.dict.has_key(name): + previous = dict[name] + else: + previous = OriginalCommand(self, name) + self.dict[name] = function + setattr(self.widget, name, function) + return previous + + def unregister(self, name): + if self.dict.has_key(name): + function = self.dict[name] + del self.dict[name] + if hasattr(self.widget, name): + delattr(self.widget, name) + return function + else: + return None + + def dispatch(self, cmd, *args): + m = self.dict.get(cmd) + try: + if m: + return apply(m, args) + else: + return self.tk.call((self.orig, cmd) + args) + except TclError: + return "" + + +class OriginalCommand: + + def __init__(self, redir, name): + self.redir = redir + self.name = name + self.tk = redir.tk + self.orig = redir.orig + self.tk_call = self.tk.call + self.orig_and_name = (self.orig, self.name) + + def __repr__(self): + return "OriginalCommand(%s, %s)" % (`self.redir`, `self.name`) + + def __call__(self, *args): + return self.tk_call(self.orig_and_name + args) + + +def main(): + root = Tk() + text = Text() + text.pack() + text.focus_set() + redir = WidgetRedirector(text) + global orig_insert + def my_insert(*args): + print "insert", args + apply(orig_insert, args) + orig_insert = redir.register("insert", my_insert) + root.mainloop() + +if __name__ == "__main__": + main() diff --git a/Lib/idlelib/WindowList.py b/Lib/idlelib/WindowList.py new file mode 100644 index 0000000..6de3e58 --- /dev/null +++ b/Lib/idlelib/WindowList.py @@ -0,0 +1,85 @@ +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: + if title == "Python Shell": + # Hack -- until we have a better way to this + continue + 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) + + def destroy(self): + registry.delete(self) + Toplevel.destroy(self) + + def get_title(self): + # Subclass can override + return self.wm_title() + + def wakeup(self): + try: + if self.wm_state() == "iconic": + self.wm_deiconify() + else: + self.tkraise() + self.focus_set() + except TclError: + # This can happen when the window menu was torn off. + # Simply ignore it. + pass diff --git a/Lib/idlelib/ZoomHeight.py b/Lib/idlelib/ZoomHeight.py new file mode 100644 index 0000000..ecc306a --- /dev/null +++ b/Lib/idlelib/ZoomHeight.py @@ -0,0 +1,46 @@ +# Sample extension: zoom a window to maximum height + +import re +import sys + +class ZoomHeight: + + menudefs = [ + ('windows', [ + ('_Zoom Height', '<<zoom-height>>'), + ]) + ] + + windows_keydefs = { + '<<zoom-height>>': ['<Alt-F2>'], + } + unix_keydefs = { + '<<zoom-height>>': ['<Control-x><Control-z>'], + } + + 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 + else: + newy = 24 + newheight = newheight - 96 + if height >= newheight: + newgeom = "" + else: + newgeom = "%dx%d+%d+%d" % (width, newheight, x, newy) + top.wm_geometry(newgeom) diff --git a/Lib/idlelib/__init__.py b/Lib/idlelib/__init__.py new file mode 100644 index 0000000..4c5b567 --- /dev/null +++ b/Lib/idlelib/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this a potential package. diff --git a/Lib/idlelib/config-unix.txt b/Lib/idlelib/config-unix.txt new file mode 100644 index 0000000..be9fa81 --- /dev/null +++ b/Lib/idlelib/config-unix.txt @@ -0,0 +1,3 @@ +[EditorWindow] +font-name= courier +font-size= 10 diff --git a/Lib/idlelib/config-win.txt b/Lib/idlelib/config-win.txt new file mode 100644 index 0000000..9faa635 --- /dev/null +++ b/Lib/idlelib/config-win.txt @@ -0,0 +1,3 @@ +[EditorWindow] +font-name: courier new +font-size: 10 diff --git a/Lib/idlelib/config.txt b/Lib/idlelib/config.txt new file mode 100644 index 0000000..586a8d8 --- /dev/null +++ b/Lib/idlelib/config.txt @@ -0,0 +1,66 @@ +# IDLE reads several config files to determine user preferences. This +# file is the default config file. When IDLE starts, it will look in +# the following four files in order: +# config.txt the default config file +# config-[win/unix/mac].txt the generic platform config file +# config-[sys.platform].txt the specific platform config file +# ~/.idle the user config file +# XXX what about Windows? +# +# The last definition of each option is used. For example, you can +# override the default window size (80x24) by defining width and +# height options in the EditorWindow section of your ~/.idle file +# +# IDLE extensions can be enabled and disabled by adding them to one of +# the config files. To enable an extension, create a section with the +# same name as the extension, e.g. the [ParenMatch] section below. To +# disable an extension, either remove the section or add the the +# enable option with the value 0. + +[EditorWindow] +width= 80 +height= 24 +# fonts defined in config-[win/unix].txt + +[Colors] +normal-foreground= black +normal-background= white +# These color types are not explicitly defined= sync, todo, stdin +keyword-foreground= #ff7700 +comment-foreground= #dd0000 +string-foreground= #00aa00 +definition-foreground= #0000ff +hilite-foreground= #000068 +hilite-background= #006868 +break-foreground= #ff7777 +hit-foreground= #ffffff +hit-background= #000000 +stdout-foreground= blue +stderr-foreground= red +console-foreground= #770000 +error-background= #ff7777 +cursor-background= black + +[SearchBinding] + +[AutoIndent] + +[AutoExpand] + +[FormatParagraph] + +[ZoomHeight] + +#[ScriptBinding] # disabled in favor of ExecBinding + +[ExecBinding] + +[CallTips] + +[ParenMatch] +enable= 0 +style= expression +flash-delay= 500 +bell= 1 +hilite-foreground= black +hilite-background= #43cd80 diff --git a/Lib/idlelib/eventparse.py b/Lib/idlelib/eventparse.py new file mode 100644 index 0000000..cb2028d --- /dev/null +++ b/Lib/idlelib/eventparse.py @@ -0,0 +1,93 @@ +#! /usr/bin/env python + +"""Parse event definitions out of comments in source files.""" + +import re +import sys +import os +import string +import getopt +import glob +import fileinput +import pprint + +def main(): + hits = [] + sublist = [] + args = sys.argv[1:] + if not args: + args = filter(lambda s: 'A' <= s[0] <= 'Z', glob.glob("*.py")) + if not args: + print "No arguments, no [A-Z]*.py files." + return 1 + for line in fileinput.input(args): + if line[:2] == '#$': + if not sublist: + sublist.append('file %s' % fileinput.filename()) + sublist.append('line %d' % fileinput.lineno()) + sublist.append(string.strip(line[2:-1])) + else: + if sublist: + hits.append(sublist) + sublist = [] + if sublist: + hits.append(sublist) + sublist = [] + dd = {} + for sublist in hits: + d = {} + for line in sublist: + words = string.split(line, None, 1) + if len(words) != 2: + continue + tag = words[0] + l = d.get(tag, []) + l.append(words[1]) + d[tag] = l + if d.has_key('event'): + keys = d['event'] + if len(keys) != 1: + print "Multiple event keys in", d + print 'File "%s", line %d' % (d['file'], d['line']) + key = keys[0] + if dd.has_key(key): + print "Duplicate event in", d + print 'File "%s", line %d' % (d['file'], d['line']) + return + dd[key] = d + else: + print "No event key in", d + print 'File "%s", line %d' % (d['file'], d['line']) + winevents = getevents(dd, "win") + unixevents = getevents(dd, "unix") + save = sys.stdout + f = open("keydefs.py", "w") + try: + sys.stdout = f + print "windows_keydefs = \\" + pprint.pprint(winevents) + print + print "unix_keydefs = \\" + pprint.pprint(unixevents) + finally: + sys.stdout = save + f.close() + +def getevents(dd, key): + res = {} + events = dd.keys() + events.sort() + for e in events: + d = dd[e] + if d.has_key(key) or d.has_key("all"): + list = [] + for x in d.get(key, []) + d.get("all", []): + list.append(x) + if key == "unix" and x[:5] == "<Alt-": + x = "<Meta-" + x[5:] + list.append(x) + res[e] = list + return res + +if __name__ == '__main__': + sys.exit(main()) diff --git a/Lib/idlelib/extend.txt b/Lib/idlelib/extend.txt new file mode 100644 index 0000000..bcc2da9 --- /dev/null +++ b/Lib/idlelib/extend.txt @@ -0,0 +1,106 @@ +Writing an IDLE extension + +An IDLE extension can define new key bindings and menu entries for IDLE +edit windows. There is a simple mechanism to load extensions when IDLE +starts up and to attach them to each edit window. (It is also possible +to make other changes to IDLE, but this must be done by editing the IDLE +source code.) + +The list of extensions loaded at startup time is configured by editing +the file extend.py; see below for details. + +An IDLE extension is defined by a class. Methods of the class define +actions that are invoked by those bindings or menu entries. Class (or +instance) variables define the bindings and menu additions; these are +automatically applied by IDLE when the extension is linked to an edit +window. + +An IDLE extension class is instantiated with a single argument, +`editwin', an EditorWindow instance. The extension cannot assume much +about this argument, but it is guarateed to have the following instance +variables: + + text a Text instance (a widget) + io an IOBinding instance (more about this later) + flist the FileList instance (shared by all edit windows) + +(There are a few more, but they are rarely useful.) + +The extension class must not bind key events. Rather, it must define +one or more virtual events, e.g. <<zoom-height>>, and corresponding +methods, e.g. zoom_height_event(), and have one or more class (or instance) +variables that define mappings between virtual events and key sequences, +e.g. <Alt-F2>. When the extension is loaded, these key sequences will +be bound to the corresponding virtual events, and the virtual events +will be bound to the corresponding methods. (This indirection is done +so that the key bindings can easily be changed, and so that other +sources of virtual events can exist, such as menu entries.) + +The following class or instance variables are used to define key +bindings for virtual events: + + keydefs for all platforms + mac_keydefs for Macintosh + windows_keydefs for Windows + unix_keydefs for Unix (and other platforms) + +Each of these variables, if it exists, must be a dictionary whose +keys are virtual events, and whose values are lists of key sequences. + +An extension can define menu entries in a similar fashion. This is done +with a class or instance variable named menudefs; it should be a list of +pair, where each pair is a menu name (lowercase) and a list of menu +entries. Each menu entry is either None (to insert a separator entry) or +a pair of strings (menu_label, virtual_event). Here, menu_label is the +label of the menu entry, and virtual_event is the virtual event to be +generated when the entry is selected. An underscore in the menu label +is removed; the character following the underscore is displayed +underlined, to indicate the shortcut character (for Windows). + +At the moment, extensions cannot define whole new menus; they must +define entries in existing menus. Some menus are not present on some +windows; such entry definitions are then ignored, but the key bindings +are still applied. (This should probably be refined in the future.) + +Here is a complete example example: + +class ZoomHeight: + + menudefs = [ + ('edit', [ + None, # Separator + ('_Zoom Height', '<<zoom-height>>'), + ]) + ] + + windows_keydefs = { + '<<zoom-height>>': ['<Alt-F2>'], + } + unix_keydefs = { + '<<zoom-height>>': ['<Control-z><Control-z>'], + } + + def __init__(self, editwin): + self.editwin = editwin + + def zoom_height_event(self, event): + "...Do what you want here..." + +The final piece of the puzzle is the file "extend.py", which contains a +simple table used to configure the loading of extensions. This file +currently contains a single list variable named "standard", which is a +list of extension names that are to be loaded. (In the future, other +configuration variables may be added to this module.) + +Extensions can define key bindings and menu entries that reference +events they don't implement (including standard events); however this is +not recommended (and may be forbidden in the future). + +Extensions are not required to define menu entries for all events they +implement. + +Note: in order to change key bindings, you must currently edit the file +keydefs. It contains two dictionaries named and formatted like the +keydefs dictionaries described above, one for the Unix bindings and one +for the Windows bindings. In the future, a better mechanism will be +provided. diff --git a/Lib/idlelib/help.txt b/Lib/idlelib/help.txt new file mode 100644 index 0000000..001da01 --- /dev/null +++ b/Lib/idlelib/help.txt @@ -0,0 +1,155 @@ +[See end for tips.] + +Click on the dotted line at the top of a menu to "tear it off": a +separate window containing the menu is created. + +File menu: + + New window -- create a new editing window + Open... -- open an existing file + Open module... -- open an existing module (searches sys.path) + Class browser -- show classes and methods in current file + Path browser -- show sys.path directories, modules, classes + and methods + --- + Save -- save current window to the associated file (unsaved + windows have a * before and after the window title) + + Save As... -- save current window to new file, which becomes + the associated file + Save Copy As... -- save current window to different file + without changing the associated file + --- + Close -- close current window (asks to save if unsaved) + Exit -- close all windows and quit IDLE (asks to save if unsaved) + +Edit menu: + + Undo -- Undo last change to current window (max 1000 changes) + Redo -- Redo last undone change to current window + --- + Cut -- Copy selection into system-wide clipboard; then delete selection + Copy -- Copy selection into system-wide clipboard + Paste -- Insert system-wide clipboard into window + Select All -- Select the entire contents of the edit buffer + --- + Find... -- Open a search dialog box with many options + Find again -- Repeat last search + Find selection -- Search for the string in the selection + Find in Files... -- Open a search dialog box for searching files + Replace... -- Open a search-and-replace dialog box + Go to line -- Ask for a line number and show that line + --- + Indent region -- Shift selected lines right 4 spaces + Dedent region -- Shift selected lines left 4 spaces + Comment out region -- Insert ## in front of selected lines + Uncomment region -- Remove leading # or ## from selected lines + Tabify region -- Turns *leading* stretches of spaces into tabs + Untabify region -- Turn *all* tabs into the right number of spaces + Expand word -- Expand the word you have typed to match another + word in the same buffer; repeat to get a different expansion + Format Paragraph -- Reformat the current blank-line-separated paragraph + --- + Import module -- Import or reload the current module + Run script -- Execute the current file in the __main__ namespace + +Windows menu: + + Zoom Height -- toggles the window between normal size (24x80) + and maximum height. + --- + The rest of this menu lists the names of all open windows; + select one to bring it to the foreground (deiconifying it if + necessary). + +Debug menu (in the Python Shell window only): + + Go to file/line -- look around the insert point for a filename + and linenumber, open the file, and show the line + Open stack viewer -- show the stack traceback of the last exception + Debugger toggle -- Run commands in the shell under the debugger + JIT Stack viewer toggle -- Open stack viewer on traceback + +Basic editing and navigation: + + Backspace deletes to the left; DEL deletes to the right + Arrow keys and Page Up/Down to move around + Home/End go to begin/end of line + Control-Home/End go to begin/end of file + Some Emacs bindings may also work, e.g. ^B/^P/^A/^E/^D/^L + +Automatic indentation: + + After a block-opening statement, the next line is indented by + 4 spaces (in the Python Shell window by one tab). After + certain keywords (break, return etc.) the next line is + dedented. In leading indentation, Backspace deletes up to 4 + spaces if they are there. Tab inserts 1-4 spaces (in the + Python Shell window one tab). See also the indent/dedent + region commands in the edit menu. + +Python Shell window: + + ^C interrupts executing command + ^D sends end-of-file; closes window if typed at >>> prompt + + Command history: + + Alt-p retrieves previous command matching what you have typed + Alt-n retrieves next + Return while on any previous command retrieves that command + Alt-/ (Expand word) is also useful here + +Syntax colors: + + The coloring is applied in a background "thread", so you may + occasionally see uncolorized text. To change the color + scheme, edit the ColorPrefs class in IdlePrefs.py. + + Python syntax colors: + + Keywords orange + Strings green + Comments red + Definitions blue + + Shell colors: + + Console output brown + stdout blue + stderr dark green + stdin black + +Other preferences: + + To change the font on Windows, open EditorWindow.py and change + text['font'] = ("lucida console", 8) + to, e.g., + text['font'] = ("courier new", 10) + + To change keyboard bindings, edit Bindings.py + +Command line usage: + + idle.py [-c command] [-d] [-e] [-s] [-t title] [arg] ... + + -c command run this command + -d enable debugger + -e edit mode; arguments are files to be edited + -s run $IDLESTARTUP or $PYTHONSTARTUP first + -t title set title of shell window + + If there are arguments: + + If -e is used, arguments are files opened for editing and + sys.argv reflects the arguments passed to IDLE itself. + + Otherwise, if -c is used, all arguments are placed in + sys.argv[1:...], with sys.argv[0] set to '-c'. + + Otherwise, if neither -e nor -c is used, the first + argument is a script which is executed with the remaining + arguments in sys.argv[1:...] and sys.argv[0] set to the + script name. If the script name is '-', no script is + executed but an interactive Python session is started; the + arguments are still available in sys.argv. diff --git a/Lib/idlelib/idle.bat b/Lib/idlelib/idle.bat new file mode 100755 index 0000000..c1b5fd2 --- /dev/null +++ b/Lib/idlelib/idle.bat @@ -0,0 +1,3 @@ +@echo off +rem Working IDLE bat for Windows - uses start instead of absolute pathname +start idle.pyw %1 %2 %3 %4 %5 %6 %7 %8 %9 diff --git a/Lib/idlelib/idle.py b/Lib/idlelib/idle.py new file mode 100644 index 0000000..71fdce5 --- /dev/null +++ b/Lib/idlelib/idle.py @@ -0,0 +1,12 @@ +#! /usr/bin/env python + +import os +import sys +import IdleConf + +idle_dir = os.path.split(sys.argv[0])[0] +IdleConf.load(idle_dir) + +# defer importing Pyshell until IdleConf is loaded +import PyShell +PyShell.main() diff --git a/Lib/idlelib/idle.pyw b/Lib/idlelib/idle.pyw new file mode 100644 index 0000000..71fdce5 --- /dev/null +++ b/Lib/idlelib/idle.pyw @@ -0,0 +1,12 @@ +#! /usr/bin/env python + +import os +import sys +import IdleConf + +idle_dir = os.path.split(sys.argv[0])[0] +IdleConf.load(idle_dir) + +# defer importing Pyshell until IdleConf is loaded +import PyShell +PyShell.main() diff --git a/Lib/idlelib/idlever.py b/Lib/idlelib/idlever.py new file mode 100644 index 0000000..e51797e --- /dev/null +++ b/Lib/idlelib/idlever.py @@ -0,0 +1 @@ +IDLE_VERSION = "0.5" diff --git a/Lib/idlelib/keydefs.py b/Lib/idlelib/keydefs.py new file mode 100644 index 0000000..3d34893 --- /dev/null +++ b/Lib/idlelib/keydefs.py @@ -0,0 +1,55 @@ +windows_keydefs = \ +{'<<Copy>>': ['<Control-c>'], + '<<Cut>>': ['<Control-x>'], + '<<Paste>>': ['<Control-v>'], + '<<beginning-of-line>>': ['<Control-a>', '<Home>'], + '<<center-insert>>': ['<Control-l>'], + '<<close-all-windows>>': ['<Control-q>'], + '<<close-window>>': ['<Alt-F4>'], + '<<dump-undo-state>>': ['<Control-backslash>'], + '<<end-of-file>>': ['<Control-d>'], + '<<python-docs>>': ['<F1>'], + '<<history-next>>': ['<Alt-n>'], + '<<history-previous>>': ['<Alt-p>'], + '<<interrupt-execution>>': ['<Control-c>'], + '<<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>'], + '<<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>']} + +unix_keydefs = \ +{'<<Copy>>': ['<Alt-w>', '<Meta-w>'], + '<<Cut>>': ['<Control-w>'], + '<<Paste>>': ['<Control-y>'], + '<<beginning-of-line>>': ['<Control-a>', '<Home>'], + '<<center-insert>>': ['<Control-l>'], + '<<close-all-windows>>': ['<Control-x><Control-c>'], + '<<close-window>>': ['<Control-x><Control-0>', '<Control-x><Key-0>'], + '<<do-nothing>>': ['<Control-x>'], + '<<dump-undo-state>>': ['<Control-backslash>'], + '<<end-of-file>>': ['<Control-d>'], + '<<help>>': ['<F1>'], + '<<history-next>>': ['<Alt-n>', '<Meta-n>'], + '<<history-previous>>': ['<Alt-p>', '<Meta-p>'], + '<<interrupt-execution>>': ['<Control-c>'], + '<<open-class-browser>>': ['<Control-x><Control-b>'], + '<<open-module>>': ['<Control-x><Control-m>'], + '<<open-new-window>>': ['<Control-x><Control-n>'], + '<<open-window-from-file>>': ['<Control-x><Control-f>'], + '<<plain-newline-and-indent>>': ['<Control-j>'], + '<<redo>>': ['<Alt-z>', '<Meta-z>'], + '<<save-copy-of-window-as-file>>': ['<Control-x><w>'], + '<<save-window-as-file>>': ['<Control-x><Control-w>'], + '<<save-window>>': ['<Control-x><Control-s>'], + '<<select-all>>': ['<Alt-a>', '<Meta-a>'], + '<<toggle-auto-coloring>>': ['<Control-slash>'], + '<<undo>>': ['<Control-z>']} diff --git a/Lib/idlelib/loader.py b/Lib/idlelib/loader.py new file mode 100644 index 0000000..6a438c3 --- /dev/null +++ b/Lib/idlelib/loader.py @@ -0,0 +1,64 @@ +# Everything is done inside the loader function so that no other names +# are placed in the global namespace. Before user code is executed, +# even this name is unbound. +def loader(): + import sys, os, protocol, threading, time + import Remote + +## Use to debug the loading process itself: +## sys.stdout = open('c:\\windows\\desktop\\stdout.txt','a') +## sys.stderr = open('c:\\windows\\desktop\\stderr.txt','a') + + # Ensure that there is absolutely no pollution of the global + # namespace by deleting the global name of this function. + global loader + del loader + + # Connect to IDLE + try: + client = protocol.Client() + except protocol.connectionLost, cL: + print 'loader: Unable to connect to IDLE', cL + return + + # Connect to an ExecBinding object that needs our help. If + # the user is starting multiple programs right now, we might get a + # different one than the one that started us. Proving that's okay is + # left as an exercise to the reader. (HINT: Twelve, by the pigeonhole + # principle) + ExecBinding = client.getobject('ExecBinding') + if not ExecBinding: + print "loader: IDLE does not need me." + return + + # All of our input and output goes through ExecBinding. + sys.stdin = Remote.pseudoIn( ExecBinding.readline ) + sys.stdout = Remote.pseudoOut( ExecBinding.write.void, tag="stdout" ) + sys.stderr = Remote.pseudoOut( ExecBinding.write.void, tag="stderr" ) + + # Create a Remote object and start it running. + remote = Remote.Remote(globals(), ExecBinding) + rthread = threading.Thread(target=remote.mainloop) + rthread.setDaemon(1) + rthread.start() + + # Block until either the client or the user program stops + user = rthread.isAlive + while user and client.isAlive(): + time.sleep(0.025) + + if not user(): + user = hasattr(sys, "ready_to_exit") and sys.ready_to_exit + for t in threading.enumerate(): + if not t.isDaemon() and t.isAlive() and t!=threading.currentThread(): + user = t.isAlive + break + + # We need to make sure we actually exit, so that the user doesn't get + # stuck with an invisible process. We want to finalize C modules, so + # we don't use os._exit(), but we don't call sys.exitfunc, which might + # block forever. + del sys.exitfunc + sys.exit() + +loader() diff --git a/Lib/idlelib/protocol.py b/Lib/idlelib/protocol.py new file mode 100644 index 0000000..10295bf --- /dev/null +++ b/Lib/idlelib/protocol.py @@ -0,0 +1,369 @@ +"""protocol (David Scherer <dscherer@cmu.edu>) + + This module implements a simple RPC or "distributed object" protocol. + I am probably the 100,000th person to write this in Python, but, hey, + it was fun. + + Contents: + + connectionLost is an exception that will be thrown by functions in + the protocol module or calls to remote methods that fail because + the remote program has closed the socket or because no connection + could be established in the first place. + + Server( port=None, connection_hook=None ) creates a server on a + well-known port, to which clients can connect. When a client + connects, a Connection is created for it. If connection_hook + is defined, then connection_hook( socket.getpeername() ) is called + before a Connection is created, and if it returns false then the + connection is refused. connection_hook must be prepared to be + called from any thread. + + Client( ip='127.0.0.1', port=None ) returns a Connection to a Server + object at a well-known address and port. + + Connection( socket ) creates an RPC connection on an arbitrary socket, + which must already be connected to another program. You do not + need to use this directly if you are using Client() or Server(). + + publish( name, connect_function ) provides an object with the + specified name to some or all Connections. When another program + calls Connection.getobject() with the specified name, the + specified connect_function is called with the arguments + + connect_function( conn, addr ) + + where conn is the Connection object to the requesting client and + addr is the address returned by socket.getpeername(). If that + function returns an object, that object becomes accessible to + the caller. If it returns None, the caller's request fails. + + Connection objects: + + .close() refuses additional RPC messages from the peer, and notifies + the peer that the connection has been closed. All pending remote + method calls in either program will fail with a connectionLost + exception. Further remote method calls on this connection will + also result in errors. + + .getobject(name) returns a proxy for the remote object with the + specified name, if it exists and the peer permits us access. + Otherwise, it returns None. It may throw a connectionLost + exception. The returned proxy supports basic attribute access + and method calls, and its methods have an extra attribute, + .void, which is a function that has the same effect but always + returns None. This last capability is provided as a performance + hack: object.method.void(params) can return without waiting for + the remote process to respond, but object.method(params) needs + to wait for a return value or exception. + + .rpc_loop(block=0) processes *incoming* messages for this connection. + If block=1, it continues processing until an exception or return + value is received, which is normally forever. Otherwise it + returns when all currently pending messages have been delivered. + It may throw a connectionLost exception. + + .set_close_hook(f) specifies a function to be called when the remote + object closes the connection during a call to rpc_loop(). This + is a good way for servers to be notified when clients disconnect. + + .set_shutdown_hook(f) specifies a function called *immediately* when + the receive loop detects that the connection has been lost. The + provided function must be prepared to run in any thread. + + Server objects: + + .rpc_loop() processes incoming messages on all connections, and + returns when all pending messages have been processed. It will + *not* throw connectionLost exceptions; the + Connection.set_close_hook() mechanism is much better for servers. +""" + +import sys, os, string, types +import socket +from threading import Thread +from Queue import Queue, Empty +from cPickle import Pickler, Unpickler, PicklingError + +class connectionLost: + def __init__(self, what=""): self.what = what + def __repr__(self): return self.what + def __str__(self): return self.what + +def getmethods(cls): + "Returns a list of the names of the methods of a class." + methods = [] + for b in cls.__bases__: + methods = methods + getmethods(b) + d = cls.__dict__ + for k in d.keys(): + if type(d[k])==types.FunctionType: + methods.append(k) + return methods + +class methodproxy: + "Proxy for a method of a remote object." + def __init__(self, classp, name): + self.classp=classp + self.name=name + self.client = classp.client + def __call__(self, *args, **keywords): + return self.client.call( 'm', self.classp.name, self.name, args, keywords ) + + def void(self, *args, **keywords): + self.client.call_void( 'm', self.classp.name,self.name,args,keywords) + +class classproxy: + "Proxy for a remote object." + def __init__(self, client, name, methods): + self.__dict__['client'] = client + self.__dict__['name'] = name + + for m in methods: + prox = methodproxy( self, m ) + self.__dict__[m] = prox + + def __getattr__(self, attr): + return self.client.call( 'g', self.name, attr ) + + def __setattr__(self, attr, value): + self.client.call_void( 's', self.name, attr, value ) + +local_connect = {} +def publish(name, connect_function): + local_connect[name]=connect_function + +class socketFile: + "File emulator based on a socket. Provides only blocking semantics for now." + + def __init__(self, socket): + self.socket = socket + self.buffer = '' + + def _recv(self,bytes): + try: + r=self.socket.recv(bytes) + except: + raise connectionLost() + if not r: + raise connectionLost() + return r + + def write(self, string): + try: + self.socket.send( string ) + except: + raise connectionLost() + + def read(self,bytes): + x = bytes-len(self.buffer) + while x>0: + self.buffer=self.buffer+self._recv(x) + x = bytes-len(self.buffer) + s = self.buffer[:bytes] + self.buffer=self.buffer[bytes:] + return s + + def readline(self): + while 1: + f = string.find(self.buffer,'\n') + if f>=0: + s = self.buffer[:f+1] + self.buffer=self.buffer[f+1:] + return s + self.buffer = self.buffer + self._recv(1024) + + +class Connection (Thread): + debug = 0 + def __init__(self, socket): + self.local_objects = {} + self.socket = socket + self.name = socket.getpeername() + self.socketfile = socketFile(socket) + self.queue = Queue(-1) + self.refuse_messages = 0 + self.cmds = { 'm': self.r_meth, + 'g': self.r_get, + 's': self.r_set, + 'o': self.r_geto, + 'e': self.r_exc, + #'r' handled by rpc_loop + } + + Thread.__init__(self) + self.setDaemon(1) + self.start() + + def getobject(self, name): + methods = self.call( 'o', name ) + if methods is None: return None + return classproxy(self, name, methods) + + # close_hook is called from rpc_loop(), like a normal remote method + # invocation + def set_close_hook(self,hook): self.close_hook = hook + + # shutdown_hook is called directly from the run() thread, and needs + # to be "thread safe" + def set_shutdown_hook(self,hook): self.shutdown_hook = hook + + close_hook = None + shutdown_hook = None + + def close(self): + self._shutdown() + self.refuse_messages = 1 + + def call(self, c, *args): + self.send( (c, args, 1 ) ) + return self.rpc_loop( block = 1 ) + + def call_void(self, c, *args): + try: + self.send( (c, args, 0 ) ) + except: + pass + + # the following methods handle individual RPC calls: + + def r_geto(self, obj): + c = local_connect.get(obj) + if not c: return None + o = c(self, self.name) + if not o: return None + self.local_objects[obj] = o + return getmethods(o.__class__) + + def r_meth(self, obj, name, args, keywords): + return apply( getattr(self.local_objects[obj],name), args, keywords) + + def r_get(self, obj, name): + return getattr(self.local_objects[obj],name) + + def r_set(self, obj, name, value): + setattr(self.local_objects[obj],name,value) + + def r_exc(self, e, v): + raise e, v + + def rpc_exec(self, cmd, arg, ret): + if self.refuse_messages: return + if self.debug: print cmd,arg,ret + if ret: + try: + r=apply(self.cmds.get(cmd), arg) + self.send( ('r', r, 0) ) + except: + try: + self.send( ('e', sys.exc_info()[:2], 0) ) + except PicklingError: + self.send( ('e', (TypeError, 'Unpicklable exception.'), 0 ) ) + else: + # we cannot report exceptions to the caller, so + # we report them in this process. + r=apply(self.cmds.get(cmd), arg) + + # the following methods implement the RPC and message loops: + + def rpc_loop(self, block=0): + if self.refuse_messages: raise connectionLost('(already closed)') + try: + while 1: + try: + cmd, arg, ret = self.queue.get( block ) + except Empty: + return None + if cmd=='r': return arg + self.rpc_exec(cmd,arg,ret) + except connectionLost: + if self.close_hook: + self.close_hook() + self.close_hook = None + raise + + def run(self): + try: + while 1: + data = self.recv() + self.queue.put( data ) + except: + self.queue.put( ('e', sys.exc_info()[:2], 0) ) + + # The following send raw pickled data to the peer + + def send(self, data): + try: + Pickler(self.socketfile,1).dump( data ) + except connectionLost: + self._shutdown() + if self.shutdown_hook: self.shutdown_hook() + raise + + def recv(self): + try: + return Unpickler(self.socketfile).load() + except connectionLost: + self._shutdown() + if self.shutdown_hook: self.shutdown_hook() + raise + except: + raise + + def _shutdown(self): + try: + self.socket.shutdown(1) + self.socket.close() + except: + pass + + +class Server (Thread): + default_port = 0x1D1E # "IDlE" + + def __init__(self, port=None, connection_hook=None): + self.connections = [] + self.port = port or self.default_port + self.connection_hook = connection_hook + + try: + self.wellknown = s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind('', self.port) + s.listen(3) + except: + raise connectionLost + + Thread.__init__(self) + self.setDaemon(1) + self.start() + + def run(self): + s = self.wellknown + while 1: + conn, addr = s.accept() + if self.connection_hook and not self.connection_hook(addr): + try: + conn.shutdown(1) + except: + pass + continue + self.connections.append( Connection(conn) ) + + def rpc_loop(self): + cns = self.connections[:] + for c in cns: + try: + c.rpc_loop(block = 0) + except connectionLost: + if c in self.connections: + self.connections.remove(c) + +def Client(ip='127.0.0.1', port=None): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect(ip,port or Server.default_port) + except socket.error, what: + raise connectionLost(str(what)) + except: + raise connectionLost() + return Connection(s) diff --git a/Lib/idlelib/pyclbr.py b/Lib/idlelib/pyclbr.py new file mode 100644 index 0000000..74b7ff7 --- /dev/null +++ b/Lib/idlelib/pyclbr.py @@ -0,0 +1,336 @@ +"""Parse a Python file and retrieve classes and methods. + +Parse enough of a Python file to recognize class and method +definitions and to find out the superclasses of a class. + +The interface consists of a single function: + readmodule(module, path) +module is the name of a Python module, path is an optional list of +directories where the module is to be searched. If present, path is +prepended to the system search path sys.path. +The return value is a dictionary. The keys of the dictionary are +the names of the classes defined in the module (including classes +that are defined via the from XXX import YYY construct). The values +are class instances of the class Class defined here. + +A class is described by the class Class in this module. Instances +of this class have the following instance variables: + name -- the name of the class + super -- a list of super classes (Class instances) + methods -- a dictionary of methods + file -- the file in which the class was defined + lineno -- the line in the file on which the class statement occurred +The dictionary of methods uses the method names as keys and the line +numbers on which the method was defined as values. +If the name of a super class is not recognized, the corresponding +entry in the list of super classes is not a class instance but a +string giving the name of the super class. Since import statements +are recognized and imported modules are scanned as well, this +shouldn't happen often. + +BUGS +- Continuation lines are not dealt with at all. +- While triple-quoted strings won't confuse it, lines that look like + def, class, import or "from ... import" stmts inside backslash-continued + single-quoted strings are treated like code. The expense of stopping + that isn't worth it. +- Code that doesn't pass tabnanny or python -t will confuse it, unless + you set the module TABWIDTH vrbl (default 8) to the correct tab width + for the file. + +PACKAGE RELATED BUGS +- If you have a package and a module inside that or another package + with the same name, module caching doesn't work properly since the + key is the base name of the module/package. +- The only entry that is returned when you readmodule a package is a + __path__ whose value is a list which confuses certain class browsers. +- When code does: + from package import subpackage + class MyClass(subpackage.SuperClass): + ... + It can't locate the parent. It probably needs to have the same + hairy logic that the import locator already does. (This logic + exists coded in Python in the freeze package.) +""" + +import os +import sys +import imp +import re +import string + +TABWIDTH = 8 + +_getnext = re.compile(r""" + (?P<String> + \""" [^"\\]* (?: + (?: \\. | "(?!"") ) + [^"\\]* + )* + \""" + + | ''' [^'\\]* (?: + (?: \\. | '(?!'') ) + [^'\\]* + )* + ''' + ) + +| (?P<Method> + ^ + (?P<MethodIndent> [ \t]* ) + def [ \t]+ + (?P<MethodName> [a-zA-Z_] \w* ) + [ \t]* \( + ) + +| (?P<Class> + ^ + (?P<ClassIndent> [ \t]* ) + class [ \t]+ + (?P<ClassName> [a-zA-Z_] \w* ) + [ \t]* + (?P<ClassSupers> \( [^)\n]* \) )? + [ \t]* : + ) + +| (?P<Import> + ^ import [ \t]+ + (?P<ImportList> [^#;\n]+ ) + ) + +| (?P<ImportFrom> + ^ from [ \t]+ + (?P<ImportFromPath> + [a-zA-Z_] \w* + (?: + [ \t]* \. [ \t]* [a-zA-Z_] \w* + )* + ) + [ \t]+ + import [ \t]+ + (?P<ImportFromList> [^#;\n]+ ) + ) +""", re.VERBOSE | re.DOTALL | re.MULTILINE).search + +_modules = {} # cache of modules we've seen + +# each Python class is represented by an instance of this class +class Class: + '''Class to represent a Python class.''' + def __init__(self, module, name, super, file, lineno): + self.module = module + self.name = name + if super is None: + super = [] + self.super = super + self.methods = {} + self.file = file + self.lineno = lineno + + def _addmethod(self, name, lineno): + self.methods[name] = lineno + +class Function(Class): + '''Class to represent a top-level Python function''' + def __init__(self, module, name, file, lineno): + Class.__init__(self, module, name, None, file, lineno) + def _addmethod(self, name, lineno): + assert 0, "Function._addmethod() shouldn't be called" + +def readmodule(module, path=[], inpackage=0): + '''Backwards compatible interface. + + Like readmodule_ex() but strips Function objects from the + resulting dictionary.''' + + dict = readmodule_ex(module, path, inpackage) + res = {} + for key, value in dict.items(): + if not isinstance(value, Function): + res[key] = value + return res + +def readmodule_ex(module, path=[], inpackage=0): + '''Read a module file and return a dictionary of classes. + + Search for MODULE in PATH and sys.path, read and parse the + module and return a dictionary with one entry for each class + found in the module.''' + + dict = {} + + i = string.rfind(module, '.') + if i >= 0: + # Dotted module name + package = string.strip(module[:i]) + submodule = string.strip(module[i+1:]) + parent = readmodule(package, path, inpackage) + child = readmodule(submodule, parent['__path__'], 1) + return child + + if _modules.has_key(module): + # we've seen this module before... + return _modules[module] + if module in sys.builtin_module_names: + # this is a built-in module + _modules[module] = dict + return dict + + # search the path for the module + f = None + if inpackage: + try: + f, file, (suff, mode, type) = \ + imp.find_module(module, path) + except ImportError: + f = None + if f is None: + fullpath = list(path) + sys.path + f, file, (suff, mode, type) = imp.find_module(module, fullpath) + if type == imp.PKG_DIRECTORY: + dict['__path__'] = [file] + _modules[module] = dict + path = [file] + path + f, file, (suff, mode, type) = \ + imp.find_module('__init__', [file]) + if type != imp.PY_SOURCE: + # not Python source, can't do anything with this module + f.close() + _modules[module] = dict + return dict + + _modules[module] = dict + imports = [] + classstack = [] # stack of (class, indent) pairs + src = f.read() + f.close() + + # To avoid having to stop the regexp at each newline, instead + # when we need a line number we simply string.count the number of + # newlines in the string since the last time we did this; i.e., + # lineno = lineno + \ + # string.count(src, '\n', last_lineno_pos, here) + # last_lineno_pos = here + countnl = string.count + lineno, last_lineno_pos = 1, 0 + i = 0 + while 1: + m = _getnext(src, i) + if not m: + break + start, i = m.span() + + if m.start("Method") >= 0: + # found a method definition or function + thisindent = _indent(m.group("MethodIndent")) + meth_name = m.group("MethodName") + lineno = lineno + \ + countnl(src, '\n', + last_lineno_pos, start) + last_lineno_pos = start + # close all classes indented at least as much + while classstack and \ + classstack[-1][1] >= thisindent: + del classstack[-1] + if classstack: + # it's a class method + cur_class = classstack[-1][0] + cur_class._addmethod(meth_name, lineno) + else: + # it's a function + f = Function(module, meth_name, + file, lineno) + dict[meth_name] = f + + elif m.start("String") >= 0: + pass + + elif m.start("Class") >= 0: + # we found a class definition + thisindent = _indent(m.group("ClassIndent")) + # close all classes indented at least as much + while classstack and \ + classstack[-1][1] >= thisindent: + del classstack[-1] + lineno = lineno + \ + countnl(src, '\n', last_lineno_pos, start) + last_lineno_pos = start + class_name = m.group("ClassName") + inherit = m.group("ClassSupers") + if inherit: + # the class inherits from other classes + inherit = string.strip(inherit[1:-1]) + names = [] + for n in string.splitfields(inherit, ','): + n = string.strip(n) + if dict.has_key(n): + # we know this super class + n = dict[n] + else: + c = string.splitfields(n, '.') + if len(c) > 1: + # super class + # is of the + # form module.class: + # look in + # module for class + m = c[-2] + c = c[-1] + if _modules.has_key(m): + d = _modules[m] + if d.has_key(c): + n = d[c] + names.append(n) + inherit = names + # remember this class + cur_class = Class(module, class_name, inherit, + file, lineno) + dict[class_name] = cur_class + classstack.append((cur_class, thisindent)) + + elif m.start("Import") >= 0: + # import module + for n in string.split(m.group("ImportList"), ','): + n = string.strip(n) + try: + # recursively read the imported module + d = readmodule(n, path, inpackage) + except: + ##print 'module', n, 'not found' + pass + + elif m.start("ImportFrom") >= 0: + # from module import stuff + mod = m.group("ImportFromPath") + names = string.split(m.group("ImportFromList"), ',') + try: + # recursively read the imported module + d = readmodule(mod, path, inpackage) + except: + ##print 'module', mod, 'not found' + continue + # add any classes that were defined in the + # imported module to our name space if they + # were mentioned in the list + for n in names: + n = string.strip(n) + if d.has_key(n): + dict[n] = d[n] + elif n == '*': + # only add a name if not + # already there (to mimic what + # Python does internally) + # also don't add names that + # start with _ + for n in d.keys(): + if n[0] != '_' and \ + not dict.has_key(n): + dict[n] = d[n] + else: + assert 0, "regexp _getnext found something unexpected" + + return dict + +def _indent(ws, _expandtabs=string.expandtabs): + return len(_expandtabs(ws, TABWIDTH)) diff --git a/Lib/idlelib/spawn.py b/Lib/idlelib/spawn.py new file mode 100644 index 0000000..ce6b41c --- /dev/null +++ b/Lib/idlelib/spawn.py @@ -0,0 +1,59 @@ +# spawn - This is ugly, OS-specific code to spawn a separate process. It +# also defines a function for getting the version of a path most +# likely to work with cranky API functions. + +import os + +def hardpath(path): + path = os.path.normcase(os.path.abspath(path)) + try: + import win32api + path = win32api.GetShortPathName( path ) + except: + pass + return path + +if hasattr(os, 'spawnv'): + + # Windows-ish OS: we use spawnv(), and stick quotes around arguments + # in case they contains spaces, since Windows will jam all the + # arguments to spawn() or exec() together into one string. The + # kill_zombies function is a noop. + + def spawn(bin, *args): + nargs = [bin] + for arg in args: + nargs.append( '"'+arg+'"' ) + os.spawnv( os.P_NOWAIT, bin, nargs ) + + def kill_zombies(): pass + +elif hasattr(os, 'fork'): + + # UNIX-ish operating system: we fork() and exec(), and we have to track + # the pids of our children and call waitpid() on them to avoid leaving + # zombies in the process table. kill_zombies() does the dirty work, and + # should be called periodically. + + zombies = [] + + def spawn(bin, *args): + pid = os.fork() + if pid: + zombies.append(pid) + else: + os.execv( bin, (bin, ) + args ) + + def kill_zombies(): + for z in zombies[:]: + stat = os.waitpid(z, os.WNOHANG) + if stat[0]==z: + zombies.remove(z) + +else: + # If you get here, you may be able to write an alternative implementation + # of these functions for your OS. + + def kill_zombies(): pass + + raise OSError, 'This OS does not support fork() or spawnv().' diff --git a/Lib/idlelib/tabnanny.py b/Lib/idlelib/tabnanny.py new file mode 100644 index 0000000..8d3eab5 --- /dev/null +++ b/Lib/idlelib/tabnanny.py @@ -0,0 +1,372 @@ +#! /usr/bin/env python + +"""The Tab Nanny despises ambiguous indentation. She knows no mercy.""" + +# Released to the public domain, by Tim Peters, 15 April 1998. + +# XXX Note: this is now a standard library module. +# XXX The API needs to undergo changes however; the current code is too +# XXX script-like. This will be addressed later. + +__version__ = "6" + +import os +import sys +import string +import getopt +import tokenize + +verbose = 0 +filename_only = 0 + +def errprint(*args): + sep = "" + for arg in args: + sys.stderr.write(sep + str(arg)) + sep = " " + sys.stderr.write("\n") + +def main(): + global verbose, filename_only + try: + opts, args = getopt.getopt(sys.argv[1:], "qv") + except getopt.error, msg: + errprint(msg) + return + for o, a in opts: + if o == '-q': + filename_only = filename_only + 1 + if o == '-v': + verbose = verbose + 1 + if not args: + errprint("Usage:", sys.argv[0], "[-v] file_or_directory ...") + return + for arg in args: + check(arg) + +class NannyNag: + def __init__(self, lineno, msg, line): + self.lineno, self.msg, self.line = lineno, msg, line + def get_lineno(self): + return self.lineno + def get_msg(self): + return self.msg + def get_line(self): + return self.line + +def check(file): + if os.path.isdir(file) and not os.path.islink(file): + if verbose: + print "%s: listing directory" % `file` + names = os.listdir(file) + for name in names: + fullname = os.path.join(file, name) + if (os.path.isdir(fullname) and + not os.path.islink(fullname) or + os.path.normcase(name[-3:]) == ".py"): + check(fullname) + return + + try: + f = open(file) + except IOError, msg: + errprint("%s: I/O Error: %s" % (`file`, str(msg))) + return + + if verbose > 1: + print "checking", `file`, "..." + + reset_globals() + try: + tokenize.tokenize(f.readline, tokeneater) + + except tokenize.TokenError, msg: + errprint("%s: Token Error: %s" % (`file`, str(msg))) + return + + except NannyNag, nag: + badline = nag.get_lineno() + line = nag.get_line() + if verbose: + print "%s: *** Line %d: trouble in tab city! ***" % ( + `file`, badline) + print "offending line:", `line` + print nag.get_msg() + else: + if ' ' in file: file = '"' + file + '"' + if filename_only: print file + else: print file, badline, `line` + return + + if verbose: + print "%s: Clean bill of health." % `file` + +class Whitespace: + # the characters used for space and tab + S, T = ' \t' + + # members: + # raw + # the original string + # n + # the number of leading whitespace characters in raw + # nt + # the number of tabs in raw[:n] + # norm + # the normal form as a pair (count, trailing), where: + # count + # a tuple such that raw[:n] contains count[i] + # instances of S * i + T + # trailing + # the number of trailing spaces in raw[:n] + # It's A Theorem that m.indent_level(t) == + # n.indent_level(t) for all t >= 1 iff m.norm == n.norm. + # is_simple + # true iff raw[:n] is of the form (T*)(S*) + + def __init__(self, ws): + self.raw = ws + S, T = Whitespace.S, Whitespace.T + count = [] + b = n = nt = 0 + for ch in self.raw: + if ch == S: + n = n + 1 + b = b + 1 + elif ch == T: + n = n + 1 + nt = nt + 1 + if b >= len(count): + count = count + [0] * (b - len(count) + 1) + count[b] = count[b] + 1 + b = 0 + else: + break + self.n = n + self.nt = nt + self.norm = tuple(count), b + self.is_simple = len(count) <= 1 + + # return length of longest contiguous run of spaces (whether or not + # preceding a tab) + def longest_run_of_spaces(self): + count, trailing = self.norm + return max(len(count)-1, trailing) + + def indent_level(self, tabsize): + # count, il = self.norm + # for i in range(len(count)): + # if count[i]: + # il = il + (i/tabsize + 1)*tabsize * count[i] + # return il + + # quicker: + # il = trailing + sum (i/ts + 1)*ts*count[i] = + # trailing + ts * sum (i/ts + 1)*count[i] = + # trailing + ts * sum i/ts*count[i] + count[i] = + # trailing + ts * [(sum i/ts*count[i]) + (sum count[i])] = + # trailing + ts * [(sum i/ts*count[i]) + num_tabs] + # and note that i/ts*count[i] is 0 when i < ts + + count, trailing = self.norm + il = 0 + for i in range(tabsize, len(count)): + il = il + i/tabsize * count[i] + return trailing + tabsize * (il + self.nt) + + # return true iff self.indent_level(t) == other.indent_level(t) + # for all t >= 1 + def equal(self, other): + return self.norm == other.norm + + # return a list of tuples (ts, i1, i2) such that + # i1 == self.indent_level(ts) != other.indent_level(ts) == i2. + # Intended to be used after not self.equal(other) is known, in which + # case it will return at least one witnessing tab size. + def not_equal_witness(self, other): + n = max(self.longest_run_of_spaces(), + other.longest_run_of_spaces()) + 1 + a = [] + for ts in range(1, n+1): + if self.indent_level(ts) != other.indent_level(ts): + a.append( (ts, + self.indent_level(ts), + other.indent_level(ts)) ) + return a + + # Return true iff self.indent_level(t) < other.indent_level(t) + # for all t >= 1. + # The algorithm is due to Vincent Broman. + # Easy to prove it's correct. + # XXXpost that. + # Trivial to prove n is sharp (consider T vs ST). + # Unknown whether there's a faster general way. I suspected so at + # first, but no longer. + # For the special (but common!) case where M and N are both of the + # form (T*)(S*), M.less(N) iff M.len() < N.len() and + # M.num_tabs() <= N.num_tabs(). Proof is easy but kinda long-winded. + # XXXwrite that up. + # Note that M is of the form (T*)(S*) iff len(M.norm[0]) <= 1. + def less(self, other): + if self.n >= other.n: + return 0 + if self.is_simple and other.is_simple: + return self.nt <= other.nt + n = max(self.longest_run_of_spaces(), + other.longest_run_of_spaces()) + 1 + # the self.n >= other.n test already did it for ts=1 + for ts in range(2, n+1): + if self.indent_level(ts) >= other.indent_level(ts): + return 0 + return 1 + + # return a list of tuples (ts, i1, i2) such that + # i1 == self.indent_level(ts) >= other.indent_level(ts) == i2. + # Intended to be used after not self.less(other) is known, in which + # case it will return at least one witnessing tab size. + def not_less_witness(self, other): + n = max(self.longest_run_of_spaces(), + other.longest_run_of_spaces()) + 1 + a = [] + for ts in range(1, n+1): + if self.indent_level(ts) >= other.indent_level(ts): + a.append( (ts, + self.indent_level(ts), + other.indent_level(ts)) ) + return a + +def format_witnesses(w): + import string + firsts = map(lambda tup: str(tup[0]), w) + prefix = "at tab size" + if len(w) > 1: + prefix = prefix + "s" + return prefix + " " + string.join(firsts, ', ') + +# The collection of globals, the reset_globals() function, and the +# tokeneater() function, depend on which version of tokenize is +# in use. + +if hasattr(tokenize, 'NL'): + # take advantage of Guido's patch! + + indents = [] + check_equal = 0 + + def reset_globals(): + global indents, check_equal + check_equal = 0 + indents = [Whitespace("")] + + def tokeneater(type, token, start, end, line, + INDENT=tokenize.INDENT, + DEDENT=tokenize.DEDENT, + NEWLINE=tokenize.NEWLINE, + JUNK=(tokenize.COMMENT, tokenize.NL) ): + global indents, check_equal + + if type == NEWLINE: + # a program statement, or ENDMARKER, will eventually follow, + # after some (possibly empty) run of tokens of the form + # (NL | COMMENT)* (INDENT | DEDENT+)? + # If an INDENT appears, setting check_equal is wrong, and will + # be undone when we see the INDENT. + check_equal = 1 + + elif type == INDENT: + check_equal = 0 + thisguy = Whitespace(token) + if not indents[-1].less(thisguy): + witness = indents[-1].not_less_witness(thisguy) + msg = "indent not greater e.g. " + format_witnesses(witness) + raise NannyNag(start[0], msg, line) + indents.append(thisguy) + + elif type == DEDENT: + # there's nothing we need to check here! what's important is + # that when the run of DEDENTs ends, the indentation of the + # program statement (or ENDMARKER) that triggered the run is + # equal to what's left at the top of the indents stack + + # Ouch! This assert triggers if the last line of the source + # is indented *and* lacks a newline -- then DEDENTs pop out + # of thin air. + # assert check_equal # else no earlier NEWLINE, or an earlier INDENT + check_equal = 1 + + del indents[-1] + + elif check_equal and type not in JUNK: + # this is the first "real token" following a NEWLINE, so it + # must be the first token of the next program statement, or an + # ENDMARKER; the "line" argument exposes the leading whitespace + # for this statement; in the case of ENDMARKER, line is an empty + # string, so will properly match the empty string with which the + # "indents" stack was seeded + check_equal = 0 + thisguy = Whitespace(line) + if not indents[-1].equal(thisguy): + witness = indents[-1].not_equal_witness(thisguy) + msg = "indent not equal e.g. " + format_witnesses(witness) + raise NannyNag(start[0], msg, line) + +else: + # unpatched version of tokenize + + nesting_level = 0 + indents = [] + check_equal = 0 + + def reset_globals(): + global nesting_level, indents, check_equal + nesting_level = check_equal = 0 + indents = [Whitespace("")] + + def tokeneater(type, token, start, end, line, + INDENT=tokenize.INDENT, + DEDENT=tokenize.DEDENT, + NEWLINE=tokenize.NEWLINE, + COMMENT=tokenize.COMMENT, + OP=tokenize.OP): + global nesting_level, indents, check_equal + + if type == INDENT: + check_equal = 0 + thisguy = Whitespace(token) + if not indents[-1].less(thisguy): + witness = indents[-1].not_less_witness(thisguy) + msg = "indent not greater e.g. " + format_witnesses(witness) + raise NannyNag(start[0], msg, line) + indents.append(thisguy) + + elif type == DEDENT: + del indents[-1] + + elif type == NEWLINE: + if nesting_level == 0: + check_equal = 1 + + elif type == COMMENT: + pass + + elif check_equal: + check_equal = 0 + thisguy = Whitespace(line) + if not indents[-1].equal(thisguy): + witness = indents[-1].not_equal_witness(thisguy) + msg = "indent not equal e.g. " + format_witnesses(witness) + raise NannyNag(start[0], msg, line) + + if type == OP and token in ('{', '[', '('): + nesting_level = nesting_level + 1 + + elif type == OP and token in ('}', ']', ')'): + if nesting_level == 0: + raise NannyNag(start[0], + "unbalanced bracket '" + token + "'", + line) + nesting_level = nesting_level - 1 + +if __name__ == '__main__': + main() + diff --git a/Lib/idlelib/testcode.py b/Lib/idlelib/testcode.py new file mode 100644 index 0000000..05eaa56 --- /dev/null +++ b/Lib/idlelib/testcode.py @@ -0,0 +1,31 @@ +import string + +def f(): + a = 0 + b = 1 + c = 2 + d = 3 + e = 4 + g() + +def g(): + h() + +def h(): + i() + +def i(): + j() + +def j(): + k() + +def k(): + l() + +l = lambda: test() + +def test(): + string.capwords(1) + +f() |