From 548d29215044cb806a1f124843c04558296b7d65 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Sun, 13 Feb 2011 22:18:36 -0500 Subject: [PATCH] Add saved settings, themes, etc. --- MANIFEST.in | 1 + example-theme.py | 3 + pudb/__init__.py | 7 +++ pudb/debugger.py | 107 ++++++++++++++++---------------- pudb/settings.py | 145 ++++++++++++++++++++++++++++++++++++++++++++ pudb/shell.py | 1 - pudb/source_view.py | 22 ++++--- pudb/theme.py | 46 +++++++++++++- setup.py | 7 ++- 9 files changed, 275 insertions(+), 64 deletions(-) create mode 100644 example-theme.py create mode 100644 pudb/settings.py diff --git a/MANIFEST.in b/MANIFEST.in index 513d064..2d1d739 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ include debug_me.py include ez_setup.py include try-the-debugger.sh +include example-theme.py diff --git a/example-theme.py b/example-theme.py new file mode 100644 index 0000000..a589c89 --- /dev/null +++ b/example-theme.py @@ -0,0 +1,3 @@ +palette.update({ + "source": (add_setting("black", "underline"), "dark green"), + }) diff --git a/pudb/__init__.py b/pudb/__init__.py index 7a11a86..83e760f 100644 --- a/pudb/__init__.py +++ b/pudb/__init__.py @@ -4,6 +4,13 @@ VERSION = ".".join(str(nv) for nv in NUM_VERSION) +from pudb.settings import load_config, save_config +CONFIG = load_config() +save_config(CONFIG) + + + + CURRENT_DEBUGGER = [] def _get_debugger(): if not CURRENT_DEBUGGER: diff --git a/pudb/debugger.py b/pudb/debugger.py index c4f6078..5512377 100644 --- a/pudb/debugger.py +++ b/pudb/debugger.py @@ -13,6 +13,8 @@ Welcome to PuDB, the Python Urwid debugger. ------------------------------------------- Keys: + Ctrl-p - edit preferences + n - step over ("next") s - step into c - continue @@ -135,7 +137,7 @@ class Debugger(bdb.Bdb): self.ui.set_current_line(lineno, self.curframe.f_code.co_filename) self.ui.update_var_view() self.ui.update_stack() - + def move_up_frame(self): if self.curindex > 0: self.set_frame_index(self.curindex-1) @@ -346,6 +348,9 @@ class DebuggerUI(FrameVarInfoKeeper): def edit_inspector_detail(w, size, key): var, pos = self.var_list._w.get_focus() + if var is None: + return + fvi = self.get_frame_var_info(read_only=False) iinfo = fvi.get_inspect_info(var.id_path, read_only=False) @@ -465,7 +470,11 @@ class DebuggerUI(FrameVarInfoKeeper): # stack listeners ----------------------------------------------------- def examine_breakpoint(w, size, key): - _, pos = self.bp_list._w.get_focus() + bp_entry, pos = self.bp_list._w.get_focus() + + if bp_entry is None: + return + bp = self._get_bp_list()[pos] if bp.cond is None: @@ -870,50 +879,6 @@ class DebuggerUI(FrameVarInfoKeeper): self.message("No exception available.") def run_shell(w, size, key): - import pudb.shell as shell - - if shell.HAVE_IPYTHON and shell.USE_IPYTHON == "ask": - def ipython(w, size, key): - self.quit_event_loop = ["ipython"] - def classic(w, size, key): - self.quit_event_loop = ["classic"] - def cancel(w, size, key): - self.quit_event_loop = [False] - - result = dbg.ui.dialog( - urwid.ListBox([urwid.Text( - "You've asked to enter a Python shell.\n\n" - "You appear to have IPython installed. If you wish to use it, " - "you may hit 'i' or select the corresponding button on the " - "right. If you prefer the 'classic' Python shell, hit '!' again or " - "select that button instead.\n\n" - "Sorry for bothering you, I won't ask again in this session." - )]), - [ - ("Classic", "classic"), - ("IPython", "ipython"), - ("Cancel", False), - ], - focus_buttons=True, - bind_enter_esc=False, - title="Shell Requested", - extra_bindings=[ - ("!", classic), - ("i", ipython), - ("esc", cancel), - ]) - - if result == False: - return - elif result == "ipython": - shell.USE_IPYTHON = use_ipython = True - elif result == "classic": - shell.USE_IPYTHON = use_ipython = False - - elif shell.HAVE_IPYTHON and shell.USE_IPYTHON: - use_ipython = True - else: - use_ipython = False self.screen.stop() @@ -925,12 +890,14 @@ class DebuggerUI(FrameVarInfoKeeper): curframe = self.debugger.curframe - if use_ipython: + from pudb import CONFIG + import pudb.shell as shell + if shell.HAVE_IPYTHON and CONFIG["shell"] == "ipython": runner = shell.run_ipython_shell else: runner = shell.run_classic_shell - runner(curframe.f_locals, curframe.f_globals, + runner(curframe.f_locals, curframe.f_globals, first_shell_run) self.screen.start() @@ -965,9 +932,20 @@ class DebuggerUI(FrameVarInfoKeeper): self.debugger.set_quit() end() + def do_edit_config(w, size, key): + from pudb.settings import edit_config, save_config + from pudb import CONFIG + edit_config(self, CONFIG) + save_config(CONFIG) + self.setup_palette(self.screen) + + for sl in self.source: + sl._invalidate() + def help(w, size, key): self.message(HELP_TEXT, title="PuDB Help") + self.top.listen("o", show_output) self.top.listen("!", run_shell) self.top.listen("e", show_traceback) @@ -979,6 +957,7 @@ class DebuggerUI(FrameVarInfoKeeper): self.top.listen("B", RHColumnFocuser(2)) self.top.listen("q", quit) + self.top.listen("ctrl p", do_edit_config) self.top.listen("H", help) self.top.listen("f1", help) self.top.listen("?", help) @@ -1068,8 +1047,10 @@ class DebuggerUI(FrameVarInfoKeeper): may_use_fancy_formats = isinstance(screen, RawScreen) and \ not hasattr(urwid.escape, "_fg_attr_xterm") + from pudb import CONFIG from pudb.theme import get_palette - screen.register_palette(get_palette(may_use_fancy_formats)) + screen.register_palette( + get_palette(may_use_fancy_formats, CONFIG["theme"])) # UI enter/exit ----------------------------------------------------------- def show(self): @@ -1101,6 +1082,30 @@ class DebuggerUI(FrameVarInfoKeeper): self.message("Package 'pygments' not found. " "Syntax highlighting disabled.") + from pudb import CONFIG + WELCOME_LEVEL = "b" + if CONFIG["seen_welcome"] < WELCOME_LEVEL: + CONFIG["seen_welcome"] = WELCOME_LEVEL + from pudb import VERSION + self.message("Welcome to PudB %s!\n\n" + "PuDB is a full-screen, console-based visual debugger for Python. " + " Its goal is to provide all the niceties of modern GUI-based " + "debuggers in a more lightweight and keyboard-friendly package. " + "PuDB allows you to debug code right where you write and test it--in " + "a terminal. If you've worked with the excellent (but nowadays " + "ancient) DOS-based Turbo Pascal or C tools, PuDB's UI might " + "look familiar.\n\n" + "New features in version 0.93:\n\n" + "- Stored preferences (no more pesky IPython prompt!)\n" + "- Themes\n" + "- Line numbers (optional)\n" + "\nHit Ctrl-P to set up PuDB.\n\n" + "If you're new here, welcome! The help screen (invoked by hitting " + "'?' after this message) should get you on your way." % VERSION) + + from pudb.settings import save_config + save_config(CONFIG) + try: if toplevel is None: toplevel = self.top @@ -1130,8 +1135,8 @@ class DebuggerUI(FrameVarInfoKeeper): from pudb import VERSION caption = [(None, - u"PuDB %s - The Python Urwid debugger - Hit ? for help" - u" - (C) Andreas Kloeckner 2009" + u"PuDB %s - ?:help, n:next, s:step into, b:breakpoint, o:console," + "t:run to cursor, !:python shell" % VERSION)] if self.debugger.post_mortem: diff --git a/pudb/settings.py b/pudb/settings.py new file mode 100644 index 0000000..c295575 --- /dev/null +++ b/pudb/settings.py @@ -0,0 +1,145 @@ +import os +from ConfigParser import ConfigParser + +# minor LGPL violation: stolen from python-xdg + +_home = os.environ.get('HOME', '/') +xdg_data_home = os.environ.get('XDG_DATA_HOME', + os.path.join(_home, '.local', 'share')) + +xdg_config_home = os.environ.get('XDG_CONFIG_HOME', + os.path.join(_home, '.config')) + +xdg_config_dirs = [xdg_config_home] + \ + os.environ.get('XDG_CONFIG_DIRS', '/etc/xdg').split(':') + +def save_config_path(*resource): + resource = os.path.join(*resource) + assert not resource.startswith('/') + path = os.path.join(xdg_config_home, resource) + if not os.path.isdir(path): + os.makedirs(path, 0700) + return path + +# end LGPL violation + +CONF_SECTION = "pudb" + +def load_config(): + from os.path import join, isdir + + cparser = ConfigParser() + + conf_dict = {} + try: + cparser.read([ + join(cdir, 'pudb', 'pudb.cfg') + for cdir in xdg_config_dirs if isdir(cdir)]) + + if cparser.has_section(CONF_SECTION): + conf_dict.update(dict(cparser.items(CONF_SECTION))) + except: + pass + + conf_dict.setdefault("shell", "classic") + conf_dict.setdefault("theme", "classic") + conf_dict.setdefault("line_numbers", False) + conf_dict.setdefault("seen_welcome", "a") + + def hack_bool(name): + try: + if conf_dict[name].lower() in ["0", "false", "off"]: + conf_dict[name] = False + except: + pass + + hack_bool("line_numbers") + + return conf_dict + + + + +def save_config(conf_dict): + from os.path import join + + cparser = ConfigParser() + cparser.add_section(CONF_SECTION) + + for key, val in conf_dict.iteritems(): + cparser.set(CONF_SECTION, key, val) + + try: + outf = open(join(save_config_path("pudb"), "pudb.cfg"), "w") + cparser.write(outf) + outf.close() + except: + pass + + + + + +def edit_config(ui, conf_dict): + import urwid + + cb_line_numbers = urwid.CheckBox("Show Line Numbers", + bool(conf_dict["line_numbers"])) + + shells = ["classic", "ipython"] + + shell_rb_grp = [] + shell_rbs = [ + urwid.RadioButton(shell_rb_grp, name, + conf_dict["shell"] == name) + for name in shells] + + from pudb.theme import THEMES + + known_theme = conf_dict["theme"] in THEMES + + theme_rb_grp = [] + theme_edit = urwid.Edit(edit_text=conf_dict["theme"]) + theme_rbs = [ + urwid.RadioButton(theme_rb_grp, name, + conf_dict["theme"] == name) + for name in THEMES]+[ + urwid.RadioButton(theme_rb_grp, "Custom:", + not known_theme), + urwid.Padding( + urwid.AttrWrap(theme_edit, "value"), + left=4), + + urwid.Text("\nTo use a custom theme, see example-theme.py in the " + "pudb distribution. Enter the full path to a file like it in the " + "box above."), + ] + + if ui.dialog( + urwid.ListBox( + [cb_line_numbers] + + [urwid.Text("")] + + [urwid.AttrWrap(urwid.Text("Shell:\n"), "group head")] + shell_rbs + + [urwid.AttrWrap(urwid.Text("\nTheme:\n"), "group head")] + theme_rbs, + ), + [ + ("OK", True), + ("Cancel", False), + ], + title="Edit Preferences"): + for shell, shell_rb in zip(shells, shell_rbs): + if shell_rb.get_state(): + conf_dict["shell"] = shell + + saw_theme = False + for theme, theme_rb in zip(THEMES, theme_rbs): + if theme_rb.get_state(): + conf_dict["theme"] = theme + saw_theme = True + + if not saw_theme: + conf_dict["theme"] = theme_edit.get_edit_text() + + conf_dict["line_numbers"] = cb_line_numbers.get_state() + + diff --git a/pudb/shell.py b/pudb/shell.py index f2ccf4c..a332437 100644 --- a/pudb/shell.py +++ b/pudb/shell.py @@ -4,7 +4,6 @@ except ImportError: HAVE_IPYTHON = False else: HAVE_IPYTHON = True - USE_IPYTHON = "ask" diff --git a/pudb/source_view.py b/pudb/source_view.py index 81173cc..0964bd4 100644 --- a/pudb/source_view.py +++ b/pudb/source_view.py @@ -4,11 +4,11 @@ import urwid class SourceLine(urwid.FlowWidget): - def __init__(self, dbg_ui, text, lineno='', attr=None, has_breakpoint=False): + def __init__(self, dbg_ui, text, line_nr='', attr=None, has_breakpoint=False): self.dbg_ui = dbg_ui self.text = text self.attr = attr - self.lineno = lineno + self.line_nr = line_nr self.has_breakpoint = has_breakpoint self.is_current = False self.highlight = False @@ -32,6 +32,9 @@ class SourceLine(urwid.FlowWidget): return 1 def render(self, (maxcol,), focus=False): + from pudb import CONFIG + render_line_nr = CONFIG["line_numbers"] + hscroll = self.dbg_ui.source_hscroll_start attrs = [] if self.is_current: @@ -53,7 +56,9 @@ class SourceLine(urwid.FlowWidget): attrs.append("highlighted") if not attrs and self.attr is not None: - attr = [("lineno", len(self.lineno))]+self.attr + attr = self.attr + if render_line_nr: + attr = [("line number", len(self.line_nr))] + attr else: attr = [(" ".join(attrs+["source"]), hscroll+maxcol-2)] @@ -66,7 +71,10 @@ class SourceLine(urwid.FlowWidget): self.dbg_ui.source_hscroll_start, rle_len(attr)) - text = crnt+bp+self.lineno+text + if render_line_nr: + text = self.line_nr + text + + text = crnt+bp+text attr = [("source", 1), ("bp_star", 1)] + attr # clipping ------------------------------------------------------------ @@ -88,13 +96,13 @@ class SourceLine(urwid.FlowWidget): def format_source(debugger_ui, lines, breakpoints): - lineno_str = "%%%dd "%(len(str(len(lines)))) + lineno_format = "%%%dd "%(len(str(len(lines)))) try: import pygments except ImportError: return [SourceLine(debugger_ui, line.rstrip("\n\r").replace("\t", 8*" "), - lineno_str%(i+1), None, + lineno_format % (i+1), None, has_breakpoint=i+1 in breakpoints) for i, line in enumerate(lines)] else: @@ -143,7 +151,7 @@ def format_source(debugger_ui, lines, breakpoints): result.append( SourceLine(debugger_ui, subself.current_line, - lineno_str%subself.lineno, + lineno_format % subself.lineno, subself.current_attr, has_breakpoint=subself.lineno in breakpoints)) subself.current_line = "" diff --git a/pudb/theme.py b/pudb/theme.py index ccb02ca..6cf36cb 100644 --- a/pudb/theme.py +++ b/pudb/theme.py @@ -1,4 +1,9 @@ -def get_palette(may_use_fancy_formats): +THEMES = ["classic", "vim"] + + + + +def get_palette(may_use_fancy_formats, theme="classic"): if may_use_fancy_formats: def add_setting(color, setting): return color+","+setting @@ -6,7 +11,7 @@ def get_palette(may_use_fancy_formats): def add_setting(color, setting): return color - return [ + palette = [ ("header", "black", "light gray", "standout"), ("breakpoint source", "yellow", "dark red"), @@ -75,6 +80,7 @@ def get_palette(may_use_fancy_formats): ("label", "black", "light gray"), ("value", "yellow", "dark blue"), ("fixed value", "light gray", "dark blue"), + ("group head", add_setting("black", "bold"), "light gray"), ("search box", "black", "dark cyan"), ("search not found", "white", "dark red"), @@ -89,7 +95,7 @@ def get_palette(may_use_fancy_formats): ("current focused source", "white", "dark cyan"), ("current highlighted source", "white", "dark cyan"), - ("lineno", "light gray", "dark blue"), + ("line number", "light gray", "dark blue"), ("keyword", add_setting("white", "bold"), "dark blue"), ("name", "light cyan", "dark blue"), ("literal", "light magenta", "dark blue"), @@ -99,3 +105,37 @@ def get_palette(may_use_fancy_formats): ] + palette_dict = dict( + (entry[0], entry[1:]) for entry in palette) + + if theme == "classic": + pass + elif theme == "vim": + palette_dict.update({ + "source": ("black", "default"), + "keyword": ("brown", "default"), + "kw_namespace": ("dark magenta", "default"), + "literal": ("black", "default"), + "string": ("dark red", "default"), + "punctuation": ("black", "default"), + "comment": ("dark blue", "default"), + "classname": ("dark cyan", "default"), + "name": ("dark cyan", "default"), + "line number": ("dark gray", "default"), + "bp_star": ("dark red", "default"), + }) + else: + try: + symbols = { + "palette": palette_dict, + "add_setting": add_setting, + } + execfile(theme, symbols) + except: + print "Error when importing theme:" + from traceback import print_exc + print_exc() + raw_input("Hit enter:") + + return [(key,)+value for key, value in palette_dict.iteritems()] + diff --git a/setup.py b/setup.py index 4e6b573..cd8273d 100644 --- a/setup.py +++ b/setup.py @@ -70,12 +70,15 @@ setup(name='pudb', python -m pudb.run my-script.py - Documentation - ------------- + Documentation and Support + ------------------------- PuDB has a `wiki `_, where documentation and debugging wisdom are collected. + PuDB also has a `mailing list `_ that + you may use to submit patches and requests for help. + Programming PuDB ----------------