debugger.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  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 = f"{basename}:{lineno}"
  44. if code.co_name != "?":
  45. message = f"{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. # TODO redo entire section, tries not needed.
  200. m1 = f"{m1}: {value}"
  201. except:
  202. pass
  203. bg = "yellow"
  204. else:
  205. m1 = ""
  206. tb = None
  207. bg = self.errorbg
  208. self.error.configure(text=m1, background=bg)
  209. #
  210. sv = self.stackviewer
  211. if sv:
  212. stack, i = self.idb.get_stack(self.frame, tb)
  213. sv.load_stack(stack, i)
  214. #
  215. self.show_variables(1)
  216. #
  217. if self.vsource.get():
  218. self.sync_source_line()
  219. #
  220. for b in self.buttons:
  221. b.configure(state="normal")
  222. #
  223. self.top.wakeup()
  224. # Nested main loop: Tkinter's main loop is not reentrant, so use
  225. # Tcl's vwait facility, which reenters the event loop until an
  226. # event handler sets the variable we're waiting on
  227. self.nesting_level += 1
  228. self.root.tk.call('vwait', '::idledebugwait')
  229. self.nesting_level -= 1
  230. #
  231. for b in self.buttons:
  232. b.configure(state="disabled")
  233. self.status.configure(text="")
  234. self.error.configure(text="", background=self.errorbg)
  235. self.frame = None
  236. def sync_source_line(self):
  237. frame = self.frame
  238. if not frame:
  239. return
  240. filename, lineno = self.__frame2fileline(frame)
  241. if filename[:1] + filename[-1:] != "<>" and os.path.exists(filename):
  242. self.flist.gotofileline(filename, lineno)
  243. def __frame2fileline(self, frame):
  244. code = frame.f_code
  245. filename = code.co_filename
  246. lineno = frame.f_lineno
  247. return filename, lineno
  248. def cont(self):
  249. self.idb.set_continue()
  250. self.abort_loop()
  251. def step(self):
  252. self.idb.set_step()
  253. self.abort_loop()
  254. def next(self):
  255. self.idb.set_next(self.frame)
  256. self.abort_loop()
  257. def ret(self):
  258. self.idb.set_return(self.frame)
  259. self.abort_loop()
  260. def quit(self):
  261. self.idb.set_quit()
  262. self.abort_loop()
  263. def abort_loop(self):
  264. self.root.tk.call('set', '::idledebugwait', '1')
  265. stackviewer = None
  266. def show_stack(self):
  267. if not self.stackviewer and self.vstack.get():
  268. self.stackviewer = sv = StackViewer(self.fstack, self.flist, self)
  269. if self.frame:
  270. stack, i = self.idb.get_stack(self.frame, None)
  271. sv.load_stack(stack, i)
  272. else:
  273. sv = self.stackviewer
  274. if sv and not self.vstack.get():
  275. self.stackviewer = None
  276. sv.close()
  277. self.fstack['height'] = 1
  278. def show_source(self):
  279. if self.vsource.get():
  280. self.sync_source_line()
  281. def show_frame(self, stackitem):
  282. self.frame = stackitem[0] # lineno is stackitem[1]
  283. self.show_variables()
  284. localsviewer = None
  285. globalsviewer = None
  286. def show_locals(self):
  287. lv = self.localsviewer
  288. if self.vlocals.get():
  289. if not lv:
  290. self.localsviewer = NamespaceViewer(self.flocals, "Locals")
  291. else:
  292. if lv:
  293. self.localsviewer = None
  294. lv.close()
  295. self.flocals['height'] = 1
  296. self.show_variables()
  297. def show_globals(self):
  298. gv = self.globalsviewer
  299. if self.vglobals.get():
  300. if not gv:
  301. self.globalsviewer = NamespaceViewer(self.fglobals, "Globals")
  302. else:
  303. if gv:
  304. self.globalsviewer = None
  305. gv.close()
  306. self.fglobals['height'] = 1
  307. self.show_variables()
  308. def show_variables(self, force=0):
  309. lv = self.localsviewer
  310. gv = self.globalsviewer
  311. frame = self.frame
  312. if not frame:
  313. ldict = gdict = None
  314. else:
  315. ldict = frame.f_locals
  316. gdict = frame.f_globals
  317. if lv and gv and ldict is gdict:
  318. ldict = None
  319. if lv:
  320. lv.load_dict(ldict, force, self.pyshell.interp.rpcclt)
  321. if gv:
  322. gv.load_dict(gdict, force, self.pyshell.interp.rpcclt)
  323. def set_breakpoint_here(self, filename, lineno):
  324. self.idb.set_break(filename, lineno)
  325. def clear_breakpoint_here(self, filename, lineno):
  326. self.idb.clear_break(filename, lineno)
  327. def clear_file_breaks(self, filename):
  328. self.idb.clear_all_file_breaks(filename)
  329. def load_breakpoints(self):
  330. "Load PyShellEditorWindow breakpoints into subprocess debugger"
  331. for editwin in self.pyshell.flist.inversedict:
  332. filename = editwin.io.filename
  333. try:
  334. for lineno in editwin.breakpoints:
  335. self.set_breakpoint_here(filename, lineno)
  336. except AttributeError:
  337. continue
  338. class StackViewer(ScrolledList):
  339. def __init__(self, master, flist, gui):
  340. if macosx.isAquaTk():
  341. # At least on with the stock AquaTk version on OSX 10.4 you'll
  342. # get a shaking GUI that eventually kills IDLE if the width
  343. # argument is specified.
  344. ScrolledList.__init__(self, master)
  345. else:
  346. ScrolledList.__init__(self, master, width=80)
  347. self.flist = flist
  348. self.gui = gui
  349. self.stack = []
  350. def load_stack(self, stack, index=None):
  351. self.stack = stack
  352. self.clear()
  353. for i in range(len(stack)):
  354. frame, lineno = stack[i]
  355. try:
  356. modname = frame.f_globals["__name__"]
  357. except:
  358. modname = "?"
  359. code = frame.f_code
  360. filename = code.co_filename
  361. funcname = code.co_name
  362. import linecache
  363. sourceline = linecache.getline(filename, lineno)
  364. sourceline = sourceline.strip()
  365. if funcname in ("?", "", None):
  366. item = "%s, line %d: %s" % (modname, lineno, sourceline)
  367. else:
  368. item = "%s.%s(), line %d: %s" % (modname, funcname,
  369. lineno, sourceline)
  370. if i == index:
  371. item = "> " + item
  372. self.append(item)
  373. if index is not None:
  374. self.select(index)
  375. def popup_event(self, event):
  376. "override base method"
  377. if self.stack:
  378. return ScrolledList.popup_event(self, event)
  379. def fill_menu(self):
  380. "override base method"
  381. menu = self.menu
  382. menu.add_command(label="Go to source line",
  383. command=self.goto_source_line)
  384. menu.add_command(label="Show stack frame",
  385. command=self.show_stack_frame)
  386. def on_select(self, index):
  387. "override base method"
  388. if 0 <= index < len(self.stack):
  389. self.gui.show_frame(self.stack[index])
  390. def on_double(self, index):
  391. "override base method"
  392. self.show_source(index)
  393. def goto_source_line(self):
  394. index = self.listbox.index("active")
  395. self.show_source(index)
  396. def show_stack_frame(self):
  397. index = self.listbox.index("active")
  398. if 0 <= index < len(self.stack):
  399. self.gui.show_frame(self.stack[index])
  400. def show_source(self, index):
  401. if not (0 <= index < len(self.stack)):
  402. return
  403. frame, lineno = self.stack[index]
  404. code = frame.f_code
  405. filename = code.co_filename
  406. if os.path.isfile(filename):
  407. edit = self.flist.open(filename)
  408. if edit:
  409. edit.gotoline(lineno)
  410. class NamespaceViewer:
  411. def __init__(self, master, title, dict=None):
  412. width = 0
  413. height = 40
  414. if dict:
  415. height = 20*len(dict) # XXX 20 == observed height of Entry widget
  416. self.master = master
  417. self.title = title
  418. import reprlib
  419. self.repr = reprlib.Repr()
  420. self.repr.maxstring = 60
  421. self.repr.maxother = 60
  422. self.frame = frame = Frame(master)
  423. self.frame.pack(expand=1, fill="both")
  424. self.label = Label(frame, text=title, borderwidth=2, relief="groove")
  425. self.label.pack(fill="x")
  426. self.vbar = vbar = Scrollbar(frame, name="vbar")
  427. vbar.pack(side="right", fill="y")
  428. self.canvas = canvas = Canvas(frame,
  429. height=min(300, max(40, height)),
  430. scrollregion=(0, 0, width, height))
  431. canvas.pack(side="left", fill="both", expand=1)
  432. vbar["command"] = canvas.yview
  433. canvas["yscrollcommand"] = vbar.set
  434. self.subframe = subframe = Frame(canvas)
  435. self.sfid = canvas.create_window(0, 0, window=subframe, anchor="nw")
  436. self.load_dict(dict)
  437. dict = -1
  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 list(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 = sorted(dict)
  451. ###
  452. # Because of (temporary) limitations on the dict_keys type (not yet
  453. # public or pickleable), have the subprocess to send a list of
  454. # keys, not a dict_keys object. sorted() will take a dict_keys
  455. # (no subprocess) or a list.
  456. #
  457. # There is also an obscure bug in sorted(dict) where the
  458. # interpreter gets into a loop requesting non-existing dict[0],
  459. # dict[1], dict[2], etc from the debugger_r.DictProxy.
  460. ###
  461. keys_list = dict.keys()
  462. names = sorted(keys_list)
  463. ###
  464. row = 0
  465. for name in names:
  466. value = dict[name]
  467. svalue = self.repr.repr(value) # repr(value)
  468. # Strip extra quotes caused by calling repr on the (already)
  469. # repr'd value sent across the RPC interface:
  470. if rpc_client:
  471. svalue = svalue[1:-1]
  472. l = Label(subframe, text=name)
  473. l.grid(row=row, column=0, sticky="nw")
  474. l = Entry(subframe, width=0, borderwidth=0)
  475. l.insert(0, svalue)
  476. l.grid(row=row, column=1, sticky="nw")
  477. row = row+1
  478. self.dict = dict
  479. # XXX Could we use a <Configure> callback for the following?
  480. subframe.update_idletasks() # Alas!
  481. width = subframe.winfo_reqwidth()
  482. height = subframe.winfo_reqheight()
  483. canvas = self.canvas
  484. self.canvas["scrollregion"] = (0, 0, width, height)
  485. if height > 300:
  486. canvas["height"] = 300
  487. frame.pack(expand=1)
  488. else:
  489. canvas["height"] = height
  490. frame.pack(expand=0)
  491. def close(self):
  492. self.frame.destroy()
  493. if __name__ == "__main__":
  494. from unittest import main
  495. main('idlelib.idle_test.test_debugger', verbosity=2, exit=False)
  496. # TODO: htest?