| #! /usr/bin/env python |
| |
| import os |
| import sys |
| import string |
| import getopt |
| import re |
| |
| import linecache |
| from code import InteractiveInterpreter |
| |
| from Tkinter import * |
| import tkMessageBox |
| |
| from EditorWindow import fixwordbreaks |
| from FileList import FileList, MultiEditorWindow, MultiIOBinding |
| from ColorDelegator import ColorDelegator |
| |
| # 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 |
| |
| |
| class PyShellEditorWindow(MultiEditorWindow): |
| |
| def __init__(self, *args): |
| apply(MultiEditorWindow.__init__, (self,) + args) |
| self.text.bind("<3>", self.right_menu_event) |
| |
| def fixedwindowsmenu(self, wmenu): |
| wmenu.add_command(label="Python Shell", command=self.flist.open_shell) |
| wmenu.add_separator() |
| |
| menu = None |
| |
| def right_menu_event(self, event): |
| self.text.mark_set("insert", "@%d,%d" % (event.x, event.y)) |
| if not self.menu: |
| self.make_menu() |
| menu = self.menu |
| iswin = sys.platform[:3] == 'win' |
| if iswin: |
| self.text.config(cursor="arrow") |
| menu.tk_popup(event.x_root, event.y_root) |
| if iswin: |
| self.text.config(cursor="ibeam") |
| |
| def make_menu(self): |
| self.menu = menu = Menu(self.text, tearoff=0) |
| menu.add_command(label="Set breakpoint here", |
| command=self.set_breakpoint_here) |
| |
| def set_breakpoint_here(self): |
| 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): |
| |
| EditorWindow = PyShellEditorWindow |
| |
| pyshell = None |
| |
| def open_shell(self): |
| if self.pyshell: |
| self.pyshell.wakeup() |
| else: |
| self.pyshell = PyShell(self) |
| self.pyshell.begin() |
| return self.pyshell |
| |
| |
| class ModifiedIOBinding(MultiIOBinding): |
| |
| def defaultfilename(self, mode="open"): |
| if self.filename: |
| return MultiIOBinding.defaultfilename(self, mode) |
| else: |
| try: |
| pwd = os.getcwd() |
| except os.error: |
| pwd = "" |
| return pwd, "" |
| |
| def open(self, event): |
| # Override base class method -- don't allow reusing this window |
| filename = self.askopenfile() |
| if filename: |
| self.flist.open(filename) |
| return "break" |
| |
| def maybesave(self): |
| # Override base class method -- don't ask any questions |
| if self.text.get_saved(): |
| return "yes" |
| else: |
| return "no" |
| |
| |
| class ModifiedColorDelegator(ColorDelegator): |
| |
| def recolorize_main(self): |
| self.tag_remove("TODO", "1.0", "iomark") |
| self.tag_add("SYNC", "1.0", "iomark") |
| ColorDelegator.recolorize_main(self) |
| |
| |
| class ModifiedInterpreter(InteractiveInterpreter): |
| |
| def __init__(self, tkconsole): |
| self.tkconsole = tkconsole |
| InteractiveInterpreter.__init__(self) |
| |
| gid = 0 |
| |
| def runsource(self, source): |
| # Extend base class to stuff the source in the line 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 |
| self.more = 0 |
| return InteractiveInterpreter.runsource(self, source, 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 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 == 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() |
| except: |
| self.showtraceback() |
| finally: |
| self.tkconsole.endexecuting() |
| |
| def write(self, s): |
| # Override base class write |
| self.tkconsole.console.write(s) |
| |
| |
| class PyShell(PyShellEditorWindow): |
| |
| # Override classes |
| ColorDelegator = ModifiedColorDelegator |
| IOBinding = ModifiedIOBinding |
| |
| # Override menu bar specs |
| menu_specs = PyShellEditorWindow.menu_specs[:] |
| menu_specs.insert(len(menu_specs)-1, ("debug", "Debug")) |
| |
| # New classes |
| from History import History |
| from PopupMenu import PopupMenu |
| |
| def __init__(self, flist=None): |
| self.interp = ModifiedInterpreter(self) |
| if flist is None: |
| root = Tk() |
| fixwordbreaks(root) |
| root.withdraw() |
| flist = PyShellFileList(root) |
| |
| PyShellEditorWindow.__init__(self, flist, None, None) |
| self.config_colors() |
| |
| import __builtin__ |
| __builtin__.quit = __builtin__.exit = "To exit, type Ctrl-D." |
| |
| self.auto.config(prefertabs=1) |
| |
| text = self.text |
| 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("<<goto-traceback-line>>", self.goto_traceback_line) |
| text.bind("<<open-stack-viewer>>", self.open_stack_viewer) |
| text.bind("<<toggle-debugger>>", self.toggle_debugger) |
| |
| sys.stdout = PseudoFile(self, "stdout") |
| sys.stderr = PseudoFile(self, "stderr") |
| sys.stdin = self |
| self.console = PseudoFile(self, "console") |
| |
| self.history = self.History(self.text) |
| self.popup = self.PopupMenu(self.text, self.flist) |
| |
| tagdefs = { |
| ##"stdin": {"background": "yellow"}, |
| "stdout": {"foreground": "blue"}, |
| "stderr": {"foreground": "#007700"}, |
| "console": {"foreground": "red"}, |
| "ERROR": {"background": "#FF7777"}, |
| None: {"foreground": "purple"}, # default |
| } |
| |
| def config_colors(self): |
| for tag, cnf in self.tagdefs.items(): |
| if cnf: |
| if not tag: |
| apply(self.text.configure, (), cnf) |
| else: |
| apply(self.text.tag_configure, (tag,), cnf) |
| self.text.tag_raise("sel") |
| |
| 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) |
| return "break" |
| db = self.interp.getdebugger() |
| if db: |
| self.close_debugger() |
| else: |
| self.open_debugger() |
| |
| 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() |
| |
| def open_debugger(self): |
| import Debugger |
| self.interp.setdebugger(Debugger.Debugger(self)) |
| sys.ps1 = "[DEBUG ON]\n>>> " |
| self.showprompt() |
| |
| 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.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( |
| "Cancel?", |
| "The program is still running; do you want to cancel it?", |
| default="ok", |
| master=self.text): |
| return "cancel" |
| self.canceled = 1 |
| if self.reading: |
| self.top.quit() |
| return "cancel" |
| reply = PyShellEditorWindow.close(self) |
| if reply != "cancel": |
| self.flist.pyshell = None |
| # Restore std streams |
| sys.stdout = sys.__stdout__ |
| sys.stderr = sys.__stderr__ |
| sys.stdin = sys.__stdin__ |
| # Break cycles |
| self.interp = None |
| self.console = None |
| return reply |
| |
| def ispythonsource(self, filename): |
| # Override this so EditorWindow never removes the colorizer |
| return 1 |
| |
| def saved_change_hook(self): |
| # Override this to get the title right |
| title = "Python Shell" |
| if self.io.filename: |
| title = title + ": " + self.io.filename |
| if not self.undo.get_saved(): |
| title = title + " *" |
| self.top.wm_title(title) |
| |
| def begin(self): |
| self.resetoutput() |
| self.write("Python %s on %s\n%s\n" % |
| (sys.version, sys.platform, sys.copyright)) |
| 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 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.autoindent(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("self.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 before its last line, |
| # insert a newline right at the insert point |
| if self.text.compare("insert", "<", "end-1c linestart"): |
| self.auto.autoindent(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.autoindent(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 |
| |
| file_line_pats = [ |
| r'File "([^"]*)", line (\d+)', |
| r'([^\s]+)\((\d+)\)', |
| r'([^\s]+):\s*(\d+):', |
| ] |
| |
| file_line_progs = None |
| |
| def goto_traceback_line(self, event=None): |
| if self.file_line_progs is None: |
| l = [] |
| for pat in self.file_line_pats: |
| l.append(re.compile(pat)) |
| 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") |
| for prog in self.file_line_progs: |
| m = prog.search(line) |
| if m: |
| break |
| else: |
| tkMessageBox.showerror("No traceback line", |
| "The line you point at doesn't look " |
| "like an error message.", |
| master=self.text) |
| return |
| filename, lineno = m.group(1, 2) |
| try: |
| f = open(filename, "r") |
| f.close() |
| except IOError, msg: |
| self.text.bell() |
| return |
| edit = self.flist.open(filename) |
| try: |
| lineno = int(lineno) |
| except ValueError, msg: |
| self.text.bell() |
| return |
| edit.gotoline(lineno) |
| |
| 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): |
| # Overrides base class write |
| self.console.write(s) |
| |
| class PseudoFile: |
| |
| def __init__(self, interp, tags): |
| self.interp = interp |
| self.text = interp.text |
| self.tags = tags |
| |
| def write(self, s): |
| self.text.mark_gravity("iomark", "right") |
| self.text.insert("iomark", str(s), self.tags) |
| self.text.mark_gravity("iomark", "left") |
| self.text.see("iomark") |
| self.text.update() |
| if self.interp.canceled: |
| self.interp.canceled = 0 |
| raise KeyboardInterrupt |
| |
| def writelines(self, l): |
| map(self.write, l) |
| |
| |
| def main(): |
| debug = 0 |
| try: |
| opts, args = getopt.getopt(sys.argv[1:], "d") |
| except getopt.error, msg: |
| sys.stderr.write("Error: %s\n" % str(msg)) |
| sys.exit(2) |
| for o, a in opts: |
| if o == "-d": |
| debug = 1 |
| global flist, root |
| root = Tk() |
| fixwordbreaks(root) |
| root.withdraw() |
| flist = PyShellFileList(root) |
| if args: |
| for filename in sys.argv[1:]: |
| flist.open(filename) |
| t = PyShell(flist) |
| flist.pyshell = t |
| t.begin() |
| if debug: |
| t.open_debugger() |
| root.mainloop() |
| |
| if __name__ == "__main__": |
| main() |