# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# 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
"""
__docformat__ = "restructuredtext en"
try:
import readline
except ImportError:
# mypy: Incompatible types in assignment (expression has type "None",
# mypy: variable has type Module))
# conditional import
readline = None # type: ignore
import os
import sys
from pdb import Pdb
import inspect
from io import StringIO
try:
from IPython import PyColorize
except ImportError:
def colorize(source, start_lineno, curlineno):
"""fallback colorize function"""
return source
def colorize_source(source):
return source
else:
[docs] 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(f"{lineno:>4}\t->\t{line}")
else:
annotated.append(f"{lineno:>4}\t\t{line}")
return "\n".join(annotated)
[docs] def colorize_source(source):
"""colorize given source"""
parser = PyColorize.Parser()
output = StringIO()
parser.format(source, output)
return output.getvalue()
[docs]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
################################################################
[docs]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")
[docs] 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 OSError:
pass
[docs] def start(self):
"""starts the interactive mode"""
self.interaction(self._tcbk.tb_frame, self._tcbk)
[docs] def setup(self, frame, tcbk):
"""setup hook: set up history file"""
self.setup_history_file()
Pdb.setup(self, frame, tcbk)
[docs] 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)
[docs] 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)]
[docs] 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(f"{expr}.{word}")
return matches
[docs] 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
[docs] 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 OSError:
Pdb.do_list(self, arg)
else:
Pdb.do_list(self, arg)
do_l = do_list
[docs] 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 = f"emacsclient --no-wait +{lineno} {filename}"
os.system(cmd)
do_o = do_open
[docs]def pm():
"""use our custom debugger"""
dbg = Debugger(sys.last_traceback)
dbg.start()
[docs]def set_trace():
Debugger().set_trace(sys._getframe().f_back)