0
|
1 """ |
|
2 Pdb++, a fancier version of pdb |
|
3 =============================== |
|
4 |
|
5 This module extends the stdlib pdb in numerous ways, e.g. by providing |
|
6 real completion of Python values instead of pdb's own commands, or by |
|
7 adding few convenience commands like ``longlist``, ``interact`` or |
|
8 ``watch``. |
|
9 |
|
10 For a full explanation of each command, refer to the docstring or type |
|
11 help <command> at the prompt. |
|
12 |
|
13 Installation |
|
14 ------------ |
|
15 |
|
16 This module is meant to replace stdlib's pdb.py from the outsite; |
|
17 simply put it in a directory in your PYTHONPATH, and you can start |
|
18 using it immediately. Since it's named pdb.py, every place which |
|
19 imports pdb will now find the new module. |
|
20 |
|
21 Dependencies |
|
22 ------------ |
|
23 |
|
24 To work properly, this module needs `rlcompleter_ng`_ to be installed. |
|
25 |
|
26 To enable syntax highlighting, you must install `pygments`. |
|
27 |
|
28 .. _pygments: http://pygments.org/ |
|
29 .. _`rlcompleter_ng`: http://codespeak.net/svn/user/antocuni/hack/rlcompleter_ng.py |
|
30 |
|
31 Configuration |
|
32 ------------- |
|
33 |
|
34 To customize the configuration of Pdb++, you need to put a file named |
|
35 .pdbrc.py in your home directory. The file must contain a class named |
|
36 ``Config`` inheriting from ``DefaultConfig`` and overridding the |
|
37 desired values. |
|
38 |
|
39 To know which options are available, look at the comment in the |
|
40 source. |
|
41 |
|
42 You can find a sample configuration file, here: |
|
43 http://codespeak.net/svn/user/antocuni/hack/pdbrc.py |
|
44 """ |
|
45 |
|
46 __version__='0.1' |
|
47 __author__ ='Antonio Cuni <anto.cuni@gmail.com>' |
|
48 __url__='http://codespeak.net/svn/user/antocuni/hack/pdb.py' |
|
49 |
|
50 import sys |
|
51 import os.path |
|
52 import inspect |
|
53 import code |
|
54 import types |
|
55 from rlcompleter_ng import Completer, ConfigurableClass, setcolor, colors |
|
56 |
|
57 |
|
58 def import_from_stdlib(name): |
|
59 import code # arbitrary module which stays in the same dir as pdb |
|
60 stdlibdir, _ = os.path.split(code.__file__) |
|
61 pyfile = os.path.join(stdlibdir, name + '.py') |
|
62 result = types.ModuleType(name) |
|
63 mydict = execfile(pyfile, result.__dict__) |
|
64 return result |
|
65 |
|
66 pdb = import_from_stdlib('pdb') |
|
67 |
|
68 |
|
69 class DefaultConfig: |
|
70 prompt = '(Pdb++) ' |
|
71 completekey = 'tab' |
|
72 highlight = True |
|
73 bg = 'dark' |
|
74 colorscheme = None |
|
75 |
|
76 line_number_color = colors.turquoise |
|
77 current_line_color = 44 # blue |
|
78 |
|
79 def getsourcelines(obj): |
|
80 try: |
|
81 return inspect.getsourcelines(obj) |
|
82 except IOError: |
|
83 pass |
|
84 |
|
85 if isinstance(obj, types.FrameType): |
|
86 filename = obj.f_code.co_filename |
|
87 if hasattr(filename, '__source__'): |
|
88 first = max(1, obj.f_lineno - 5) |
|
89 lines = [line + '\n' for line in filename.__source__.lines] |
|
90 return lines, first |
|
91 raise IOError('could not get source code') |
|
92 |
|
93 def setbgcolor(line, color): |
|
94 # hack hack hack |
|
95 # add a bgcolor attribute to all escape sequences found |
|
96 import re |
|
97 setbg = '\x1b[%dm' % color |
|
98 regexbg = '\\1;%dm' % color |
|
99 return setbg + re.sub('(\x1b\\[.*?)m', regexbg, line) + '\x1b[00m' |
|
100 |
|
101 CLEARSCREEN = '\033[2J\033[1;1H' |
|
102 |
|
103 def lasti2lineno(code, lasti): |
|
104 import dis |
|
105 linestarts = list(dis.findlinestarts(code)) |
|
106 linestarts.reverse() |
|
107 for i, lineno in linestarts: |
|
108 if lasti >= i: |
|
109 return lineno |
|
110 assert False, 'Invalid instruction number: %s' % lasti |
|
111 |
|
112 class Undefined: |
|
113 def __repr__(self): |
|
114 return '<undefined>' |
|
115 undefined = Undefined() |
|
116 |
|
117 class Pdb(pdb.Pdb, ConfigurableClass): |
|
118 |
|
119 DefaultConfig = DefaultConfig |
|
120 config_filename = '.pdbrc.py' |
|
121 |
|
122 def __init__(self, *args, **kwds): |
|
123 Config = kwds.pop('Config', None) |
|
124 pdb.Pdb.__init__(self, *args, **kwds) |
|
125 self.config = self.get_config(Config) |
|
126 self.prompt = self.config.prompt |
|
127 self.completekey = self.config.completekey |
|
128 |
|
129 self.mycompleter = None |
|
130 self.watching = {} # frame --> (name --> last seen value) |
|
131 self.sticky = False |
|
132 self.sticky_ranges = {} # frame --> (start, end) |
|
133 self.tb_lineno = {} # frame --> lineno where the exception raised |
|
134 |
|
135 def interaction(self, frame, traceback, orig_traceback=None): |
|
136 self.setup(frame, traceback, orig_traceback) |
|
137 self.print_stack_entry(self.stack[self.curindex]) |
|
138 self.cmdloop() |
|
139 self.forget() |
|
140 |
|
141 def setup(self, frame, tb, orig_tb=None): |
|
142 pdb.Pdb.setup(self, frame, tb) |
|
143 tb = orig_tb |
|
144 while tb: |
|
145 lineno = lasti2lineno(tb.tb_frame.f_code, tb.tb_lasti) |
|
146 self.tb_lineno[tb.tb_frame] = lineno |
|
147 tb = tb.tb_next |
|
148 |
|
149 def forget(self): |
|
150 pdb.Pdb.forget(self) |
|
151 self.raise_lineno = {} |
|
152 |
|
153 def complete(self, text, state): |
|
154 if state == 0: |
|
155 mydict = self.curframe.f_globals.copy() |
|
156 mydict.update(self.curframe.f_locals) |
|
157 self.mycompleter = Completer(mydict) |
|
158 return self.mycompleter.complete(text, state) |
|
159 |
|
160 def _init_pygments(self): |
|
161 try: |
|
162 from pygments.lexers import PythonLexer |
|
163 from pygments.formatters import TerminalFormatter |
|
164 except ImportError: |
|
165 return False |
|
166 |
|
167 if hasattr(self, '_fmt'): |
|
168 return True |
|
169 |
|
170 self._fmt = TerminalFormatter(bg=self.config.bg, |
|
171 colorscheme=self.config.colorscheme) |
|
172 self._lexer = PythonLexer() |
|
173 return True |
|
174 |
|
175 def format_source(self, src): |
|
176 if not self._init_pygments(): |
|
177 return src |
|
178 from pygments import highlight, lex |
|
179 return highlight(src, self._lexer, self._fmt) |
|
180 |
|
181 def format_line(self, lineno, marker, line): |
|
182 lineno = '%4d' % lineno |
|
183 if self.config.highlight: |
|
184 lineno = setcolor(lineno, self.config.line_number_color) |
|
185 line = '%s %2s %s' % (lineno, marker, line) |
|
186 if self.config.highlight and marker == '->': |
|
187 line = setbgcolor(line, self.config.current_line_color) |
|
188 return line |
|
189 |
|
190 def parseline(self, line): |
|
191 cmd, arg, newline = pdb.Pdb.parseline(self, line) |
|
192 # don't execute short disruptive commands if a variable with |
|
193 # the name exits in the current contex; this prevents pdb to |
|
194 # quit if you type e.g. 'r[0]' by mystake. |
|
195 if cmd in ['c', 'r', 'q'] and (cmd in self.curframe.f_globals or |
|
196 cmd in self.curframe.f_locals): |
|
197 line = '!' + line |
|
198 return pdb.Pdb.parseline(self, line) |
|
199 return cmd, arg, newline |
|
200 |
|
201 def do_longlist(self, arg): |
|
202 """ |
|
203 {longlist|ll} |
|
204 List source code for the current function. |
|
205 |
|
206 Differently that list, the whole function is displayed; the |
|
207 current line is marked with '->'. In case of post-mortem |
|
208 debugging, the line which effectively raised the exception is |
|
209 marked with '>>'. |
|
210 |
|
211 If the 'highlight' config option is set and pygments is |
|
212 installed, the source code is colorized. |
|
213 """ |
|
214 self.lastcmd = 'longlist' |
|
215 self._printlonglist() |
|
216 |
|
217 def _printlonglist(self, linerange=None): |
|
218 try: |
|
219 lines, lineno = getsourcelines(self.curframe) |
|
220 except IOError, e: |
|
221 print '** Error: %s **' % e |
|
222 return |
|
223 if linerange: |
|
224 start, end = linerange |
|
225 start = max(start, lineno) |
|
226 end = min(end, lineno+len(lines)) |
|
227 lines = lines[start-lineno:end-lineno] |
|
228 lineno = start |
|
229 self._print_lines(lines, lineno) |
|
230 |
|
231 def _print_lines(self, lines, lineno, print_markers=True): |
|
232 exc_lineno = self.tb_lineno.get(self.curframe, None) |
|
233 lines = [line[:-1] for line in lines] # remove the trailing '\n' |
|
234 if self.config.highlight: |
|
235 maxlength = max(map(len, lines)) |
|
236 lines = [line.ljust(maxlength) for line in lines] |
|
237 src = self.format_source('\n'.join(lines)) |
|
238 lines = src.splitlines() |
|
239 for i, line in enumerate(lines): |
|
240 marker = '' |
|
241 if lineno == self.curframe.f_lineno and print_markers: |
|
242 marker = '->' |
|
243 elif lineno == exc_lineno and print_markers: |
|
244 marker = '>>' |
|
245 lines[i] = self.format_line(lineno, marker, line) |
|
246 lineno += 1 |
|
247 print '\n'.join(lines) |
|
248 |
|
249 do_ll = do_longlist |
|
250 |
|
251 def do_list(self, arg): |
|
252 from StringIO import StringIO |
|
253 oldstdout = sys.stdout |
|
254 sys.stdout = StringIO() |
|
255 pdb.Pdb.do_list(self, arg) |
|
256 src = self.format_source(sys.stdout.getvalue()) |
|
257 sys.stdout = oldstdout |
|
258 print src, |
|
259 |
|
260 do_l = do_list |
|
261 |
|
262 def do_interact(self, arg): |
|
263 """ |
|
264 interact |
|
265 |
|
266 Start an interative interpreter whose global namespace |
|
267 contains all the names found in the current scope. |
|
268 """ |
|
269 ns = self.curframe.f_globals.copy() |
|
270 ns.update(self.curframe.f_locals) |
|
271 code.interact("*interactive*", local=ns) |
|
272 |
|
273 def do_track(self, arg): |
|
274 """ |
|
275 track expression |
|
276 |
|
277 Display a graph showing which objects are referred by the |
|
278 value of the expression. This command requires pypy to be in |
|
279 the current PYTHONPATH. |
|
280 """ |
|
281 try: |
|
282 from pypy.translator.tool.reftracker import track |
|
283 except ImportError: |
|
284 print '** cannot import pypy.translator.tool.reftracker **' |
|
285 return |
|
286 val = self._getval(arg) |
|
287 track(val) |
|
288 |
|
289 def _get_watching(self): |
|
290 return self.watching.setdefault(self.curframe, {}) |
|
291 |
|
292 def _getval_or_undefined(self, arg): |
|
293 try: |
|
294 return eval(arg, self.curframe.f_globals, |
|
295 self.curframe.f_locals) |
|
296 except NameError: |
|
297 return undefined |
|
298 |
|
299 def do_watch(self, arg): |
|
300 """ |
|
301 watch expression |
|
302 |
|
303 Add expression to the watching list; expressions in this list |
|
304 are evaluated at each step, and printed every time its value |
|
305 changes. |
|
306 |
|
307 WARNING: since the expressions is evaluated multiple time, pay |
|
308 attention not to put expressions with side-effects in the |
|
309 watching list. |
|
310 """ |
|
311 try: |
|
312 value = self._getval_or_undefined(arg) |
|
313 except: |
|
314 return |
|
315 self._get_watching()[arg] = value |
|
316 |
|
317 def do_unwatch(self, arg): |
|
318 """ |
|
319 unwatch expression |
|
320 |
|
321 Remove expression from the watching list. |
|
322 """ |
|
323 try: |
|
324 del self._get_watching()[arg] |
|
325 except KeyError: |
|
326 print '** not watching %s **' % arg |
|
327 |
|
328 def _print_if_sticky(self): |
|
329 if self.sticky: |
|
330 sys.stdout.write(CLEARSCREEN) |
|
331 frame, lineno = self.stack[self.curindex] |
|
332 filename = self.canonic(frame.f_code.co_filename) |
|
333 s = '> %s(%r)' % (filename, lineno) |
|
334 print s |
|
335 print |
|
336 sticky_range = self.sticky_ranges.get(self.curframe, None) |
|
337 self._printlonglist(sticky_range) |
|
338 |
|
339 def do_sticky(self, arg): |
|
340 """ |
|
341 sticky [start end] |
|
342 |
|
343 Toggle sticky mode. When in sticky mode, it clear the screen |
|
344 and longlist the current functions, making the source |
|
345 appearing always in the same position. Useful to follow the |
|
346 flow control of a function when doing step-by-step execution. |
|
347 |
|
348 If ``start`` and ``end`` are given, sticky mode is enabled and |
|
349 only lines within that range (extremes included) will be |
|
350 displayed. |
|
351 """ |
|
352 if arg: |
|
353 try: |
|
354 start, end = map(int, arg.split()) |
|
355 except ValueError: |
|
356 print '** Error when parsing argument: %s **' % arg |
|
357 return |
|
358 self.sticky = True |
|
359 self.sticky_ranges[self.curframe] = start, end+1 |
|
360 else: |
|
361 self.sticky = not self.sticky |
|
362 self.sticky_range = None |
|
363 self._print_if_sticky() |
|
364 |
|
365 def preloop(self): |
|
366 self._print_if_sticky() |
|
367 watching = self._get_watching() |
|
368 for expr, oldvalue in watching.iteritems(): |
|
369 newvalue = self._getval_or_undefined(expr) |
|
370 # check for identity first; this prevents custom __eq__ to |
|
371 # be called at every loop, and also prevents instances |
|
372 # whose fields are changed to be displayed |
|
373 if newvalue is not oldvalue or newvalue != oldvalue: |
|
374 watching[expr] = newvalue |
|
375 print '%s: %r --> %r' % (expr, oldvalue, newvalue) |
|
376 |
|
377 def do_source(self, arg): |
|
378 try: |
|
379 obj = self._getval(arg) |
|
380 except: |
|
381 return |
|
382 try: |
|
383 lines, lineno = getsourcelines(obj) |
|
384 except (IOError, TypeError), e: |
|
385 print '** Error: %s **' % e |
|
386 return |
|
387 self._print_lines(lines, lineno, print_markers=False) |
|
388 |
|
389 # Simplified interface |
|
390 |
|
391 # copy some functions from pdb.py, but rebind the global dictionary |
|
392 for name in 'run runeval runctx runcall set_trace pm'.split(): |
|
393 func = getattr(pdb, name) |
|
394 newfunc = types.FunctionType(func.func_code, globals(), func.func_name) |
|
395 globals()[name] = newfunc |
|
396 del name, func, newfunc |
|
397 |
|
398 def post_mortem(t, Pdb=Pdb): |
|
399 p = Pdb() |
|
400 p.reset() |
|
401 orig_tb = t |
|
402 while t.tb_next is not None: |
|
403 t = t.tb_next |
|
404 p.interaction(t.tb_frame, t, orig_tb) |
|
405 |
|
406 # pdb++ specific interface |
|
407 |
|
408 def xpm(Pdb=Pdb): |
|
409 """ |
|
410 To be used inside an except clause, enter a post-mortem pdb |
|
411 related to the just catched exception. |
|
412 """ |
|
413 post_mortem(sys.exc_info()[2], Pdb) |