From ab7d734ca2e5778d7f60e566f32a4ee5893f25ac Mon Sep 17 00:00:00 2001 From: Mark Roseman Date: Sat, 24 Oct 2020 13:36:34 -0700 Subject: [PATCH 01/12] bpo-17942: reworking of debugger UI - updated to use ttk widgets throughout - simplified controls, changed go/step/etc. to image buttons - overall layout is a two-pane horizontal window (controls, stack, status on left, variables on right) - treeview widget used to replace custom scrolled-listbox stackviewer and custom canvas-based namespaceviewer - integrated locals/globals into a single treeview on right; can collapse/expand each independently instead of having checkboxes to control whether shown or not --- Lib/idlelib/Icons/debug_current.gif | Bin 0 -> 298 bytes Lib/idlelib/Icons/debug_go.gif | Bin 0 -> 124 bytes Lib/idlelib/Icons/debug_go_disabled.gif | Bin 0 -> 76 bytes Lib/idlelib/Icons/debug_line.gif | Bin 0 -> 64 bytes Lib/idlelib/Icons/debug_out.gif | Bin 0 -> 97 bytes Lib/idlelib/Icons/debug_out_disabled.gif | Bin 0 -> 93 bytes Lib/idlelib/Icons/debug_over.gif | Bin 0 -> 94 bytes Lib/idlelib/Icons/debug_over_disabled.gif | Bin 0 -> 91 bytes Lib/idlelib/Icons/debug_prefs.gif | Bin 0 -> 379 bytes Lib/idlelib/Icons/debug_prefs_disabled.gif | Bin 0 -> 189 bytes Lib/idlelib/Icons/debug_step.gif | Bin 0 -> 94 bytes Lib/idlelib/Icons/debug_step_disabled.gif | Bin 0 -> 91 bytes Lib/idlelib/Icons/debug_stop.gif | Bin 0 -> 106 bytes Lib/idlelib/Icons/debug_stop_disabled.gif | Bin 0 -> 77 bytes Lib/idlelib/debugger.py | 612 ++++++++++----------- Lib/idlelib/filelist.py | 8 + Lib/idlelib/idle_test/test_debugger.py | 2 - Lib/idlelib/pyshell.py | 7 + 18 files changed, 295 insertions(+), 334 deletions(-) create mode 100644 Lib/idlelib/Icons/debug_current.gif create mode 100644 Lib/idlelib/Icons/debug_go.gif create mode 100644 Lib/idlelib/Icons/debug_go_disabled.gif create mode 100644 Lib/idlelib/Icons/debug_line.gif create mode 100644 Lib/idlelib/Icons/debug_out.gif create mode 100644 Lib/idlelib/Icons/debug_out_disabled.gif create mode 100644 Lib/idlelib/Icons/debug_over.gif create mode 100644 Lib/idlelib/Icons/debug_over_disabled.gif create mode 100644 Lib/idlelib/Icons/debug_prefs.gif create mode 100644 Lib/idlelib/Icons/debug_prefs_disabled.gif create mode 100644 Lib/idlelib/Icons/debug_step.gif create mode 100644 Lib/idlelib/Icons/debug_step_disabled.gif create mode 100644 Lib/idlelib/Icons/debug_stop.gif create mode 100644 Lib/idlelib/Icons/debug_stop_disabled.gif diff --git a/Lib/idlelib/Icons/debug_current.gif b/Lib/idlelib/Icons/debug_current.gif new file mode 100644 index 0000000000000000000000000000000000000000..226157b0557b01d59735902a5ea9ca9cfa6e611c GIT binary patch literal 298 zcmZ?wbh9u|6kyc$O3G8x-+$!ljDw%>B*}OUh{Cb6g21P<<6{41v;-JQKZb&m7Y&NZm7}X5H?}@RbeX2xo}BMjfvzL*3Fq5Q+WKB hA5QFc_HuocP>`d*subYC!pExI?7_^a(&@-x4FJHyR6qa# literal 0 HcmV?d00001 diff --git a/Lib/idlelib/Icons/debug_go.gif b/Lib/idlelib/Icons/debug_go.gif new file mode 100644 index 0000000000000000000000000000000000000000..f56ca55670c4e8747fb1f502ac207bba1b413915 GIT binary patch literal 124 zcmZ?wbh9u|6krfw*vtR|TwGiN0s;~e5_)=i*4EZePEMhrp=oJp|Ns97%cBsAKUp|| zS{QUdq98LESTqApdamAE_CVU<#uSDgd5yj&Ws6pB-Sqa$p`|>>71u5@z4ld_H^W25 Ije)@$03;6}Jpcdz literal 0 HcmV?d00001 diff --git a/Lib/idlelib/Icons/debug_go_disabled.gif b/Lib/idlelib/Icons/debug_go_disabled.gif new file mode 100644 index 0000000000000000000000000000000000000000..4fcdc211213f664f6fa8ba96f24e89e8ea62b501 GIT binary patch literal 76 zcmZ?wbh9u|6krfwn8*MEt5&W0|NlRbq4<-9lYxPmK?fuXl4oF&ozuT^^+D-J&Ku7K aem*Vp$*(p_qiTVs@3q#A?^cU2SOWke78un4 literal 0 HcmV?d00001 diff --git a/Lib/idlelib/Icons/debug_line.gif b/Lib/idlelib/Icons/debug_line.gif new file mode 100644 index 0000000000000000000000000000000000000000..4bed66bb4649b9661403ed9bf6b4ae352babf4a6 GIT binary patch literal 64 zcmZ?wbh9u|6ky6&R{hw MgStJx0~r{s0UnqU#sB~S literal 0 HcmV?d00001 diff --git a/Lib/idlelib/Icons/debug_out.gif b/Lib/idlelib/Icons/debug_out.gif new file mode 100644 index 0000000000000000000000000000000000000000..dfc44540ba9622a6c062dc3bd28f2b9ad19354d4 GIT binary patch literal 97 zcmZ?wbhEHb6krfwn8?7OY1Q)o|9=Js1|X^UlZBCiftf)E$OXy@FfcF~&FNoxdM=Y9 z)0ZO>0XNv*-LK$gjd0r5(5!B8SJQI3OW27ywRbjlzUx{wRYaqC-4AOK1_o;Y;aVb4 literal 0 HcmV?d00001 diff --git a/Lib/idlelib/Icons/debug_out_disabled.gif b/Lib/idlelib/Icons/debug_out_disabled.gif new file mode 100644 index 0000000000000000000000000000000000000000..630116dd2d94716afab78a9212235dcf61f8e70d GIT binary patch literal 93 zcmZ?wbh9u|6krfwn8*MEt5&W0|NlRbq4<-9lYxPmK?fuXl4oGjo72DY^jt>8E_JuY r7PElg*K|e16Hc3#G&*xlO7{GioOrJ+Lvi-3%}GXgn)Wy|GFSruuEZUS literal 0 HcmV?d00001 diff --git a/Lib/idlelib/Icons/debug_over.gif b/Lib/idlelib/Icons/debug_over.gif new file mode 100644 index 0000000000000000000000000000000000000000..4bb4f3931dda4141643fdb11ed88c353d60a7ed2 GIT binary patch literal 94 zcmZ?wbhEHb6krfwn8?7OY1Q)o|9=Js1|X^UlZBCiftf)E$OXy@FfcIb&*|?xwCH9; wg?pQKug!wrsy@eEyADqaReWCh;0cfFvb8=^rWJX1;Kf|WI@t}c0-F@}$<^TWx!8TKYQ2fcl$-tn&paU`s z>7%)!W`^uOd3pV z>>MJpiq2f@OIVm(H*$Hfu&#^_S;Fe>B;LU*5-rTzE$$&Bs3;ySsw61m*;3~wcI~RO K+TGiZ4AuZ9d~EXo literal 0 HcmV?d00001 diff --git a/Lib/idlelib/Icons/debug_prefs_disabled.gif b/Lib/idlelib/Icons/debug_prefs_disabled.gif new file mode 100644 index 0000000000000000000000000000000000000000..bc67734cfc626e00901efa0aac4813b4ba994e9f GIT binary patch literal 189 zcmZ?wbh9u|6krfw*vtR|HMKP@tu5^x?LECclO|7^J!kg31@o3HTeAP)zO(1gUcG+x z`mO8#|NjT8Wl;Rd!pXqE$Djk^g3MrG$+~b-(q+aBuON=1aJ3%yM7hNcrE1rU?p#qw z%T?xGHlxCLc31O~N1rTfbi17$8lI}~N;sNkm~l_;D=o;;;b2Z(ddk3ytGOUS*CSbT kaltBEF4^U5Zdr*5b_+S}H0>HC-JQiO>)T?xg#{U`0e1FAF#rGn literal 0 HcmV?d00001 diff --git a/Lib/idlelib/Icons/debug_step.gif b/Lib/idlelib/Icons/debug_step.gif new file mode 100644 index 0000000000000000000000000000000000000000..ba66aced21a2b1e21e1b5b3cdc64bd46a4765542 GIT binary patch literal 94 zcmZ?wbhEHb6krfwn8?7OY1Q)o|9=Js1|X^UlZBCiftf)E$OXy@FfcIb&*@)znw@{L wfX0pPw2a6%jIMXhV{RjcIZXPJd#I5_5?n8=-xwPMk_w;yMH)VZb|{j6|WHMeJKMWaN29}9yu E0B|HAoB#j- literal 0 HcmV?d00001 diff --git a/Lib/idlelib/Icons/debug_stop_disabled.gif b/Lib/idlelib/Icons/debug_stop_disabled.gif new file mode 100644 index 0000000000000000000000000000000000000000..8448592fcc98af73bf08064795cb606096eae572 GIT binary patch literal 77 zcmZ?wbh9u|6krfwn8*MEt5&W0|NlRbq4<-9lYxPmK?fuXl4oF&o72B?^+SHyE|W7{ a`qR?w_cqRmm^ejpVdV43Gu+y&4AuY)lNMY6 literal 0 HcmV?d00001 diff --git a/Lib/idlelib/debugger.py b/Lib/idlelib/debugger.py index ccd03e46e16147..acfa6e85f54c3e 100644 --- a/Lib/idlelib/debugger.py +++ b/Lib/idlelib/debugger.py @@ -1,14 +1,22 @@ import bdb import os +import linecache from tkinter import * from tkinter.ttk import Frame, Scrollbar +from tkinter import ttk +from tkinter import PhotoImage +from tkinter.font import Font +from tkinter.ttk import Scrollbar from idlelib import macosx -from idlelib.scrolledlist import ScrolledList from idlelib.window import ListedToplevel +def underscore_at_end(s): + return s.replace('_', '~') + + class Idb(bdb.Bdb): def __init__(self, gui): @@ -68,6 +76,14 @@ def __init__(self, pyshell, idb=None): self.make_gui() self.interacting = 0 self.nesting_level = 0 + self.framevars = {} + self.running = False + + def getimage(self, filename): + "Return an image object for a file in our 'Icons' directory" + dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'Icons') + return PhotoImage(master=self.root, file=os.path.join(dir, filename)) + def run(self, *args): # Deal with the scenario where we've already got a program running @@ -109,6 +125,15 @@ def run(self, *args): finally: self.interacting = 0 + def beginexecuting(self): + self.running = True + + def endexecuting(self): + self.running = False + self.show_status('') + self.enable_buttons(['prefs']) + self.clear_stack() + def close(self, event=None): try: self.quit() @@ -117,8 +142,7 @@ def close(self, event=None): if self.interacting: self.top.bell() return - if self.stackviewer: - self.stackviewer.close(); self.stackviewer = None + self.abort_loop() # Clean up pyshell if user clicked debugger control close widget. # (Causes a harmless extra cycle through close_debugger() if user # toggled debugger from pyshell Debug menu) @@ -130,80 +154,185 @@ def make_gui(self): pyshell = self.pyshell self.flist = pyshell.flist self.root = root = pyshell.root + self.clickable_cursor = 'hand2' + windowingsystem = self.root.tk.call('tk', 'windowingsystem') + if windowingsystem == 'aqua': + self.clickable_cursor = 'pointinghand' + self.tooltip = None + self.var_values = {} self.top = top = ListedToplevel(root) self.top.wm_title("Debug Control") self.top.wm_iconname("Debug") top.wm_protocol("WM_DELETE_WINDOW", self.close) self.top.bind("", self.close) - # - self.bframe = bframe = Frame(top) - self.bframe.pack(anchor="w") - self.buttons = bl = [] - # - self.bcont = b = Button(bframe, text="Go", command=self.cont) - bl.append(b) - self.bstep = b = Button(bframe, text="Step", command=self.step) - bl.append(b) - self.bnext = b = Button(bframe, text="Over", command=self.next) - bl.append(b) - self.bret = b = Button(bframe, text="Out", command=self.ret) - bl.append(b) - self.bret = b = Button(bframe, text="Quit", command=self.quit) - bl.append(b) - # - for b in bl: - b.configure(state="disabled") - b.pack(side="left") - # - self.cframe = cframe = Frame(bframe) - self.cframe.pack(side="left") - # - if not self.vstack: - self.__class__.vstack = BooleanVar(top) - self.vstack.set(1) - self.bstack = Checkbutton(cframe, - text="Stack", command=self.show_stack, variable=self.vstack) - self.bstack.grid(row=0, column=0) - if not self.vsource: - self.__class__.vsource = BooleanVar(top) - self.bsource = Checkbutton(cframe, - text="Source", command=self.show_source, variable=self.vsource) - self.bsource.grid(row=0, column=1) - if not self.vlocals: - self.__class__.vlocals = BooleanVar(top) - self.vlocals.set(1) - self.blocals = Checkbutton(cframe, - text="Locals", command=self.show_locals, variable=self.vlocals) - self.blocals.grid(row=1, column=0) - if not self.vglobals: - self.__class__.vglobals = BooleanVar(top) - self.bglobals = Checkbutton(cframe, - text="Globals", command=self.show_globals, variable=self.vglobals) - self.bglobals.grid(row=1, column=1) - # - self.status = Label(top, anchor="w") - self.status.pack(anchor="w") - self.error = Label(top, anchor="w") - self.error.pack(anchor="w", fill="x") - self.errorbg = self.error.cget("background") - # - self.fstack = Frame(top, height=1) - self.fstack.pack(expand=1, fill="both") - self.flocals = Frame(top) - self.flocals.pack(expand=1, fill="both") - self.fglobals = Frame(top, height=1) - self.fglobals.pack(expand=1, fill="both") - # - if self.vstack.get(): - self.show_stack() - if self.vlocals.get(): - self.show_locals() - if self.vglobals.get(): - self.show_globals() + + self.var_open_source_windows = BooleanVar(top, False) + + self.pane = ttk.PanedWindow(self.top, orient='horizontal') + self.pane.grid(column=0, row=0, sticky='nwes') + self.top.grid_columnconfigure(0, weight=1) + self.top.grid_rowconfigure(0, weight=1) + self.left = left = ttk.Frame(self.pane, padding=5) + self.pane.add(left, weight=1) + controls = ttk.Frame(left) + col = 0 + f = ('helvetica', 9) + self.buttondata = {} + self.buttons = ['go', 'step', 'over', 'out', 'stop', 'prefs'] + self.button_names = {'go':'Go', 'step':'Step', 'over':'Over', + 'out':'Out', 'stop':'Stop', 'prefs':'Options'} + self.button_cmds = {'go':self.cont, 'step':self.step, + 'over':self.next, 'out':self.ret, + 'stop':self.quit, 'prefs':self.options} + for key in self.buttons: + normal = self.getimage('debug_'+key+'.gif') + disabled = self.getimage('debug_'+key+'_disabled.gif') + b = ttk.Label(controls, image=normal, text=self.button_names[key], + compound='top', font=f) + b.grid(column=col, row=0, padx=[0,5]) + self.buttondata[key] = (b, normal, disabled) + col += 1 + self.enable_buttons(['prefs']) + self.status = ttk.Label(controls, text=' ', font=('helvetica', 13)) + self.status.grid(column=6, row=0, sticky='nw', padx=[25,0]) + controls.grid(column=0, row=0, sticky='new', pady=[0,6]) + controls.grid_columnconfigure(7, weight=1) + + self.current_line_img = self.getimage('debug_current.gif') + self.regular_line_img = self.getimage('debug_line.gif') + self.stack = ttk.Treeview(left, columns=('statement', ), + height=5, selectmode='browse') + self.stack.column('#0', width=100) + self.stack.column('#1', width=150) + self.stack.tag_configure('error', foreground='red') + self.stack.bind('<>', + lambda e: self.stack_selection_changed()) + self.stack.bind('', lambda e: self.stack_doubleclick()) + if windowingsystem == 'aqua': + self.stack.bind('', self.stack_contextmenu) + self.stack.bind('', self.stack_contextmenu) + else: + self.stack.bind('', self.stack_contextmenu) + + scroll = ttk.Scrollbar(left, command=self.stack.yview) + self.stack['yscrollcommand'] = scroll.set + self.stack.grid(column=0, row=2, sticky='nwes') + scroll.grid(column=1, row=2, sticky='ns') + left.grid_columnconfigure(0, weight=1) + left.grid_rowconfigure(2, weight=1) + + right = ttk.Frame(self.pane, padding=5) + self.pane.add(right, weight=1) + self.vars = ttk.Treeview(right, columns=('value',), height=5, + selectmode='none') + self.locals = self.vars.insert('', 'end', text='Locals', + open=True) + self.globals = self.vars.insert('', 'end', text='Globals', + open=False) + self.vars.column('#0', width=100) + self.vars.column('#1', width=150) + self.vars.bind('', self.mouse_moved_vars) + self.vars.bind('', self.leave_vars) + scroll2 = ttk.Scrollbar(right, command=self.vars.yview) + self.vars['yscrollcommand'] = scroll2.set + self.vars.grid(column=0, row=0, sticky='nwes') + scroll2.grid(column=1, row=0, sticky='ns') + right.grid_columnconfigure(0, weight=1) + right.grid_rowconfigure(0, weight=1) + left.bind('', lambda e: self._adjust_layout()) + self.clear_stack() + + + def _adjust_layout(self): + # if too narrow, move message below buttons + if self.left.winfo_width() < 380: + self.status.grid(column=0, row=1, columnspan=8, padx=[5,0]) + else: + self.status.grid(column=6, row=0, columnspan=1, padx=[25,0]) + + def enable_buttons(self, buttons=None): + for key in self.buttons: + if buttons is None or not key in buttons: + self.buttondata[key][0]['image'] = self.buttondata[key][2] + self.buttondata[key][0]['foreground'] = '#aaaaaa' + self.buttondata[key][0]['cursor'] = '' + self.buttondata[key][0].bind('<1>', 'break') + self.buttondata[key][0].bind('<>', 'break') + else: + self.buttondata[key][0]['image'] = self.buttondata[key][1] + self.buttondata[key][0]['foreground'] = '#000000' + self.buttondata[key][0].bind('<1>', self.button_cmds[key]) + self.buttondata[key][0].bind('<>', + self.button_cmds[key]) + self.buttondata[key][0]['cursor'] = self.clickable_cursor + + def stack_selection_changed(self): + self.show_vars() + + def stack_doubleclick(self): + sel = self.stack.selection() + if len(sel) == 1: + self.show_source(sel[0]) + + def stack_contextmenu(self, event): + item = self.stack.identify('item', event.x, event.y) + if item is not None and item != -1 and item != '': + menu = Menu(self.top, tearoff=0) + menu.add_command(label='View Source', + command = lambda: self.show_source(item)) + menu.tk_popup(event.x_root, event.y_root) + + def show_source(self, item): + if item in self.framevars: + fname = self.framevars[item][2] + lineno = self.framevars[item][3] + if fname[:1] + fname[-1:] != "<>" and os.path.exists(fname): + self.flist.gotofileline(fname, lineno) + + def show_status(self, msg, error=False): + self.status['text'] = msg + self.status['foreground'] = '#ff0000' if error else '#006600' + self.status['font'] = ('helvetica', 13, 'italic') if error \ + else ('helvetica', 13) + + def clear_stack(self): + self.stack.delete(*self.stack.get_children('')) + self.vars.delete(*self.vars.get_children(self.locals)) + self.vars.delete(*self.vars.get_children(self.globals)) + self.vars.detach(self.locals) + self.vars.detach(self.globals) + self.var_values = {} + + def add_stackframe(self, frame, lineno, current=False): + func = frame.f_code.co_name + if func in ("?", "", None): + func = '.' + try: + selfval = frame.f_locals['self'] + if selfval.__class__.__name__ == 'str': + # we've probably got the string representation of the + # object sent from the remote debugger, see if we can + # parse it into something useful + match = re.match('^<(?:.*)\.([^\.]*) object at 0x[0-9a-f]+>$', + selfval) + if match: + func = match.group(1) + '.' + func + else: + func = selfval.__class__.__name__ + '.' + func + except Exception: + pass + stmt = linecache.getline(frame.f_code.co_filename, lineno).strip() + image=self.current_line_img if current else self.regular_line_img + item = self.stack.insert('', 'end', text=func, + values=(stmt,), image=image) + self.framevars[item] = (frame.f_locals, frame.f_globals, + frame.f_code.co_filename, lineno) + if current: + self.stack.selection_set(item) def interaction(self, message, frame, info=None): self.frame = frame - self.status.configure(text=message) + self.show_status(message) # if info: type, value, tb = info @@ -216,26 +345,21 @@ def interaction(self, message, frame, info=None): m1 = "%s: %s" % (m1, str(value)) except: pass - bg = "yellow" else: m1 = "" tb = None - bg = self.errorbg - self.error.configure(text=m1, background=bg) - # - sv = self.stackviewer - if sv: - stack, i = self.idb.get_stack(self.frame, tb) - sv.load_stack(stack, i) - # - self.show_variables(1) - # - if self.vsource.get(): - self.sync_source_line() - # - for b in self.buttons: - b.configure(state="normal") - # + + if m1 != '': + self.show_status(m1, error=True) + stack, idx = self.idb.get_stack(self.frame, tb) + self.clear_stack() + for i in range(len(stack)): + frame, lineno = stack[i] + self.add_stackframe(frame, lineno, current=(i == idx)) + self.show_vars() + self.sync_source_line() + self.enable_buttons(self.buttons) + self.top.wakeup() # Nested main loop: Tkinter's main loop is not reentrant, so use # Tcl's vwait facility, which reenters the event loop until an @@ -243,20 +367,62 @@ def interaction(self, message, frame, info=None): self.nesting_level += 1 self.root.tk.call('vwait', '::idledebugwait') self.nesting_level -= 1 - # - for b in self.buttons: - b.configure(state="disabled") - self.status.configure(text="") - self.error.configure(text="", background=self.errorbg) self.frame = None + def show_vars(self): + self.vars.move(self.locals, '', 0) + self.vars.move(self.globals, '', 1) + self.vars.delete(*self.vars.get_children(self.locals)) + self.vars.delete(*self.vars.get_children(self.globals)) + self.var_values = {} + sel = self.stack.selection() + if len(sel) == 1 and sel[0] in self.framevars: + locals, globals, _, _ = self.framevars[sel[0]] + # note: locals/globals may be from a remotedebugger, in + # which case for reasons we don't need to get into here, + # they aren't iterable + self.add_varheader() + for name in sorted(locals.keys(), key=underscore_at_end): + self.add_var(name, locals[name]) + self.add_varheader(isGlobal=True) + for name in sorted(globals.keys(), key=underscore_at_end): + self.add_var(name, globals[name], isGlobal=True) + + def add_varheader(self, isGlobal=False): + pass + + def add_var(self, varname, value, isGlobal=False): + item = self.vars.insert(self.globals if isGlobal else self.locals, + 'end', text=varname, values=(value, )) + self.var_values[item] = value + + def mouse_moved_vars(self, ev): + # tooltip_schedule(ev, self.var_tooltip) + pass + + def leave_vars(self, ev): + # tooltip_clear() + pass + + def var_tooltip(self, ev): + # Callback from tooltip package to return text of tooltip + item = None + if self.vars.identify('column', ev.x, ev.y) == '#1': + item = self.vars.identify('item', ev.x, ev.y) + if item and item in self.var_values: + return(self.var_values[item], ev.x + self.vars.winfo_rootx() + 10, + ev.y + self.vars.winfo_rooty() + 5) + return None + def sync_source_line(self): frame = self.frame if not frame: return filename, lineno = self.__frame2fileline(frame) if filename[:1] + filename[-1:] != "<>" and os.path.exists(filename): - self.flist.gotofileline(filename, lineno) + if self.var_open_source_windows.get() or\ + self.flist.already_open(filename): + self.flist.gotofileline(filename, lineno) def __frame2fileline(self, frame): code = frame.f_code @@ -264,94 +430,46 @@ def __frame2fileline(self, frame): lineno = frame.f_lineno return filename, lineno - def cont(self): + def invoke_program(self): + "Called just before taking the next action in debugger, adjust state" + self.enable_buttons(['stop']) + self.show_status('Running...') + + def cont(self, ev=None): + self.invoke_program() self.idb.set_continue() self.abort_loop() - def step(self): + def step(self, ev=None): + self.invoke_program() self.idb.set_step() self.abort_loop() - def next(self): + def next(self, ev=None): + self.invoke_program() self.idb.set_next(self.frame) self.abort_loop() - def ret(self): + def ret(self, ev=None): + self.invoke_program() self.idb.set_return(self.frame) self.abort_loop() - def quit(self): + def quit(self, ev=None): + self.invoke_program() self.idb.set_quit() self.abort_loop() def abort_loop(self): self.root.tk.call('set', '::idledebugwait', '1') - stackviewer = None - - def show_stack(self): - if not self.stackviewer and self.vstack.get(): - self.stackviewer = sv = StackViewer(self.fstack, self.flist, self) - if self.frame: - stack, i = self.idb.get_stack(self.frame, None) - sv.load_stack(stack, i) - else: - sv = self.stackviewer - if sv and not self.vstack.get(): - self.stackviewer = None - sv.close() - self.fstack['height'] = 1 - - def show_source(self): - if self.vsource.get(): - self.sync_source_line() - - def show_frame(self, stackitem): - self.frame = stackitem[0] # lineno is stackitem[1] - self.show_variables() - - localsviewer = None - globalsviewer = None - - def show_locals(self): - lv = self.localsviewer - if self.vlocals.get(): - if not lv: - self.localsviewer = NamespaceViewer(self.flocals, "Locals") - else: - if lv: - self.localsviewer = None - lv.close() - self.flocals['height'] = 1 - self.show_variables() - - def show_globals(self): - gv = self.globalsviewer - if self.vglobals.get(): - if not gv: - self.globalsviewer = NamespaceViewer(self.fglobals, "Globals") - else: - if gv: - self.globalsviewer = None - gv.close() - self.fglobals['height'] = 1 - self.show_variables() - - def show_variables(self, force=0): - lv = self.localsviewer - gv = self.globalsviewer - frame = self.frame - if not frame: - ldict = gdict = None - else: - ldict = frame.f_locals - gdict = frame.f_globals - if lv and gv and ldict is gdict: - ldict = None - if lv: - lv.load_dict(ldict, force, self.pyshell.interp.rpcclt) - if gv: - gv.load_dict(gdict, force, self.pyshell.interp.rpcclt) + def options(self, ev=None): + menu = Menu(self.top, tearoff=0) + menu.add_checkbutton(label='Show Source in Open Files Only', + variable=self.var_open_source_windows, onvalue=False) + menu.add_checkbutton(label='Automatically Open Files to Show Source', + variable=self.var_open_source_windows, onvalue=True) + menu.tk_popup(ev.x_root, ev.y_root) def set_breakpoint_here(self, filename, lineno): self.idb.set_break(filename, lineno) @@ -372,176 +490,6 @@ def load_breakpoints(self): except AttributeError: continue -class StackViewer(ScrolledList): - - def __init__(self, master, flist, gui): - if macosx.isAquaTk(): - # At least on with the stock AquaTk version on OSX 10.4 you'll - # get a shaking GUI that eventually kills IDLE if the width - # argument is specified. - ScrolledList.__init__(self, master) - else: - ScrolledList.__init__(self, master, width=80) - self.flist = flist - self.gui = gui - self.stack = [] - - def load_stack(self, stack, index=None): - self.stack = stack - self.clear() - for i in range(len(stack)): - frame, lineno = stack[i] - try: - modname = frame.f_globals["__name__"] - except: - modname = "?" - code = frame.f_code - filename = code.co_filename - funcname = code.co_name - import linecache - sourceline = linecache.getline(filename, lineno) - sourceline = sourceline.strip() - if funcname in ("?", "", None): - item = "%s, line %d: %s" % (modname, lineno, sourceline) - else: - item = "%s.%s(), line %d: %s" % (modname, funcname, - lineno, sourceline) - if i == index: - item = "> " + item - self.append(item) - if index is not None: - self.select(index) - - def popup_event(self, event): - "override base method" - if self.stack: - return ScrolledList.popup_event(self, event) - - def fill_menu(self): - "override base method" - menu = self.menu - menu.add_command(label="Go to source line", - command=self.goto_source_line) - menu.add_command(label="Show stack frame", - command=self.show_stack_frame) - - def on_select(self, index): - "override base method" - if 0 <= index < len(self.stack): - self.gui.show_frame(self.stack[index]) - - def on_double(self, index): - "override base method" - self.show_source(index) - - def goto_source_line(self): - index = self.listbox.index("active") - self.show_source(index) - - def show_stack_frame(self): - index = self.listbox.index("active") - if 0 <= index < len(self.stack): - self.gui.show_frame(self.stack[index]) - - def show_source(self, index): - if not (0 <= index < len(self.stack)): - return - frame, lineno = self.stack[index] - code = frame.f_code - filename = code.co_filename - if os.path.isfile(filename): - edit = self.flist.open(filename) - if edit: - edit.gotoline(lineno) - - -class NamespaceViewer: - - def __init__(self, master, title, dict=None): - width = 0 - height = 40 - if dict: - height = 20*len(dict) # XXX 20 == observed height of Entry widget - self.master = master - self.title = title - import reprlib - self.repr = reprlib.Repr() - self.repr.maxstring = 60 - self.repr.maxother = 60 - self.frame = frame = Frame(master) - self.frame.pack(expand=1, fill="both") - self.label = Label(frame, text=title, borderwidth=2, relief="groove") - self.label.pack(fill="x") - self.vbar = vbar = Scrollbar(frame, name="vbar") - vbar.pack(side="right", fill="y") - self.canvas = canvas = Canvas(frame, - height=min(300, max(40, height)), - scrollregion=(0, 0, width, height)) - canvas.pack(side="left", fill="both", expand=1) - vbar["command"] = canvas.yview - canvas["yscrollcommand"] = vbar.set - self.subframe = subframe = Frame(canvas) - self.sfid = canvas.create_window(0, 0, window=subframe, anchor="nw") - self.load_dict(dict) - - dict = -1 - - def load_dict(self, dict, force=0, rpc_client=None): - if dict is self.dict and not force: - return - subframe = self.subframe - frame = self.frame - for c in list(subframe.children.values()): - c.destroy() - self.dict = None - if not dict: - l = Label(subframe, text="None") - l.grid(row=0, column=0) - else: - #names = sorted(dict) - ### - # Because of (temporary) limitations on the dict_keys type (not yet - # public or pickleable), have the subprocess to send a list of - # keys, not a dict_keys object. sorted() will take a dict_keys - # (no subprocess) or a list. - # - # There is also an obscure bug in sorted(dict) where the - # interpreter gets into a loop requesting non-existing dict[0], - # dict[1], dict[2], etc from the debugger_r.DictProxy. - ### - keys_list = dict.keys() - names = sorted(keys_list) - ### - row = 0 - for name in names: - value = dict[name] - svalue = self.repr.repr(value) # repr(value) - # Strip extra quotes caused by calling repr on the (already) - # repr'd value sent across the RPC interface: - if rpc_client: - svalue = svalue[1:-1] - l = Label(subframe, text=name) - l.grid(row=row, column=0, sticky="nw") - l = Entry(subframe, width=0, borderwidth=0) - l.insert(0, svalue) - l.grid(row=row, column=1, sticky="nw") - row = row+1 - self.dict = dict - # XXX Could we use a callback for the following? - subframe.update_idletasks() # Alas! - width = subframe.winfo_reqwidth() - height = subframe.winfo_reqheight() - canvas = self.canvas - self.canvas["scrollregion"] = (0, 0, width, height) - if height > 300: - canvas["height"] = 300 - frame.pack(expand=1) - else: - canvas["height"] = height - frame.pack(expand=0) - - def close(self): - self.frame.destroy() if __name__ == "__main__": from unittest import main diff --git a/Lib/idlelib/filelist.py b/Lib/idlelib/filelist.py index 0d200854ef0007..c57bfdef639c34 100644 --- a/Lib/idlelib/filelist.py +++ b/Lib/idlelib/filelist.py @@ -41,6 +41,14 @@ def open(self, filename, action=None): edit._close() return None + def already_open(self, filename): + assert filename + filename = self.canonize(filename) + if not os.path.isdir(filename): + key = os.path.normcase(filename) + return key in self.dict + return False + def gotofileline(self, filename, lineno=None): edit = self.open(filename) if edit is not None and lineno is not None: diff --git a/Lib/idlelib/idle_test/test_debugger.py b/Lib/idlelib/idle_test/test_debugger.py index 35efb3411c73b5..ed4fa6f7bfbd52 100644 --- a/Lib/idlelib/idle_test/test_debugger.py +++ b/Lib/idlelib/idle_test/test_debugger.py @@ -19,8 +19,6 @@ def tearDownClass(cls): cls.root.destroy() del cls.root - def test_init(self): - debugger.NamespaceViewer(self.root, 'Test') # Other classes are Idb, Debugger, and StackViewer. diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index b69916dbe876ca..530d70d3e8ad54 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -489,6 +489,7 @@ def restart_subprocess(self, with_cwd=False, filename=''): debugger_r.close_subprocess_debugger(self.rpcclt) except: pass + debug.endexecuting() # Kill subprocess, spawn a new one, accept connection. self.rpcclt.close() self.terminate_subprocess() @@ -791,6 +792,8 @@ def runcode(self, code): if self.tkconsole.canceled: self.tkconsole.canceled = False print("KeyboardInterrupt", file=self.tkconsole.stderr) + if self.interp.debugger: + self.interp.debugger.endexecuting() else: self.showtraceback() finally: @@ -991,11 +994,15 @@ def open_debugger(self): def beginexecuting(self): "Helper for ModifiedInterpreter" + if self.interp.debugger: + self.interp.debugger.beginexecuting() self.resetoutput() self.executing = True def endexecuting(self): "Helper for ModifiedInterpreter" + if self.interp.debugger: + self.interp.debugger.endexecuting() self.executing = False self.canceled = False self.showprompt() From 3cabc0ad80f5bf13b41d28f607c77f869eef7af0 Mon Sep 17 00:00:00 2001 From: Mark Roseman Date: Sat, 24 Oct 2020 13:38:18 -0700 Subject: [PATCH 02/12] scrolledlist no longer needed was being used by stack trace in debugger --- Lib/idlelib/idle_test/test_scrolledlist.py | 27 ---- Lib/idlelib/scrolledlist.py | 149 --------------------- 2 files changed, 176 deletions(-) delete mode 100644 Lib/idlelib/idle_test/test_scrolledlist.py delete mode 100644 Lib/idlelib/scrolledlist.py diff --git a/Lib/idlelib/idle_test/test_scrolledlist.py b/Lib/idlelib/idle_test/test_scrolledlist.py deleted file mode 100644 index 2f819fda025ba3..00000000000000 --- a/Lib/idlelib/idle_test/test_scrolledlist.py +++ /dev/null @@ -1,27 +0,0 @@ -"Test scrolledlist, coverage 38%." - -from idlelib.scrolledlist import ScrolledList -import unittest -from test.support import requires -requires('gui') -from tkinter import Tk - - -class ScrolledListTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.root = Tk() - - @classmethod - def tearDownClass(cls): - cls.root.destroy() - del cls.root - - - def test_init(self): - ScrolledList(self.root) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/scrolledlist.py b/Lib/idlelib/scrolledlist.py deleted file mode 100644 index 71fd18ab19ec8a..00000000000000 --- a/Lib/idlelib/scrolledlist.py +++ /dev/null @@ -1,149 +0,0 @@ -from tkinter import * -from tkinter.ttk import Frame, Scrollbar - -from idlelib import macosx - - -class ScrolledList: - - default = "(None)" - - def __init__(self, master, **options): - # Create top frame, with scrollbar and listbox - self.master = master - self.frame = frame = Frame(master) - self.frame.pack(fill="both", expand=1) - self.vbar = vbar = Scrollbar(frame, name="vbar") - self.vbar.pack(side="right", fill="y") - self.listbox = listbox = Listbox(frame, exportselection=0, - background="white") - if options: - listbox.configure(options) - listbox.pack(expand=1, fill="both") - # Tie listbox and scrollbar together - vbar["command"] = listbox.yview - listbox["yscrollcommand"] = vbar.set - # Bind events to the list box - listbox.bind("", self.click_event) - listbox.bind("", self.double_click_event) - if macosx.isAquaTk(): - listbox.bind("", self.popup_event) - listbox.bind("", self.popup_event) - else: - listbox.bind("", self.popup_event) - listbox.bind("", self.up_event) - listbox.bind("", self.down_event) - # Mark as empty - self.clear() - - def close(self): - self.frame.destroy() - - def clear(self): - self.listbox.delete(0, "end") - self.empty = 1 - self.listbox.insert("end", self.default) - - def append(self, item): - if self.empty: - self.listbox.delete(0, "end") - self.empty = 0 - self.listbox.insert("end", str(item)) - - def get(self, index): - return self.listbox.get(index) - - def click_event(self, event): - self.listbox.activate("@%d,%d" % (event.x, event.y)) - index = self.listbox.index("active") - self.select(index) - self.on_select(index) - return "break" - - def double_click_event(self, event): - index = self.listbox.index("active") - self.select(index) - self.on_double(index) - return "break" - - menu = None - - def popup_event(self, event): - if not self.menu: - self.make_menu() - menu = self.menu - self.listbox.activate("@%d,%d" % (event.x, event.y)) - index = self.listbox.index("active") - self.select(index) - menu.tk_popup(event.x_root, event.y_root) - return "break" - - def make_menu(self): - menu = Menu(self.listbox, tearoff=0) - self.menu = menu - self.fill_menu() - - def up_event(self, event): - index = self.listbox.index("active") - if self.listbox.selection_includes(index): - index = index - 1 - else: - index = self.listbox.size() - 1 - if index < 0: - self.listbox.bell() - else: - self.select(index) - self.on_select(index) - return "break" - - def down_event(self, event): - index = self.listbox.index("active") - if self.listbox.selection_includes(index): - index = index + 1 - else: - index = 0 - if index >= self.listbox.size(): - self.listbox.bell() - else: - self.select(index) - self.on_select(index) - return "break" - - def select(self, index): - self.listbox.focus_set() - self.listbox.activate(index) - self.listbox.selection_clear(0, "end") - self.listbox.selection_set(index) - self.listbox.see(index) - - # Methods to override for specific actions - - def fill_menu(self): - pass - - def on_select(self, index): - pass - - def on_double(self, index): - pass - - -def _scrolled_list(parent): # htest # - top = Toplevel(parent) - x, y = map(int, parent.geometry().split('+')[1:]) - top.geometry("+%d+%d" % (x+200, y + 175)) - class MyScrolledList(ScrolledList): - def fill_menu(self): self.menu.add_command(label="right click") - def on_select(self, index): print("select", self.get(index)) - def on_double(self, index): print("double", self.get(index)) - - scrolled_list = MyScrolledList(top) - for i in range(30): - scrolled_list.append("Item %02d" % i) - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_scrolledlist', verbosity=2,) - - from idlelib.idle_test.htest import run - run(_scrolled_list) From a8b8da45585764f16a8667252324497ac81e7737 Mon Sep 17 00:00:00 2001 From: Mark Roseman Date: Sun, 25 Oct 2020 10:37:10 -0700 Subject: [PATCH 03/12] adjust fonts to use standard named fonts - button captions now use TkIconFont (a bit bigger than before) - status line now uses default font, plus derives italic variation for errors --- Lib/idlelib/debugger.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Lib/idlelib/debugger.py b/Lib/idlelib/debugger.py index acfa6e85f54c3e..58e4d5716d57b1 100644 --- a/Lib/idlelib/debugger.py +++ b/Lib/idlelib/debugger.py @@ -176,7 +176,6 @@ def make_gui(self): self.pane.add(left, weight=1) controls = ttk.Frame(left) col = 0 - f = ('helvetica', 9) self.buttondata = {} self.buttons = ['go', 'step', 'over', 'out', 'stop', 'prefs'] self.button_names = {'go':'Go', 'step':'Step', 'over':'Over', @@ -188,12 +187,15 @@ def make_gui(self): normal = self.getimage('debug_'+key+'.gif') disabled = self.getimage('debug_'+key+'_disabled.gif') b = ttk.Label(controls, image=normal, text=self.button_names[key], - compound='top', font=f) + compound='top', font='TkIconFont') b.grid(column=col, row=0, padx=[0,5]) self.buttondata[key] = (b, normal, disabled) col += 1 self.enable_buttons(['prefs']) - self.status = ttk.Label(controls, text=' ', font=('helvetica', 13)) + self.status_normal_font = Font(root=self.root, name='TkDefaultFont', exists=True) + self.status_error_font = self.status_normal_font.copy() + self.status_error_font['slant'] = 'italic' + self.status = ttk.Label(controls, text=' ', font=self.status_normal_font) self.status.grid(column=6, row=0, sticky='nw', padx=[25,0]) controls.grid(column=0, row=0, sticky='new', pady=[0,6]) controls.grid_columnconfigure(7, weight=1) @@ -292,8 +294,8 @@ def show_source(self, item): def show_status(self, msg, error=False): self.status['text'] = msg self.status['foreground'] = '#ff0000' if error else '#006600' - self.status['font'] = ('helvetica', 13, 'italic') if error \ - else ('helvetica', 13) + self.status['font'] = self.status_error_font if error \ + else self.status_normal_font def clear_stack(self): self.stack.delete(*self.stack.get_children('')) From 7787255070111d30e885536cc9533adf18843659 Mon Sep 17 00:00:00 2001 From: Mark Roseman Date: Mon, 2 Nov 2020 07:39:38 -0800 Subject: [PATCH 04/12] Apply suggestions from code review Co-authored-by: Terry Jan Reedy Co-authored-by: E-Paine <63801254+E-Paine@users.noreply.github.com> --- Lib/idlelib/debugger.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Lib/idlelib/debugger.py b/Lib/idlelib/debugger.py index 58e4d5716d57b1..0d497be3a1f0e6 100644 --- a/Lib/idlelib/debugger.py +++ b/Lib/idlelib/debugger.py @@ -84,7 +84,6 @@ def getimage(self, filename): dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'Icons') return PhotoImage(master=self.root, file=os.path.join(dir, filename)) - def run(self, *args): # Deal with the scenario where we've already got a program running # in the debugger and we want to start another. If that is the case, @@ -175,7 +174,6 @@ def make_gui(self): self.left = left = ttk.Frame(self.pane, padding=5) self.pane.add(left, weight=1) controls = ttk.Frame(left) - col = 0 self.buttondata = {} self.buttons = ['go', 'step', 'over', 'out', 'stop', 'prefs'] self.button_names = {'go':'Go', 'step':'Step', 'over':'Over', @@ -183,14 +181,13 @@ def make_gui(self): self.button_cmds = {'go':self.cont, 'step':self.step, 'over':self.next, 'out':self.ret, 'stop':self.quit, 'prefs':self.options} - for key in self.buttons: + for col, key in enumerate(self.buttons): normal = self.getimage('debug_'+key+'.gif') disabled = self.getimage('debug_'+key+'_disabled.gif') b = ttk.Label(controls, image=normal, text=self.button_names[key], compound='top', font='TkIconFont') b.grid(column=col, row=0, padx=[0,5]) self.buttondata[key] = (b, normal, disabled) - col += 1 self.enable_buttons(['prefs']) self.status_normal_font = Font(root=self.root, name='TkDefaultFont', exists=True) self.status_error_font = self.status_normal_font.copy() @@ -254,7 +251,7 @@ def _adjust_layout(self): def enable_buttons(self, buttons=None): for key in self.buttons: - if buttons is None or not key in buttons: + if buttons is None or key not in buttons: self.buttondata[key][0]['image'] = self.buttondata[key][2] self.buttondata[key][0]['foreground'] = '#aaaaaa' self.buttondata[key][0]['cursor'] = '' @@ -324,7 +321,7 @@ def add_stackframe(self, frame, lineno, current=False): except Exception: pass stmt = linecache.getline(frame.f_code.co_filename, lineno).strip() - image=self.current_line_img if current else self.regular_line_img + image = self.current_line_img if current else self.regular_line_img item = self.stack.insert('', 'end', text=func, values=(stmt,), image=image) self.framevars[item] = (frame.f_locals, frame.f_globals, @@ -422,8 +419,8 @@ def sync_source_line(self): return filename, lineno = self.__frame2fileline(frame) if filename[:1] + filename[-1:] != "<>" and os.path.exists(filename): - if self.var_open_source_windows.get() or\ - self.flist.already_open(filename): + if (self.var_open_source_windows.get() or + self.flist.already_open(filename)): self.flist.gotofileline(filename, lineno) def __frame2fileline(self, frame): From 37399edb80147b6fb49f4eb8fda09322613e2a9a Mon Sep 17 00:00:00 2001 From: Mark Roseman Date: Mon, 2 Nov 2020 07:54:46 -0800 Subject: [PATCH 05/12] apply suggestions from code review --- Lib/idlelib/debugger.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Lib/idlelib/debugger.py b/Lib/idlelib/debugger.py index 0d497be3a1f0e6..90427f8819d50b 100644 --- a/Lib/idlelib/debugger.py +++ b/Lib/idlelib/debugger.py @@ -9,11 +9,14 @@ from tkinter.font import Font from tkinter.ttk import Scrollbar -from idlelib import macosx from idlelib.window import ListedToplevel def underscore_at_end(s): + """Helper used when displaying a sorted list of local or global variables + so that internal variables (starting with an underscore) are displayed + after others, not before. + """ return s.replace('_', '~') @@ -154,8 +157,7 @@ def make_gui(self): self.flist = pyshell.flist self.root = root = pyshell.root self.clickable_cursor = 'hand2' - windowingsystem = self.root.tk.call('tk', 'windowingsystem') - if windowingsystem == 'aqua': + if self.root._windowingsystem == 'aqua': self.clickable_cursor = 'pointinghand' self.tooltip = None self.var_values = {} @@ -207,7 +209,7 @@ def make_gui(self): self.stack.bind('<>', lambda e: self.stack_selection_changed()) self.stack.bind('', lambda e: self.stack_doubleclick()) - if windowingsystem == 'aqua': + if self.root._windowingsystem == 'aqua': self.stack.bind('', self.stack_contextmenu) self.stack.bind('', self.stack_contextmenu) else: @@ -383,15 +385,15 @@ def show_vars(self): self.add_varheader() for name in sorted(locals.keys(), key=underscore_at_end): self.add_var(name, locals[name]) - self.add_varheader(isGlobal=True) + self.add_varheader(is_global=True) for name in sorted(globals.keys(), key=underscore_at_end): - self.add_var(name, globals[name], isGlobal=True) + self.add_var(name, globals[name], is_global=True) - def add_varheader(self, isGlobal=False): + def add_varheader(self, is_global=False): pass - def add_var(self, varname, value, isGlobal=False): - item = self.vars.insert(self.globals if isGlobal else self.locals, + def add_var(self, varname, value, is_global=False): + item = self.vars.insert(self.globals if is_global else self.locals, 'end', text=varname, values=(value, )) self.var_values[item] = value From 041d552cd25f6f4daf5ac775dfa5c5cff4e2091b Mon Sep 17 00:00:00 2001 From: Mark Roseman Date: Mon, 2 Nov 2020 08:17:54 -0800 Subject: [PATCH 06/12] apply suggestions from code review --- Lib/idlelib/debugger.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/idlelib/debugger.py b/Lib/idlelib/debugger.py index 90427f8819d50b..51f988a2ae3b03 100644 --- a/Lib/idlelib/debugger.py +++ b/Lib/idlelib/debugger.py @@ -3,11 +3,9 @@ import linecache from tkinter import * -from tkinter.ttk import Frame, Scrollbar from tkinter import ttk from tkinter import PhotoImage from tkinter.font import Font -from tkinter.ttk import Scrollbar from idlelib.window import ListedToplevel @@ -178,7 +176,7 @@ def make_gui(self): controls = ttk.Frame(left) self.buttondata = {} self.buttons = ['go', 'step', 'over', 'out', 'stop', 'prefs'] - self.button_names = {'go':'Go', 'step':'Step', 'over':'Over', + button_names = {'go':'Go', 'step':'Step', 'over':'Over', 'out':'Out', 'stop':'Stop', 'prefs':'Options'} self.button_cmds = {'go':self.cont, 'step':self.step, 'over':self.next, 'out':self.ret, @@ -186,7 +184,7 @@ def make_gui(self): for col, key in enumerate(self.buttons): normal = self.getimage('debug_'+key+'.gif') disabled = self.getimage('debug_'+key+'_disabled.gif') - b = ttk.Label(controls, image=normal, text=self.button_names[key], + b = ttk.Label(controls, image=normal, text=button_names[key], compound='top', font='TkIconFont') b.grid(column=col, row=0, padx=[0,5]) self.buttondata[key] = (b, normal, disabled) From 448527e74b91e77412d773be3d48dc76a6fd05d3 Mon Sep 17 00:00:00 2001 From: Mark Roseman Date: Wed, 4 Nov 2020 08:51:12 -0800 Subject: [PATCH 07/12] clean up based on review comments - re, exception --- Lib/idlelib/debugger.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Lib/idlelib/debugger.py b/Lib/idlelib/debugger.py index 51f988a2ae3b03..aef47fde371785 100644 --- a/Lib/idlelib/debugger.py +++ b/Lib/idlelib/debugger.py @@ -1,6 +1,7 @@ import bdb import os import linecache +import re from tkinter import * from tkinter import ttk @@ -308,18 +309,21 @@ def add_stackframe(self, frame, lineno, current=False): func = '.' try: selfval = frame.f_locals['self'] + except KeyError: + selfval = None; + if selfval: + # this stackframe represents an object method; preface the method + # name with the name of the class if selfval.__class__.__name__ == 'str': - # we've probably got the string representation of the - # object sent from the remote debugger, see if we can - # parse it into something useful - match = re.match('^<(?:.*)\.([^\.]*) object at 0x[0-9a-f]+>$', + # we've got the string representation of the object sent from + # the remote debugger; parse out the name of the class, e.g. + # from "" extract "Random" + match = re.match(r'^<(?:.*)\.([^.]*) object at 0x[0-9a-f]+>$', selfval) if match: func = match.group(1) + '.' + func else: func = selfval.__class__.__name__ + '.' + func - except Exception: - pass stmt = linecache.getline(frame.f_code.co_filename, lineno).strip() image = self.current_line_img if current else self.regular_line_img item = self.stack.insert('', 'end', text=func, From a05703257de847b996f44788fc027446a18e35d3 Mon Sep 17 00:00:00 2001 From: Mark Roseman Date: Wed, 4 Nov 2020 09:05:32 -0800 Subject: [PATCH 08/12] explicit tkinter imports as per review comments --- Lib/idlelib/debugger.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Lib/idlelib/debugger.py b/Lib/idlelib/debugger.py index aef47fde371785..7cd26f181ad599 100644 --- a/Lib/idlelib/debugger.py +++ b/Lib/idlelib/debugger.py @@ -3,8 +3,8 @@ import linecache import re -from tkinter import * -from tkinter import ttk +from tkinter import BooleanVar, Menu +from tkinter.ttk import PanedWindow, Frame, Label, Treeview, Scrollbar from tkinter import PhotoImage from tkinter.font import Font @@ -168,13 +168,13 @@ def make_gui(self): self.var_open_source_windows = BooleanVar(top, False) - self.pane = ttk.PanedWindow(self.top, orient='horizontal') + self.pane = PanedWindow(self.top, orient='horizontal') self.pane.grid(column=0, row=0, sticky='nwes') self.top.grid_columnconfigure(0, weight=1) self.top.grid_rowconfigure(0, weight=1) - self.left = left = ttk.Frame(self.pane, padding=5) + self.left = left = Frame(self.pane, padding=5) self.pane.add(left, weight=1) - controls = ttk.Frame(left) + controls = Frame(left) self.buttondata = {} self.buttons = ['go', 'step', 'over', 'out', 'stop', 'prefs'] button_names = {'go':'Go', 'step':'Step', 'over':'Over', @@ -185,7 +185,7 @@ def make_gui(self): for col, key in enumerate(self.buttons): normal = self.getimage('debug_'+key+'.gif') disabled = self.getimage('debug_'+key+'_disabled.gif') - b = ttk.Label(controls, image=normal, text=button_names[key], + b = Label(controls, image=normal, text=button_names[key], compound='top', font='TkIconFont') b.grid(column=col, row=0, padx=[0,5]) self.buttondata[key] = (b, normal, disabled) @@ -193,14 +193,14 @@ def make_gui(self): self.status_normal_font = Font(root=self.root, name='TkDefaultFont', exists=True) self.status_error_font = self.status_normal_font.copy() self.status_error_font['slant'] = 'italic' - self.status = ttk.Label(controls, text=' ', font=self.status_normal_font) + self.status = Label(controls, text=' ', font=self.status_normal_font) self.status.grid(column=6, row=0, sticky='nw', padx=[25,0]) controls.grid(column=0, row=0, sticky='new', pady=[0,6]) controls.grid_columnconfigure(7, weight=1) self.current_line_img = self.getimage('debug_current.gif') self.regular_line_img = self.getimage('debug_line.gif') - self.stack = ttk.Treeview(left, columns=('statement', ), + self.stack = Treeview(left, columns=('statement', ), height=5, selectmode='browse') self.stack.column('#0', width=100) self.stack.column('#1', width=150) @@ -214,16 +214,16 @@ def make_gui(self): else: self.stack.bind('', self.stack_contextmenu) - scroll = ttk.Scrollbar(left, command=self.stack.yview) + scroll = Scrollbar(left, command=self.stack.yview) self.stack['yscrollcommand'] = scroll.set self.stack.grid(column=0, row=2, sticky='nwes') scroll.grid(column=1, row=2, sticky='ns') left.grid_columnconfigure(0, weight=1) left.grid_rowconfigure(2, weight=1) - right = ttk.Frame(self.pane, padding=5) + right = Frame(self.pane, padding=5) self.pane.add(right, weight=1) - self.vars = ttk.Treeview(right, columns=('value',), height=5, + self.vars = Treeview(right, columns=('value',), height=5, selectmode='none') self.locals = self.vars.insert('', 'end', text='Locals', open=True) @@ -233,7 +233,7 @@ def make_gui(self): self.vars.column('#1', width=150) self.vars.bind('', self.mouse_moved_vars) self.vars.bind('', self.leave_vars) - scroll2 = ttk.Scrollbar(right, command=self.vars.yview) + scroll2 = Scrollbar(right, command=self.vars.yview) self.vars['yscrollcommand'] = scroll2.set self.vars.grid(column=0, row=0, sticky='nwes') scroll2.grid(column=1, row=0, sticky='ns') From 430496976d0e82b44070408c1758af3ddcd271fd Mon Sep 17 00:00:00 2001 From: Mark Roseman Date: Sun, 15 Nov 2020 14:25:30 -0800 Subject: [PATCH 09/12] status line always goes below toolbar buttons, regardless of window width --- Lib/idlelib/debugger.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/Lib/idlelib/debugger.py b/Lib/idlelib/debugger.py index 7cd26f181ad599..9a69a61db9b9f3 100644 --- a/Lib/idlelib/debugger.py +++ b/Lib/idlelib/debugger.py @@ -194,7 +194,7 @@ def make_gui(self): self.status_error_font = self.status_normal_font.copy() self.status_error_font['slant'] = 'italic' self.status = Label(controls, text=' ', font=self.status_normal_font) - self.status.grid(column=6, row=0, sticky='nw', padx=[25,0]) + self.status.grid(column=0, row=1, columnspan=8, sticky='nw', padx=[5,0]) controls.grid(column=0, row=0, sticky='new', pady=[0,6]) controls.grid_columnconfigure(7, weight=1) @@ -239,17 +239,8 @@ def make_gui(self): scroll2.grid(column=1, row=0, sticky='ns') right.grid_columnconfigure(0, weight=1) right.grid_rowconfigure(0, weight=1) - left.bind('', lambda e: self._adjust_layout()) self.clear_stack() - - def _adjust_layout(self): - # if too narrow, move message below buttons - if self.left.winfo_width() < 380: - self.status.grid(column=0, row=1, columnspan=8, padx=[5,0]) - else: - self.status.grid(column=6, row=0, columnspan=1, padx=[25,0]) - def enable_buttons(self, buttons=None): for key in self.buttons: if buttons is None or key not in buttons: From 812768f2655ebefb4088caa5161cb74b47fa91ef Mon Sep 17 00:00:00 2001 From: Mark Roseman Date: Sun, 15 Nov 2020 14:36:33 -0800 Subject: [PATCH 10/12] Apply suggestions from code review (comments) Co-authored-by: Terry Jan Reedy --- Lib/idlelib/debugger.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/idlelib/debugger.py b/Lib/idlelib/debugger.py index 9a69a61db9b9f3..603a70bb6e4f46 100644 --- a/Lib/idlelib/debugger.py +++ b/Lib/idlelib/debugger.py @@ -306,9 +306,9 @@ def add_stackframe(self, frame, lineno, current=False): # this stackframe represents an object method; preface the method # name with the name of the class if selfval.__class__.__name__ == 'str': - # we've got the string representation of the object sent from + # We've got the string representation of the object sent from # the remote debugger; parse out the name of the class, e.g. - # from "" extract "Random" + # from "" extract "Random". match = re.match(r'^<(?:.*)\.([^.]*) object at 0x[0-9a-f]+>$', selfval) if match: @@ -399,7 +399,7 @@ def leave_vars(self, ev): pass def var_tooltip(self, ev): - # Callback from tooltip package to return text of tooltip + # Callback from tooltip package to return text of tooltip. item = None if self.vars.identify('column', ev.x, ev.y) == '#1': item = self.vars.identify('item', ev.x, ev.y) From 72740a3060bf9fe037cd97218d7eb1f15a8ea693 Mon Sep 17 00:00:00 2001 From: Mark Roseman Date: Sun, 15 Nov 2020 14:42:57 -0800 Subject: [PATCH 11/12] missing import --- Lib/idlelib/debugger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/debugger.py b/Lib/idlelib/debugger.py index 603a70bb6e4f46..3dcfc0b807b226 100644 --- a/Lib/idlelib/debugger.py +++ b/Lib/idlelib/debugger.py @@ -3,7 +3,7 @@ import linecache import re -from tkinter import BooleanVar, Menu +from tkinter import BooleanVar, Menu, TclError from tkinter.ttk import PanedWindow, Frame, Label, Treeview, Scrollbar from tkinter import PhotoImage from tkinter.font import Font From f9f9331faee39ba858740afc14e83ce86071da93 Mon Sep 17 00:00:00 2001 From: Mark Roseman Date: Sun, 15 Nov 2020 14:49:50 -0800 Subject: [PATCH 12/12] tweak comments as per review suggestions --- Lib/idlelib/debugger.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Lib/idlelib/debugger.py b/Lib/idlelib/debugger.py index 3dcfc0b807b226..b3a4cc68f82b06 100644 --- a/Lib/idlelib/debugger.py +++ b/Lib/idlelib/debugger.py @@ -303,8 +303,8 @@ def add_stackframe(self, frame, lineno, current=False): except KeyError: selfval = None; if selfval: - # this stackframe represents an object method; preface the method - # name with the name of the class + # This stackframe represents an object method; preface the method + # name with the name of the class. if selfval.__class__.__name__ == 'str': # We've got the string representation of the object sent from # the remote debugger; parse out the name of the class, e.g. @@ -357,7 +357,7 @@ def interaction(self, message, frame, info=None): self.top.wakeup() # Nested main loop: Tkinter's main loop is not reentrant, so use # Tcl's vwait facility, which reenters the event loop until an - # event handler sets the variable we're waiting on + # event handler sets the variable we're waiting on. self.nesting_level += 1 self.root.tk.call('vwait', '::idledebugwait') self.nesting_level -= 1 @@ -372,9 +372,8 @@ def show_vars(self): sel = self.stack.selection() if len(sel) == 1 and sel[0] in self.framevars: locals, globals, _, _ = self.framevars[sel[0]] - # note: locals/globals may be from a remotedebugger, in - # which case for reasons we don't need to get into here, - # they aren't iterable + # locals/globals are normally rpc DictProxy objects, which are + # not iterable. self.add_varheader() for name in sorted(locals.keys(), key=underscore_at_end): self.add_var(name, locals[name])