|
1 import os |
|
2 import bdb |
|
3 import types |
|
4 from Tkinter import * |
|
5 from WindowList import ListedToplevel |
|
6 from ScrolledList import ScrolledList |
|
7 import macosxSupport |
|
8 |
|
9 |
|
10 class Idb(bdb.Bdb): |
|
11 |
|
12 def __init__(self, gui): |
|
13 self.gui = gui |
|
14 bdb.Bdb.__init__(self) |
|
15 |
|
16 def user_line(self, frame): |
|
17 if self.in_rpc_code(frame): |
|
18 self.set_step() |
|
19 return |
|
20 message = self.__frame2message(frame) |
|
21 self.gui.interaction(message, frame) |
|
22 |
|
23 def user_exception(self, frame, info): |
|
24 if self.in_rpc_code(frame): |
|
25 self.set_step() |
|
26 return |
|
27 message = self.__frame2message(frame) |
|
28 self.gui.interaction(message, frame, info) |
|
29 |
|
30 def in_rpc_code(self, frame): |
|
31 if frame.f_code.co_filename.count('rpc.py'): |
|
32 return True |
|
33 else: |
|
34 prev_frame = frame.f_back |
|
35 if prev_frame.f_code.co_filename.count('Debugger.py'): |
|
36 # (that test will catch both Debugger.py and RemoteDebugger.py) |
|
37 return False |
|
38 return self.in_rpc_code(prev_frame) |
|
39 |
|
40 def __frame2message(self, frame): |
|
41 code = frame.f_code |
|
42 filename = code.co_filename |
|
43 lineno = frame.f_lineno |
|
44 basename = os.path.basename(filename) |
|
45 message = "%s:%s" % (basename, lineno) |
|
46 if code.co_name != "?": |
|
47 message = "%s: %s()" % (message, code.co_name) |
|
48 return message |
|
49 |
|
50 |
|
51 class Debugger: |
|
52 |
|
53 vstack = vsource = vlocals = vglobals = None |
|
54 |
|
55 def __init__(self, pyshell, idb=None): |
|
56 if idb is None: |
|
57 idb = Idb(self) |
|
58 self.pyshell = pyshell |
|
59 self.idb = idb |
|
60 self.frame = None |
|
61 self.make_gui() |
|
62 self.interacting = 0 |
|
63 |
|
64 def run(self, *args): |
|
65 try: |
|
66 self.interacting = 1 |
|
67 return self.idb.run(*args) |
|
68 finally: |
|
69 self.interacting = 0 |
|
70 |
|
71 def close(self, event=None): |
|
72 if self.interacting: |
|
73 self.top.bell() |
|
74 return |
|
75 if self.stackviewer: |
|
76 self.stackviewer.close(); self.stackviewer = None |
|
77 # Clean up pyshell if user clicked debugger control close widget. |
|
78 # (Causes a harmless extra cycle through close_debugger() if user |
|
79 # toggled debugger from pyshell Debug menu) |
|
80 self.pyshell.close_debugger() |
|
81 # Now close the debugger control window.... |
|
82 self.top.destroy() |
|
83 |
|
84 def make_gui(self): |
|
85 pyshell = self.pyshell |
|
86 self.flist = pyshell.flist |
|
87 self.root = root = pyshell.root |
|
88 self.top = top = ListedToplevel(root) |
|
89 self.top.wm_title("Debug Control") |
|
90 self.top.wm_iconname("Debug") |
|
91 top.wm_protocol("WM_DELETE_WINDOW", self.close) |
|
92 self.top.bind("<Escape>", self.close) |
|
93 # |
|
94 self.bframe = bframe = Frame(top) |
|
95 self.bframe.pack(anchor="w") |
|
96 self.buttons = bl = [] |
|
97 # |
|
98 self.bcont = b = Button(bframe, text="Go", command=self.cont) |
|
99 bl.append(b) |
|
100 self.bstep = b = Button(bframe, text="Step", command=self.step) |
|
101 bl.append(b) |
|
102 self.bnext = b = Button(bframe, text="Over", command=self.next) |
|
103 bl.append(b) |
|
104 self.bret = b = Button(bframe, text="Out", command=self.ret) |
|
105 bl.append(b) |
|
106 self.bret = b = Button(bframe, text="Quit", command=self.quit) |
|
107 bl.append(b) |
|
108 # |
|
109 for b in bl: |
|
110 b.configure(state="disabled") |
|
111 b.pack(side="left") |
|
112 # |
|
113 self.cframe = cframe = Frame(bframe) |
|
114 self.cframe.pack(side="left") |
|
115 # |
|
116 if not self.vstack: |
|
117 self.__class__.vstack = BooleanVar(top) |
|
118 self.vstack.set(1) |
|
119 self.bstack = Checkbutton(cframe, |
|
120 text="Stack", command=self.show_stack, variable=self.vstack) |
|
121 self.bstack.grid(row=0, column=0) |
|
122 if not self.vsource: |
|
123 self.__class__.vsource = BooleanVar(top) |
|
124 self.bsource = Checkbutton(cframe, |
|
125 text="Source", command=self.show_source, variable=self.vsource) |
|
126 self.bsource.grid(row=0, column=1) |
|
127 if not self.vlocals: |
|
128 self.__class__.vlocals = BooleanVar(top) |
|
129 self.vlocals.set(1) |
|
130 self.blocals = Checkbutton(cframe, |
|
131 text="Locals", command=self.show_locals, variable=self.vlocals) |
|
132 self.blocals.grid(row=1, column=0) |
|
133 if not self.vglobals: |
|
134 self.__class__.vglobals = BooleanVar(top) |
|
135 self.bglobals = Checkbutton(cframe, |
|
136 text="Globals", command=self.show_globals, variable=self.vglobals) |
|
137 self.bglobals.grid(row=1, column=1) |
|
138 # |
|
139 self.status = Label(top, anchor="w") |
|
140 self.status.pack(anchor="w") |
|
141 self.error = Label(top, anchor="w") |
|
142 self.error.pack(anchor="w", fill="x") |
|
143 self.errorbg = self.error.cget("background") |
|
144 # |
|
145 self.fstack = Frame(top, height=1) |
|
146 self.fstack.pack(expand=1, fill="both") |
|
147 self.flocals = Frame(top) |
|
148 self.flocals.pack(expand=1, fill="both") |
|
149 self.fglobals = Frame(top, height=1) |
|
150 self.fglobals.pack(expand=1, fill="both") |
|
151 # |
|
152 if self.vstack.get(): |
|
153 self.show_stack() |
|
154 if self.vlocals.get(): |
|
155 self.show_locals() |
|
156 if self.vglobals.get(): |
|
157 self.show_globals() |
|
158 |
|
159 def interaction(self, message, frame, info=None): |
|
160 self.frame = frame |
|
161 self.status.configure(text=message) |
|
162 # |
|
163 if info: |
|
164 type, value, tb = info |
|
165 try: |
|
166 m1 = type.__name__ |
|
167 except AttributeError: |
|
168 m1 = "%s" % str(type) |
|
169 if value is not None: |
|
170 try: |
|
171 m1 = "%s: %s" % (m1, str(value)) |
|
172 except: |
|
173 pass |
|
174 bg = "yellow" |
|
175 else: |
|
176 m1 = "" |
|
177 tb = None |
|
178 bg = self.errorbg |
|
179 self.error.configure(text=m1, background=bg) |
|
180 # |
|
181 sv = self.stackviewer |
|
182 if sv: |
|
183 stack, i = self.idb.get_stack(self.frame, tb) |
|
184 sv.load_stack(stack, i) |
|
185 # |
|
186 self.show_variables(1) |
|
187 # |
|
188 if self.vsource.get(): |
|
189 self.sync_source_line() |
|
190 # |
|
191 for b in self.buttons: |
|
192 b.configure(state="normal") |
|
193 # |
|
194 self.top.wakeup() |
|
195 self.root.mainloop() |
|
196 # |
|
197 for b in self.buttons: |
|
198 b.configure(state="disabled") |
|
199 self.status.configure(text="") |
|
200 self.error.configure(text="", background=self.errorbg) |
|
201 self.frame = None |
|
202 |
|
203 def sync_source_line(self): |
|
204 frame = self.frame |
|
205 if not frame: |
|
206 return |
|
207 filename, lineno = self.__frame2fileline(frame) |
|
208 if filename[:1] + filename[-1:] != "<>" and os.path.exists(filename): |
|
209 self.flist.gotofileline(filename, lineno) |
|
210 |
|
211 def __frame2fileline(self, frame): |
|
212 code = frame.f_code |
|
213 filename = code.co_filename |
|
214 lineno = frame.f_lineno |
|
215 return filename, lineno |
|
216 |
|
217 def cont(self): |
|
218 self.idb.set_continue() |
|
219 self.root.quit() |
|
220 |
|
221 def step(self): |
|
222 self.idb.set_step() |
|
223 self.root.quit() |
|
224 |
|
225 def next(self): |
|
226 self.idb.set_next(self.frame) |
|
227 self.root.quit() |
|
228 |
|
229 def ret(self): |
|
230 self.idb.set_return(self.frame) |
|
231 self.root.quit() |
|
232 |
|
233 def quit(self): |
|
234 self.idb.set_quit() |
|
235 self.root.quit() |
|
236 |
|
237 stackviewer = None |
|
238 |
|
239 def show_stack(self): |
|
240 if not self.stackviewer and self.vstack.get(): |
|
241 self.stackviewer = sv = StackViewer(self.fstack, self.flist, self) |
|
242 if self.frame: |
|
243 stack, i = self.idb.get_stack(self.frame, None) |
|
244 sv.load_stack(stack, i) |
|
245 else: |
|
246 sv = self.stackviewer |
|
247 if sv and not self.vstack.get(): |
|
248 self.stackviewer = None |
|
249 sv.close() |
|
250 self.fstack['height'] = 1 |
|
251 |
|
252 def show_source(self): |
|
253 if self.vsource.get(): |
|
254 self.sync_source_line() |
|
255 |
|
256 def show_frame(self, (frame, lineno)): |
|
257 self.frame = frame |
|
258 self.show_variables() |
|
259 |
|
260 localsviewer = None |
|
261 globalsviewer = None |
|
262 |
|
263 def show_locals(self): |
|
264 lv = self.localsviewer |
|
265 if self.vlocals.get(): |
|
266 if not lv: |
|
267 self.localsviewer = NamespaceViewer(self.flocals, "Locals") |
|
268 else: |
|
269 if lv: |
|
270 self.localsviewer = None |
|
271 lv.close() |
|
272 self.flocals['height'] = 1 |
|
273 self.show_variables() |
|
274 |
|
275 def show_globals(self): |
|
276 gv = self.globalsviewer |
|
277 if self.vglobals.get(): |
|
278 if not gv: |
|
279 self.globalsviewer = NamespaceViewer(self.fglobals, "Globals") |
|
280 else: |
|
281 if gv: |
|
282 self.globalsviewer = None |
|
283 gv.close() |
|
284 self.fglobals['height'] = 1 |
|
285 self.show_variables() |
|
286 |
|
287 def show_variables(self, force=0): |
|
288 lv = self.localsviewer |
|
289 gv = self.globalsviewer |
|
290 frame = self.frame |
|
291 if not frame: |
|
292 ldict = gdict = None |
|
293 else: |
|
294 ldict = frame.f_locals |
|
295 gdict = frame.f_globals |
|
296 if lv and gv and ldict is gdict: |
|
297 ldict = None |
|
298 if lv: |
|
299 lv.load_dict(ldict, force, self.pyshell.interp.rpcclt) |
|
300 if gv: |
|
301 gv.load_dict(gdict, force, self.pyshell.interp.rpcclt) |
|
302 |
|
303 def set_breakpoint_here(self, filename, lineno): |
|
304 self.idb.set_break(filename, lineno) |
|
305 |
|
306 def clear_breakpoint_here(self, filename, lineno): |
|
307 self.idb.clear_break(filename, lineno) |
|
308 |
|
309 def clear_file_breaks(self, filename): |
|
310 self.idb.clear_all_file_breaks(filename) |
|
311 |
|
312 def load_breakpoints(self): |
|
313 "Load PyShellEditorWindow breakpoints into subprocess debugger" |
|
314 pyshell_edit_windows = self.pyshell.flist.inversedict.keys() |
|
315 for editwin in pyshell_edit_windows: |
|
316 filename = editwin.io.filename |
|
317 try: |
|
318 for lineno in editwin.breakpoints: |
|
319 self.set_breakpoint_here(filename, lineno) |
|
320 except AttributeError: |
|
321 continue |
|
322 |
|
323 class StackViewer(ScrolledList): |
|
324 |
|
325 def __init__(self, master, flist, gui): |
|
326 if macosxSupport.runningAsOSXApp(): |
|
327 # At least on with the stock AquaTk version on OSX 10.4 you'll |
|
328 # get an shaking GUI that eventually kills IDLE if the width |
|
329 # argument is specified. |
|
330 ScrolledList.__init__(self, master) |
|
331 else: |
|
332 ScrolledList.__init__(self, master, width=80) |
|
333 self.flist = flist |
|
334 self.gui = gui |
|
335 self.stack = [] |
|
336 |
|
337 def load_stack(self, stack, index=None): |
|
338 self.stack = stack |
|
339 self.clear() |
|
340 for i in range(len(stack)): |
|
341 frame, lineno = stack[i] |
|
342 try: |
|
343 modname = frame.f_globals["__name__"] |
|
344 except: |
|
345 modname = "?" |
|
346 code = frame.f_code |
|
347 filename = code.co_filename |
|
348 funcname = code.co_name |
|
349 import linecache |
|
350 sourceline = linecache.getline(filename, lineno) |
|
351 import string |
|
352 sourceline = string.strip(sourceline) |
|
353 if funcname in ("?", "", None): |
|
354 item = "%s, line %d: %s" % (modname, lineno, sourceline) |
|
355 else: |
|
356 item = "%s.%s(), line %d: %s" % (modname, funcname, |
|
357 lineno, sourceline) |
|
358 if i == index: |
|
359 item = "> " + item |
|
360 self.append(item) |
|
361 if index is not None: |
|
362 self.select(index) |
|
363 |
|
364 def popup_event(self, event): |
|
365 "override base method" |
|
366 if self.stack: |
|
367 return ScrolledList.popup_event(self, event) |
|
368 |
|
369 def fill_menu(self): |
|
370 "override base method" |
|
371 menu = self.menu |
|
372 menu.add_command(label="Go to source line", |
|
373 command=self.goto_source_line) |
|
374 menu.add_command(label="Show stack frame", |
|
375 command=self.show_stack_frame) |
|
376 |
|
377 def on_select(self, index): |
|
378 "override base method" |
|
379 if 0 <= index < len(self.stack): |
|
380 self.gui.show_frame(self.stack[index]) |
|
381 |
|
382 def on_double(self, index): |
|
383 "override base method" |
|
384 self.show_source(index) |
|
385 |
|
386 def goto_source_line(self): |
|
387 index = self.listbox.index("active") |
|
388 self.show_source(index) |
|
389 |
|
390 def show_stack_frame(self): |
|
391 index = self.listbox.index("active") |
|
392 if 0 <= index < len(self.stack): |
|
393 self.gui.show_frame(self.stack[index]) |
|
394 |
|
395 def show_source(self, index): |
|
396 if not (0 <= index < len(self.stack)): |
|
397 return |
|
398 frame, lineno = self.stack[index] |
|
399 code = frame.f_code |
|
400 filename = code.co_filename |
|
401 if os.path.isfile(filename): |
|
402 edit = self.flist.open(filename) |
|
403 if edit: |
|
404 edit.gotoline(lineno) |
|
405 |
|
406 |
|
407 class NamespaceViewer: |
|
408 |
|
409 def __init__(self, master, title, dict=None): |
|
410 width = 0 |
|
411 height = 40 |
|
412 if dict: |
|
413 height = 20*len(dict) # XXX 20 == observed height of Entry widget |
|
414 self.master = master |
|
415 self.title = title |
|
416 import repr |
|
417 self.repr = repr.Repr() |
|
418 self.repr.maxstring = 60 |
|
419 self.repr.maxother = 60 |
|
420 self.frame = frame = Frame(master) |
|
421 self.frame.pack(expand=1, fill="both") |
|
422 self.label = Label(frame, text=title, borderwidth=2, relief="groove") |
|
423 self.label.pack(fill="x") |
|
424 self.vbar = vbar = Scrollbar(frame, name="vbar") |
|
425 vbar.pack(side="right", fill="y") |
|
426 self.canvas = canvas = Canvas(frame, |
|
427 height=min(300, max(40, height)), |
|
428 scrollregion=(0, 0, width, height)) |
|
429 canvas.pack(side="left", fill="both", expand=1) |
|
430 vbar["command"] = canvas.yview |
|
431 canvas["yscrollcommand"] = vbar.set |
|
432 self.subframe = subframe = Frame(canvas) |
|
433 self.sfid = canvas.create_window(0, 0, window=subframe, anchor="nw") |
|
434 self.load_dict(dict) |
|
435 |
|
436 dict = -1 |
|
437 |
|
438 def load_dict(self, dict, force=0, rpc_client=None): |
|
439 if dict is self.dict and not force: |
|
440 return |
|
441 subframe = self.subframe |
|
442 frame = self.frame |
|
443 for c in subframe.children.values(): |
|
444 c.destroy() |
|
445 self.dict = None |
|
446 if not dict: |
|
447 l = Label(subframe, text="None") |
|
448 l.grid(row=0, column=0) |
|
449 else: |
|
450 names = dict.keys() |
|
451 names.sort() |
|
452 row = 0 |
|
453 for name in names: |
|
454 value = dict[name] |
|
455 svalue = self.repr.repr(value) # repr(value) |
|
456 # Strip extra quotes caused by calling repr on the (already) |
|
457 # repr'd value sent across the RPC interface: |
|
458 if rpc_client: |
|
459 svalue = svalue[1:-1] |
|
460 l = Label(subframe, text=name) |
|
461 l.grid(row=row, column=0, sticky="nw") |
|
462 l = Entry(subframe, width=0, borderwidth=0) |
|
463 l.insert(0, svalue) |
|
464 l.grid(row=row, column=1, sticky="nw") |
|
465 row = row+1 |
|
466 self.dict = dict |
|
467 # XXX Could we use a <Configure> callback for the following? |
|
468 subframe.update_idletasks() # Alas! |
|
469 width = subframe.winfo_reqwidth() |
|
470 height = subframe.winfo_reqheight() |
|
471 canvas = self.canvas |
|
472 self.canvas["scrollregion"] = (0, 0, width, height) |
|
473 if height > 300: |
|
474 canvas["height"] = 300 |
|
475 frame.pack(expand=1) |
|
476 else: |
|
477 canvas["height"] = height |
|
478 frame.pack(expand=0) |
|
479 |
|
480 def close(self): |
|
481 self.frame.destroy() |