Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Make the default stringifier a setting #9

Merged
merged 6 commits into from

2 participants

@asmeurer
Collaborator

Right now, only type, str, and repr are allowed, but the ability to
define your own stringifier will be added.

With the code cleanups we've done, adding new options is really easy.

I still want to add the ability to define your own custom stringifier (I think I will just require the user to define it in a file), but I am too tired to do any more work today. You can merge this if you want, or wait for the rest of the fixes.

asmeurer added some commits
@asmeurer asmeurer Make the default stringifier a setting
Right now, only type, str, and repr are allowed, but the ability to
define your own stringifier will be added.
90d21d3
@asmeurer asmeurer Don't try to update a custom theme automatically in the prefs window
This doesn't work, as the user will likely try to choose "Custom" before
entering a file name, resulting in an error.  So instead, make choosing
"Custom" select the file entry box, and don't try to load it until the
preferences dialog is closed.  Built-in themes are still updated
automatically.
654504a
@asmeurer asmeurer Add support for custom stringifiers to PuDB
Also included in this commit is a change that makes the custom theme and
custom stringifier preferences persistant, so that the user does not
have to re-enter them if deselects "Custom".  This wasn't easy to split
out, so it's included here.

I still need to add support for manually choosing a the custom
stringifier on a per-variable basis.
820be5f
@asmeurer asmeurer Add example-stringifier.py file
This shows one possible use of the custom stringifier functionality.
The example function computes str(), unless it takes longer than one
second to compute (it uses signal.alarm() to determine this), in which
case it falls back to type.
9f8cb66
@asmeurer asmeurer Add some more useful information to the example-stringifier.py file acacfaa
@asmeurer asmeurer Add support for custom stringifiers on a per-variable basis
The keyboard shortcut for custom stringifiers is c.
4d287ba
@asmeurer
Collaborator

OK, this is ready to be reviewed. I've added full support for custom stringifiers. You just give the path to a file that contains a function pudb_stringifier at the module level, and it will use that. I included an example-stringifier.py to show one potential example (the one from the mailing list).

I also made some improvements to the custom stuff for both stringifiers and themes (see the commit messages). For example, choosing "custom" now selects the text entry field, and the stuff is not auto-updated (since the user will likely not have entered the file path yet). Also, the paths are now persistant, even if the user chooses a non-custom option. This is particularly useful for custom stringifiers, as you can manually change the stringifier for each variable, which means that you can use the custom one even if it isn't selected as the default.

@inducer inducer merged commit 4018c98 into inducer:master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jul 28, 2011
  1. @asmeurer

    Make the default stringifier a setting

    asmeurer authored
    Right now, only type, str, and repr are allowed, but the ability to
    define your own stringifier will be added.
  2. @asmeurer

    Don't try to update a custom theme automatically in the prefs window

    asmeurer authored
    This doesn't work, as the user will likely try to choose "Custom" before
    entering a file name, resulting in an error.  So instead, make choosing
    "Custom" select the file entry box, and don't try to load it until the
    preferences dialog is closed.  Built-in themes are still updated
    automatically.
Commits on Jul 29, 2011
  1. @asmeurer

    Add support for custom stringifiers to PuDB

    asmeurer authored
    Also included in this commit is a change that makes the custom theme and
    custom stringifier preferences persistant, so that the user does not
    have to re-enter them if deselects "Custom".  This wasn't easy to split
    out, so it's included here.
    
    I still need to add support for manually choosing a the custom
    stringifier on a per-variable basis.
  2. @asmeurer

    Add example-stringifier.py file

    asmeurer authored
    This shows one possible use of the custom stringifier functionality.
    The example function computes str(), unless it takes longer than one
    second to compute (it uses signal.alarm() to determine this), in which
    case it falls back to type.
  3. @asmeurer
  4. @asmeurer

    Add support for custom stringifiers on a per-variable basis

    asmeurer authored
    The keyboard shortcut for custom stringifiers is c.
