# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:[email protected]
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common. If not, see <http://www.gnu.org/licenses/>.
"""Customized version of pdb's default debugger.
- sets up a history file
- uses ipython if available to colorize lines of code
- overrides list command to search for current block instead
of using 5 lines of context
"""
from __future__ import print_function
__docformat__ = "restructuredtext en"
try:
import readline
except ImportError:
readline = None
import os
import os.path as osp
import sys
from pdb import Pdb
import inspect
from logilab.common.compat import StringIO
try:
from IPython import PyColorize
except ImportError:
def colorize(source, *args):
"""fallback colorize function"""
return source
def colorize_source(source, *args):
return source
else:
def colorize(source, start_lineno, curlineno):
"""colorize and annotate source with linenos
(as in pdb's list command)
"""
parser = PyColorize.Parser()
output = StringIO()
parser.format(source, output)
annotated = []
for index, line in enumerate(output.getvalue().splitlines()):
lineno = index + start_lineno
if lineno == curlineno:
annotated.append('%4s\t->\t%s' % (lineno, line))
else:
annotated.append('%4s\t\t%s' % (lineno, line))
return '\n'.join(annotated)
def colorize_source(source):
"""colorize given source"""
parser = PyColorize.Parser()
output = StringIO()
parser.format(source, output)
return output.getvalue()
def getsource(obj):
"""Return the text of the source code for an object.
The argument may be a module, class, method, function, traceback, frame,
or code object. The source code is returned as a single string. An
IOError is raised if the source code cannot be retrieved."""
lines, lnum = inspect.getsourcelines(obj)
return ''.join(lines), lnum
################################################################
class Debugger(Pdb):
"""custom debugger
- sets up a history file
- uses ipython if available to colorize lines of code
- overrides list command to search for current block instead
of using 5 lines of context
"""
def __init__(self, tcbk=None):
Pdb.__init__(self)
self.reset()
if tcbk:
while tcbk.tb_next is not None:
tcbk = tcbk.tb_next
self._tcbk = tcbk
self._histfile = os.path.expanduser("~/.pdbhist")
def setup_history_file(self):
"""if readline is available, read pdb history file
"""
if readline is not None:
try:
# XXX try..except shouldn't be necessary
# read_history_file() can accept None
readline.read_history_file(self._histfile)
except IOError:
pass
def start(self):
"""starts the interactive mode"""
self.interaction(self._tcbk.tb_frame, self._tcbk)
def setup(self, frame, tcbk):
"""setup hook: set up history file"""
self.setup_history_file()
Pdb.setup(self, frame, tcbk)
def set_quit(self):
"""quit hook: save commands in the history file"""
if readline is not None:
readline.write_history_file(self._histfile)
Pdb.set_quit(self)
def complete_p(self, text, line, begin_idx, end_idx):
"""provide variable names completion for the ``p`` command"""
namespace = dict(self.curframe.f_globals)
namespace.update(self.curframe.f_locals)
if '.' in text:
return self.attr_matches(text, namespace)
return [varname for varname in namespace if varname.startswith(text)]
def attr_matches(self, text, namespace):
"""implementation coming from rlcompleter.Completer.attr_matches
Compute matches when text contains a dot.
Assuming the text is of the form NAME.NAME....[NAME], and is
evaluatable in self.namespace, it will be evaluated and its attributes
(as revealed by dir()) are used as possible completions. (For class
instances, class members are also considered.)
WARNING: this can still invoke arbitrary C code, if an object
with a __getattr__ hook is evaluated.
"""
import re
m = re.match(r"(\w+(\.\w+)*)\.(\w*)", text)
if not m:
return
expr, attr = m.group(1, 3)
object = eval(expr, namespace)
words = dir(object)
if hasattr(object, '__class__'):
words.append('__class__')
words = words + self.get_class_members(object.__class__)
matches = []
n = len(attr)
for word in words:
if word[:n] == attr and word != "__builtins__":
matches.append("%s.%s" % (expr, word))
return matches
def get_class_members(self, klass):
"""implementation coming from rlcompleter.get_class_members"""
ret = dir(klass)
if hasattr(klass, '__bases__'):
for base in klass.__bases__:
ret = ret + self.get_class_members(base)
return ret
## specific / overridden commands
def do_list(self, arg):
"""overrides default list command to display the surrounding block
instead of 5 lines of context
"""
self.lastcmd = 'list'
if not arg:
try:
source, start_lineno = getsource(self.curframe)
print(colorize(''.join(source), start_lineno,
self.curframe.f_lineno))
except KeyboardInterrupt:
pass
except IOError:
Pdb.do_list(self, arg)
else:
Pdb.do_list(self, arg)
do_l = do_list
def do_open(self, arg):
"""opens source file corresponding to the current stack level"""
filename = self.curframe.f_code.co_filename
lineno = self.curframe.f_lineno
cmd = 'emacsclient --no-wait +%s %s' % (lineno, filename)
os.system(cmd)
do_o = do_open
def pm():
"""use our custom debugger"""
dbg = Debugger(sys.last_traceback)
dbg.start()
def set_trace():
Debugger().set_trace(sys._getframe().f_back)