OLD | NEW |
(Empty) | |
| 1 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
| 2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
| 3 # |
| 4 # This file is part of logilab-common. |
| 5 # |
| 6 # logilab-common is free software: you can redistribute it and/or modify it unde
r |
| 7 # the terms of the GNU Lesser General Public License as published by the Free |
| 8 # Software Foundation, either version 2.1 of the License, or (at your option) an
y |
| 9 # later version. |
| 10 # |
| 11 # logilab-common is distributed in the hope that it will be useful, but WITHOUT |
| 12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| 13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
| 14 # details. |
| 15 # |
| 16 # You should have received a copy of the GNU Lesser General Public License along |
| 17 # with logilab-common. If not, see <http://www.gnu.org/licenses/>. |
| 18 """Customized version of pdb's default debugger. |
| 19 |
| 20 - sets up a history file |
| 21 - uses ipython if available to colorize lines of code |
| 22 - overrides list command to search for current block instead |
| 23 of using 5 lines of context |
| 24 |
| 25 |
| 26 |
| 27 |
| 28 """ |
| 29 |
| 30 from __future__ import print_function |
| 31 |
| 32 __docformat__ = "restructuredtext en" |
| 33 |
| 34 try: |
| 35 import readline |
| 36 except ImportError: |
| 37 readline = None |
| 38 import os |
| 39 import os.path as osp |
| 40 import sys |
| 41 from pdb import Pdb |
| 42 import inspect |
| 43 |
| 44 from logilab.common.compat import StringIO |
| 45 |
| 46 try: |
| 47 from IPython import PyColorize |
| 48 except ImportError: |
| 49 def colorize(source, *args): |
| 50 """fallback colorize function""" |
| 51 return source |
| 52 def colorize_source(source, *args): |
| 53 return source |
| 54 else: |
| 55 def colorize(source, start_lineno, curlineno): |
| 56 """colorize and annotate source with linenos |
| 57 (as in pdb's list command) |
| 58 """ |
| 59 parser = PyColorize.Parser() |
| 60 output = StringIO() |
| 61 parser.format(source, output) |
| 62 annotated = [] |
| 63 for index, line in enumerate(output.getvalue().splitlines()): |
| 64 lineno = index + start_lineno |
| 65 if lineno == curlineno: |
| 66 annotated.append('%4s\t->\t%s' % (lineno, line)) |
| 67 else: |
| 68 annotated.append('%4s\t\t%s' % (lineno, line)) |
| 69 return '\n'.join(annotated) |
| 70 |
| 71 def colorize_source(source): |
| 72 """colorize given source""" |
| 73 parser = PyColorize.Parser() |
| 74 output = StringIO() |
| 75 parser.format(source, output) |
| 76 return output.getvalue() |
| 77 |
| 78 |
| 79 def getsource(obj): |
| 80 """Return the text of the source code for an object. |
| 81 |
| 82 The argument may be a module, class, method, function, traceback, frame, |
| 83 or code object. The source code is returned as a single string. An |
| 84 IOError is raised if the source code cannot be retrieved.""" |
| 85 lines, lnum = inspect.getsourcelines(obj) |
| 86 return ''.join(lines), lnum |
| 87 |
| 88 |
| 89 ################################################################ |
| 90 class Debugger(Pdb): |
| 91 """custom debugger |
| 92 |
| 93 - sets up a history file |
| 94 - uses ipython if available to colorize lines of code |
| 95 - overrides list command to search for current block instead |
| 96 of using 5 lines of context |
| 97 """ |
| 98 def __init__(self, tcbk=None): |
| 99 Pdb.__init__(self) |
| 100 self.reset() |
| 101 if tcbk: |
| 102 while tcbk.tb_next is not None: |
| 103 tcbk = tcbk.tb_next |
| 104 self._tcbk = tcbk |
| 105 self._histfile = os.path.expanduser("~/.pdbhist") |
| 106 |
| 107 def setup_history_file(self): |
| 108 """if readline is available, read pdb history file |
| 109 """ |
| 110 if readline is not None: |
| 111 try: |
| 112 # XXX try..except shouldn't be necessary |
| 113 # read_history_file() can accept None |
| 114 readline.read_history_file(self._histfile) |
| 115 except IOError: |
| 116 pass |
| 117 |
| 118 def start(self): |
| 119 """starts the interactive mode""" |
| 120 self.interaction(self._tcbk.tb_frame, self._tcbk) |
| 121 |
| 122 def setup(self, frame, tcbk): |
| 123 """setup hook: set up history file""" |
| 124 self.setup_history_file() |
| 125 Pdb.setup(self, frame, tcbk) |
| 126 |
| 127 def set_quit(self): |
| 128 """quit hook: save commands in the history file""" |
| 129 if readline is not None: |
| 130 readline.write_history_file(self._histfile) |
| 131 Pdb.set_quit(self) |
| 132 |
| 133 def complete_p(self, text, line, begin_idx, end_idx): |
| 134 """provide variable names completion for the ``p`` command""" |
| 135 namespace = dict(self.curframe.f_globals) |
| 136 namespace.update(self.curframe.f_locals) |
| 137 if '.' in text: |
| 138 return self.attr_matches(text, namespace) |
| 139 return [varname for varname in namespace if varname.startswith(text)] |
| 140 |
| 141 |
| 142 def attr_matches(self, text, namespace): |
| 143 """implementation coming from rlcompleter.Completer.attr_matches |
| 144 Compute matches when text contains a dot. |
| 145 |
| 146 Assuming the text is of the form NAME.NAME....[NAME], and is |
| 147 evaluatable in self.namespace, it will be evaluated and its attributes |
| 148 (as revealed by dir()) are used as possible completions. (For class |
| 149 instances, class members are also considered.) |
| 150 |
| 151 WARNING: this can still invoke arbitrary C code, if an object |
| 152 with a __getattr__ hook is evaluated. |
| 153 |
| 154 """ |
| 155 import re |
| 156 m = re.match(r"(\w+(\.\w+)*)\.(\w*)", text) |
| 157 if not m: |
| 158 return |
| 159 expr, attr = m.group(1, 3) |
| 160 object = eval(expr, namespace) |
| 161 words = dir(object) |
| 162 if hasattr(object, '__class__'): |
| 163 words.append('__class__') |
| 164 words = words + self.get_class_members(object.__class__) |
| 165 matches = [] |
| 166 n = len(attr) |
| 167 for word in words: |
| 168 if word[:n] == attr and word != "__builtins__": |
| 169 matches.append("%s.%s" % (expr, word)) |
| 170 return matches |
| 171 |
| 172 def get_class_members(self, klass): |
| 173 """implementation coming from rlcompleter.get_class_members""" |
| 174 ret = dir(klass) |
| 175 if hasattr(klass, '__bases__'): |
| 176 for base in klass.__bases__: |
| 177 ret = ret + self.get_class_members(base) |
| 178 return ret |
| 179 |
| 180 ## specific / overridden commands |
| 181 def do_list(self, arg): |
| 182 """overrides default list command to display the surrounding block |
| 183 instead of 5 lines of context |
| 184 """ |
| 185 self.lastcmd = 'list' |
| 186 if not arg: |
| 187 try: |
| 188 source, start_lineno = getsource(self.curframe) |
| 189 print(colorize(''.join(source), start_lineno, |
| 190 self.curframe.f_lineno)) |
| 191 except KeyboardInterrupt: |
| 192 pass |
| 193 except IOError: |
| 194 Pdb.do_list(self, arg) |
| 195 else: |
| 196 Pdb.do_list(self, arg) |
| 197 do_l = do_list |
| 198 |
| 199 def do_open(self, arg): |
| 200 """opens source file corresponding to the current stack level""" |
| 201 filename = self.curframe.f_code.co_filename |
| 202 lineno = self.curframe.f_lineno |
| 203 cmd = 'emacsclient --no-wait +%s %s' % (lineno, filename) |
| 204 os.system(cmd) |
| 205 |
| 206 do_o = do_open |
| 207 |
| 208 def pm(): |
| 209 """use our custom debugger""" |
| 210 dbg = Debugger(sys.last_traceback) |
| 211 dbg.start() |
| 212 |
| 213 def set_trace(): |
| 214 Debugger().set_trace(sys._getframe().f_back) |
OLD | NEW |