This page is out of date. Refresh to see the latest.
View
90 example-stringifier.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python
+"""
+This file shows how you can define a custom stringifier for PuDB.
+
+A stringifier is a function that is called on the variables in the namespace
+for display in the variables list. The default is type()*, as this is fast and
+cannot fail. PuDB also includes built-in options for using str() and repr().
+
+Note that str() and repr() will be slower than type(), which is especially
+noticable when you have many varialbes, or some of your variables have very
+large string/repr representations.
+
+Also note that if you just want to change the type for one or two variables,
+you can do that by selecting the variable in the variables list and pressing
+Enter, or by pressing t, s, or r.
+
+To define a custom stringifier, create a file like this one with a function
+called pudb_stringifier() at the module level. pudb_stringifier(obj) should
+return a string value for an object (note that str() will always be called on
+the result). Note that the file will be execfile'd.
+
+Then, go to the PuDB preferences window (type Ctrl-p inside of
+PuDB), and add the path to the file in the "Custom" field under the "Variable
+Stringifier" heading.
+
+The example in this file returns the string value, unless it take more than a
+second to compute, in which case it falls back to the type.
+
+TIP: Run "python -m pudb.run example-stringifier.py and set this file to be
+your stringifier in the settings to see how it works.
+
+You can use custom stringifiers to do all sorts of things: callbacks, custom
+views on variables of interest without having to use a watch variable or the
+expanded view, etc.
+
+* - Actually, the default is a mix between type() and str(). str() is used for
+ a handful of "safe" types for which it is guaranteed to be fast and not to
+ fail.
+"""
+import time
+import signal
+
+class TimeOutError(Exception):
+ pass
+
+def timeout(signum, frame, time):
+ raise TimeOutError("Timed out after %d seconds" % time)
+
+def run_with_timeout(code, time, globals=None):
+ """
+ Evaluate ``code``, timing out after ``time`` seconds.
+
+ ``time`` must be an integer. The return value is whatever ``code`` returns.
+ """
+ # Set the signal handler and a ``time``-second alarm
+ signal.signal(signal.SIGALRM, lambda s, f: timeout(s, f, time))
+ signal.alarm(time)
+ r = eval(code, globals)
+ signal.alarm(0) # Disable the alarm
+ return r
+
+def pudb_stringifier(obj):
+ """
+ This is the custom stringifier.
+
+ It returns str(obj), unless it take more than a second to compute,
+ in which case it falls back to type(obj).
+ """
+ try:
+ return run_with_timeout("str(obj)", 1, {'obj':obj})
+ except TimeOutError:
+ return (type(obj), "(str too slow to compute)")
+
+# Example usage
+
+class FastString(object):
+ def __str__(self):
+ return "This was fast to compute."
+
+class SlowString(object):
+ def __str__(self):
+ time.sleep(10) # Return the string value after ten seconds
+ return "This was slow to compute."
+
+fast = FastString()
+slow = SlowString()
+
+# If you are running this in PuDB, set this file as your custom stringifier in
+# the prefs (Ctrl-p) and run to here. Notice how fast shows the string value,
+# but slow shows the type, as the string value takes too long to compute.
View
18 pudb/debugger.py
@@ -55,7 +55,7 @@
Keys in variables list:
\ - expand/collapse
- t/r/s - show type/repr/str for this variable
+ t/r/s/c - show type/repr/str/custom for this variable
h - toggle highlighting
@ - toggle repetition at top
* - toggle private members
@@ -374,6 +374,7 @@ def change_var_state(w, size, key):
elif key == "t": iinfo.display_type = "type"
elif key == "r": iinfo.display_type = "repr"
elif key == "s": iinfo.display_type = "str"
+ elif key == "c": iinfo.display_type = CONFIG["custom_stringifier"]
elif key == "h": iinfo.highlighted = not iinfo.highlighted
elif key == "@": iinfo.repeated_at_top = not iinfo.repeated_at_top
elif key == "*": iinfo.show_private_members = not iinfo.show_private_members
@@ -418,6 +419,9 @@ def edit_inspector_detail(w, size, key):
iinfo.display_type == "repr")
rb_show_str = urwid.RadioButton(rb_grp, "Show str()",
iinfo.display_type == "str")
+ rb_show_custom = urwid.RadioButton(rb_grp, "Show custom (set in prefs)",
+ iinfo.display_type == CONFIG["custom_stringifier"])
+
expanded_checkbox = urwid.CheckBox("Expanded", iinfo.show_detail)
highlighted_checkbox = urwid.CheckBox("Highlighted", iinfo.highlighted)
@@ -442,9 +446,14 @@ def edit_inspector_detail(w, size, key):
iinfo.repeated_at_top = repeated_at_top_checkbox.get_state()
iinfo.show_private_members = show_private_checkbox.get_state()
- if rb_show_type.get_state(): iinfo.display_type = "type"
- elif rb_show_repr.get_state(): iinfo.display_type = "repr"
- elif rb_show_str.get_state(): iinfo.display_type = "str"
+ if rb_show_type.get_state():
+ iinfo.display_type = "type"
+ elif rb_show_repr.get_state():
+ iinfo.display_type = "repr"
+ elif rb_show_str.get_state():
+ iinfo.display_type = "str"
+ elif rb_show_custom.get_state():
+ iinfo.display_type = CONFIG["custom_stringifier"]
if var.watch_expr is not None:
var.watch_expr.expression = watch_edit.get_edit_text()
@@ -480,6 +489,7 @@ def insert_watch(w, size, key):
self.var_list.listen("t", change_var_state)
self.var_list.listen("r", change_var_state)
self.var_list.listen("s", change_var_state)
+ self.var_list.listen("c", change_var_state)
self.var_list.listen("h", change_var_state)
self.var_list.listen("@", change_var_state)
self.var_list.listen("enter", edit_inspector_detail)
View
133 pudb/settings.py
@@ -32,8 +32,6 @@ def get_save_config_path(*resource):
BREAKPOINTS_FILE_NAME = "breakpoints"
-
-
def load_config():
from os.path import join, isdir
@@ -62,6 +60,11 @@ def load_config():
conf_dict.setdefault("current_stack_frame", "top")
+ conf_dict.setdefault("stringifier", "type")
+
+ conf_dict.setdefault("custom_theme", "")
+ conf_dict.setdefault("custom_stringifier", "")
+
def normalize_bool_inplace(name):
try:
if conf_dict[name].lower() in ["0", "false", "off"]:
@@ -114,6 +117,11 @@ def _update_line_numbers():
def _update_current_stack_frame():
ui.update_stack()
+ def _update_stringifier():
+ import pudb.var_view
+ pudb.var_view.custom_stringifier_dict = {}
+ ui.update_var_view()
+
def _update_config(check_box, new_state, option_newvalue):
option, newvalue = option_newvalue
new_conf_dict = {option: newvalue}
@@ -121,7 +129,10 @@ def _update_config(check_box, new_state, option_newvalue):
# only activate if the new state of the radio button is 'on'
if new_state:
if newvalue is None:
- newvalue = theme_edit.get_edit_text()
+ # Select the custom theme entry dialog
+ # XXX: Is there a better way to do this?
+ lb.set_focus(13)
+ return
conf_dict.update(theme=newvalue)
_update_theme()
@@ -136,6 +147,17 @@ def _update_config(check_box, new_state, option_newvalue):
if new_state:
conf_dict.update(new_conf_dict)
_update_current_stack_frame()
+
+ elif option == "stringifier":
+ # only activate if the new state of the radio button is 'on'
+ if new_state:
+ if newvalue is None:
+ lb.set_focus(25)
+ return
+
+ conf_dict.update(stringifier=newvalue)
+ _update_stringifier()
+
heading = urwid.Text("This is the preferences screen for PuDB. "
"Hit Ctrl-P at any time to get back to it.\n\n"
"Configuration settings are saved in "
@@ -148,9 +170,9 @@ def _update_config(check_box, new_state, option_newvalue):
shell_info = urwid.Text("This is the shell that will be used when you hit '!'.\n")
shells = ["classic", "ipython"]
- shell_rb_grp = []
+ shell_rb_group = []
shell_rbs = [
- urwid.RadioButton(shell_rb_grp, name,
+ urwid.RadioButton(shell_rb_group, name,
conf_dict["shell"] == name)
for name in shells]
@@ -158,14 +180,14 @@ def _update_config(check_box, new_state, option_newvalue):
known_theme = conf_dict["theme"] in THEMES
- theme_rb_grp = []
- theme_edit = urwid.Edit(edit_text=conf_dict["theme"])
+ theme_rb_group = []
+ theme_edit = urwid.Edit(edit_text=conf_dict["custom_theme"])
theme_rbs = [
- urwid.RadioButton(theme_rb_grp, name,
+ urwid.RadioButton(theme_rb_group, name,
conf_dict["theme"] == name, on_state_change=_update_config,
user_data=("theme", name))
for name in THEMES]+[
- urwid.RadioButton(theme_rb_grp, "Custom:",
+ urwid.RadioButton(theme_rb_group, "Custom:",
not known_theme, on_state_change=_update_config,
user_data=("theme", None)),
urwid.Padding(
@@ -174,7 +196,9 @@ def _update_config(check_box, new_state, option_newvalue):
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. '~' will be expanded to your home directory."),
+ "box above. '~' will be expanded to your home directory. "
+ "Note that a custom theme will not be applied until you close "
+ "this dialog."),
]
stack_rb_group = []
@@ -188,28 +212,76 @@ def _update_config(check_box, new_state, option_newvalue):
for name in stack_opts
]
- if ui.dialog(
- urwid.ListBox(
- [heading]
- + [cb_line_numbers]
- + [urwid.Text("")]
- + [urwid.AttrMap(urwid.Text("Shell:\n"), "group head")]
- + [shell_info]
- + shell_rbs
- + [urwid.AttrMap(urwid.Text("\nTheme:\n"), "group head")]
- + theme_rbs
- + [urwid.AttrMap(urwid.Text("\nStack Order:\n"), "group head")]
- + [stack_info]
- + stack_rbs
- ),
- [
- ("OK", True),
- ("Cancel", False),
- ],
+ stringifier_opts = ["type", "str", "repr"]
+ known_stringifier = conf_dict["stringifier"] in stringifier_opts
+ stringifier_rb_group = []
+ stringifier_edit = urwid.Edit(edit_text=conf_dict["custom_stringifier"])
+ stringifier_info = urwid.Text("This is the default function that will be "
+ "called on variables in the variables list. Note that you can change "
+ "this on a per-variable basis by selecting a variable and hitting Enter "
+ "or by typing t/s/r. Note that str and repr will be slower than type "
+ "and have the potential to crash PuDB.")
+ stringifier_rbs = [
+ urwid.RadioButton(stringifier_rb_group, name,
+ conf_dict["stringifier"] == name,
+ on_state_change=_update_config,
+ user_data=("stringifier", name))
+ for name in stringifier_opts
+ ]+[
+ urwid.RadioButton(stringifier_rb_group, "Custom:",
+ not known_stringifier, on_state_change=_update_config,
+ user_data=("stringifier", None)),
+ urwid.Padding(
+ urwid.AttrMap(stringifier_edit, "value"),
+ left=4),
+
+ urwid.Text("\nTo use a custom stringifier, see example-stringifier.py "
+ "in the pudb distribution. Enter the full path to a file like "
+ "it in the box above. '~' will be expanded to your home directory. "
+ "The file should contain a function called pudb_stringifier() "
+ "at the module level, which should take a single argument and "
+ "return the desired string form of the object passed to it. "
+ "Note that the variables view will not be updated until you "
+ "close this dialog."),
+ ]
+
+ lb = urwid.ListBox(
+ [heading]
+ + [urwid.AttrMap(urwid.Text("Line Numbers:\n"), "group head")]
+ + [cb_line_numbers]
+ + [urwid.AttrMap(urwid.Text("\nShell:\n"), "group head")]
+ + [shell_info]
+ + shell_rbs
+ + [urwid.AttrMap(urwid.Text("\nTheme:\n"), "group head")]
+ + theme_rbs
+ + [urwid.AttrMap(urwid.Text("\nStack Order:\n"), "group head")]
+ + [stack_info]
+ + stack_rbs
+ + [urwid.AttrMap(urwid.Text("\nVariable Stringifier:\n"), "group head")]
+ + [stringifier_info]
+ + stringifier_rbs
+ )
+
+
+ if ui.dialog(lb, [
+ ("OK", True),
+ ("Cancel", False),
+ ],
title="Edit Preferences"):
+ # Only update the settings here that instant-apply (above) doesn't take
+ # care of.
+
+ # if we had a custom theme, it wasn't updated live
+ if theme_rb_group[-1].state:
+ newvalue = theme_edit.get_edit_text()
+ conf_dict.update(theme=newvalue, custom_theme=newvalue)
+ _update_theme()
- # Only update the shell setting here. Instant-apply (above) takes care
- # of updating everything else.
+ # Ditto for custom stringifiers
+ if stringifier_rb_group[-1].state:
+ newvalue = stringifier_edit.get_edit_text()
+ conf_dict.update(stringifier=newvalue, custom_stringifier=newvalue)
+ _update_stringifier()
for shell, shell_rb in zip(shells, shell_rbs):
if shell_rb.get_state():
@@ -220,6 +292,7 @@ def _update_config(check_box, new_state, option_newvalue):
_update_theme()
# _update_line_numbers() is equivalent to _update_theme()
_update_current_stack_frame()
+ _update_stringifier()
View
54 pudb/var_view.py
@@ -7,6 +7,8 @@
except ImportError:
HAVE_NUMPY = 0
+from pudb import CONFIG
+
# data ------------------------------------------------------------------------
class FrameVarInfo(object):
def __init__(self):
@@ -24,7 +26,7 @@ def get_inspect_info(self, id_path, read_only):
class InspectInfo(object):
def __init__(self):
self.show_detail = False
- self.display_type = "type"
+ self.display_type = CONFIG["stringifier"]
self.highlighted = False
self.repeated_at_top = False
self.show_private_members = False
@@ -121,7 +123,43 @@ def render(self, size, focus=False):
def keypress(self, size, key):
return key
+custom_stringifier_dict = {}
+def get_stringifier(iinfo):
+ if iinfo.display_type == "type":
+ def _stringifier(value):
+ if HAVE_NUMPY and isinstance(value, numpy.ndarray):
+ return "ndarray %s %s" % (value.dtype, value.shape)
+ elif isinstance(value, STR_SAFE_TYPES):
+ return str(value)
+ else:
+ return type(value).__name__
+ return _stringifier
+ elif iinfo.display_type == "repr":
+ return repr
+ elif iinfo.display_type == "str":
+ return str
+ else:
+ try:
+ if not custom_stringifier_dict: # Only execfile once
+ from os.path import expanduser
+ execfile(expanduser(iinfo.display_type), custom_stringifier_dict)
+ except:
+ print "Error when importing custom stringifier:"
+ from traceback import print_exc
+ print_exc()
+ raw_input("Hit enter:")
+ return lambda value: "ERROR: Invalid custom stringifier file."
+ else:
+ if "pudb_stringifier" not in custom_stringifier_dict:
+ print "%s does not contain " % iinfo.display_type
+ "a function named pudb_stringifier at the module level."
+ raw_input("Hit enter:")
+ return lambda value: ("ERROR: Invalid custom stringifier file: "
+ "pudb_stringifer not defined.")
+ else:
+ return (lambda value:
+ str(custom_stringifier_dict["pudb_stringifier"](value)))
# tree walking ----------------------------------------------------------------
@@ -140,19 +178,7 @@ def walk_value(self, prefix, label, value, id_path=None, attr_prefix=None):
elif isinstance(value, (str, unicode)):
self.add_item(prefix, label, repr(value)[:200], id_path, attr_prefix)
else:
- if iinfo.display_type == "type":
- if HAVE_NUMPY and isinstance(value, numpy.ndarray):
- displayed_value = "ndarray %s %s" % (value.dtype, value.shape)
- elif isinstance(value, STR_SAFE_TYPES):
- displayed_value = str(value)
- else:
- displayed_value = type(value).__name__
- elif iinfo.display_type == "repr":
- displayed_value = repr(value)
- elif iinfo.display_type == "str":
- displayed_value = str(value)
- else:
- displayed_value = "ERROR: Invalid display_type"
+ displayed_value = get_stringifier(iinfo)(value)
self.add_item(prefix, label,
displayed_value, id_path, attr_prefix)
Something went wrong with that request. Please try again.