debugger.py 19 KB

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