17db96d56Sopenharmony_ci"""An IDLE extension to avoid having very long texts printed in the shell. 27db96d56Sopenharmony_ci 37db96d56Sopenharmony_ciA common problem in IDLE's interactive shell is printing of large amounts of 47db96d56Sopenharmony_citext into the shell. This makes looking at the previous history difficult. 57db96d56Sopenharmony_ciWorse, this can cause IDLE to become very slow, even to the point of being 67db96d56Sopenharmony_cicompletely unusable. 77db96d56Sopenharmony_ci 87db96d56Sopenharmony_ciThis extension will automatically replace long texts with a small button. 97db96d56Sopenharmony_ciDouble-clicking this button will remove it and insert the original text instead. 107db96d56Sopenharmony_ciMiddle-clicking will copy the text to the clipboard. Right-clicking will open 117db96d56Sopenharmony_cithe text in a separate viewing window. 127db96d56Sopenharmony_ci 137db96d56Sopenharmony_ciAdditionally, any output can be manually "squeezed" by the user. This includes 147db96d56Sopenharmony_cioutput written to the standard error stream ("stderr"), such as exception 157db96d56Sopenharmony_cimessages and their tracebacks. 167db96d56Sopenharmony_ci""" 177db96d56Sopenharmony_ciimport re 187db96d56Sopenharmony_ci 197db96d56Sopenharmony_ciimport tkinter as tk 207db96d56Sopenharmony_cifrom tkinter import messagebox 217db96d56Sopenharmony_ci 227db96d56Sopenharmony_cifrom idlelib.config import idleConf 237db96d56Sopenharmony_cifrom idlelib.textview import view_text 247db96d56Sopenharmony_cifrom idlelib.tooltip import Hovertip 257db96d56Sopenharmony_cifrom idlelib import macosx 267db96d56Sopenharmony_ci 277db96d56Sopenharmony_ci 287db96d56Sopenharmony_cidef count_lines_with_wrapping(s, linewidth=80): 297db96d56Sopenharmony_ci """Count the number of lines in a given string. 307db96d56Sopenharmony_ci 317db96d56Sopenharmony_ci Lines are counted as if the string was wrapped so that lines are never over 327db96d56Sopenharmony_ci linewidth characters long. 337db96d56Sopenharmony_ci 347db96d56Sopenharmony_ci Tabs are considered tabwidth characters long. 357db96d56Sopenharmony_ci """ 367db96d56Sopenharmony_ci tabwidth = 8 # Currently always true in Shell. 377db96d56Sopenharmony_ci pos = 0 387db96d56Sopenharmony_ci linecount = 1 397db96d56Sopenharmony_ci current_column = 0 407db96d56Sopenharmony_ci 417db96d56Sopenharmony_ci for m in re.finditer(r"[\t\n]", s): 427db96d56Sopenharmony_ci # Process the normal chars up to tab or newline. 437db96d56Sopenharmony_ci numchars = m.start() - pos 447db96d56Sopenharmony_ci pos += numchars 457db96d56Sopenharmony_ci current_column += numchars 467db96d56Sopenharmony_ci 477db96d56Sopenharmony_ci # Deal with tab or newline. 487db96d56Sopenharmony_ci if s[pos] == '\n': 497db96d56Sopenharmony_ci # Avoid the `current_column == 0` edge-case, and while we're 507db96d56Sopenharmony_ci # at it, don't bother adding 0. 517db96d56Sopenharmony_ci if current_column > linewidth: 527db96d56Sopenharmony_ci # If the current column was exactly linewidth, divmod 537db96d56Sopenharmony_ci # would give (1,0), even though a new line hadn't yet 547db96d56Sopenharmony_ci # been started. The same is true if length is any exact 557db96d56Sopenharmony_ci # multiple of linewidth. Therefore, subtract 1 before 567db96d56Sopenharmony_ci # dividing a non-empty line. 577db96d56Sopenharmony_ci linecount += (current_column - 1) // linewidth 587db96d56Sopenharmony_ci linecount += 1 597db96d56Sopenharmony_ci current_column = 0 607db96d56Sopenharmony_ci else: 617db96d56Sopenharmony_ci assert s[pos] == '\t' 627db96d56Sopenharmony_ci current_column += tabwidth - (current_column % tabwidth) 637db96d56Sopenharmony_ci 647db96d56Sopenharmony_ci # If a tab passes the end of the line, consider the entire 657db96d56Sopenharmony_ci # tab as being on the next line. 667db96d56Sopenharmony_ci if current_column > linewidth: 677db96d56Sopenharmony_ci linecount += 1 687db96d56Sopenharmony_ci current_column = tabwidth 697db96d56Sopenharmony_ci 707db96d56Sopenharmony_ci pos += 1 # After the tab or newline. 717db96d56Sopenharmony_ci 727db96d56Sopenharmony_ci # Process remaining chars (no more tabs or newlines). 737db96d56Sopenharmony_ci current_column += len(s) - pos 747db96d56Sopenharmony_ci # Avoid divmod(-1, linewidth). 757db96d56Sopenharmony_ci if current_column > 0: 767db96d56Sopenharmony_ci linecount += (current_column - 1) // linewidth 777db96d56Sopenharmony_ci else: 787db96d56Sopenharmony_ci # Text ended with newline; don't count an extra line after it. 797db96d56Sopenharmony_ci linecount -= 1 807db96d56Sopenharmony_ci 817db96d56Sopenharmony_ci return linecount 827db96d56Sopenharmony_ci 837db96d56Sopenharmony_ci 847db96d56Sopenharmony_ciclass ExpandingButton(tk.Button): 857db96d56Sopenharmony_ci """Class for the "squeezed" text buttons used by Squeezer 867db96d56Sopenharmony_ci 877db96d56Sopenharmony_ci These buttons are displayed inside a Tk Text widget in place of text. A 887db96d56Sopenharmony_ci user can then use the button to replace it with the original text, copy 897db96d56Sopenharmony_ci the original text to the clipboard or view the original text in a separate 907db96d56Sopenharmony_ci window. 917db96d56Sopenharmony_ci 927db96d56Sopenharmony_ci Each button is tied to a Squeezer instance, and it knows to update the 937db96d56Sopenharmony_ci Squeezer instance when it is expanded (and therefore removed). 947db96d56Sopenharmony_ci """ 957db96d56Sopenharmony_ci def __init__(self, s, tags, numoflines, squeezer): 967db96d56Sopenharmony_ci self.s = s 977db96d56Sopenharmony_ci self.tags = tags 987db96d56Sopenharmony_ci self.numoflines = numoflines 997db96d56Sopenharmony_ci self.squeezer = squeezer 1007db96d56Sopenharmony_ci self.editwin = editwin = squeezer.editwin 1017db96d56Sopenharmony_ci self.text = text = editwin.text 1027db96d56Sopenharmony_ci # The base Text widget is needed to change text before iomark. 1037db96d56Sopenharmony_ci self.base_text = editwin.per.bottom 1047db96d56Sopenharmony_ci 1057db96d56Sopenharmony_ci line_plurality = "lines" if numoflines != 1 else "line" 1067db96d56Sopenharmony_ci button_text = f"Squeezed text ({numoflines} {line_plurality})." 1077db96d56Sopenharmony_ci tk.Button.__init__(self, text, text=button_text, 1087db96d56Sopenharmony_ci background="#FFFFC0", activebackground="#FFFFE0") 1097db96d56Sopenharmony_ci 1107db96d56Sopenharmony_ci button_tooltip_text = ( 1117db96d56Sopenharmony_ci "Double-click to expand, right-click for more options." 1127db96d56Sopenharmony_ci ) 1137db96d56Sopenharmony_ci Hovertip(self, button_tooltip_text, hover_delay=80) 1147db96d56Sopenharmony_ci 1157db96d56Sopenharmony_ci self.bind("<Double-Button-1>", self.expand) 1167db96d56Sopenharmony_ci if macosx.isAquaTk(): 1177db96d56Sopenharmony_ci # AquaTk defines <2> as the right button, not <3>. 1187db96d56Sopenharmony_ci self.bind("<Button-2>", self.context_menu_event) 1197db96d56Sopenharmony_ci else: 1207db96d56Sopenharmony_ci self.bind("<Button-3>", self.context_menu_event) 1217db96d56Sopenharmony_ci self.selection_handle( # X windows only. 1227db96d56Sopenharmony_ci lambda offset, length: s[int(offset):int(offset) + int(length)]) 1237db96d56Sopenharmony_ci 1247db96d56Sopenharmony_ci self.is_dangerous = None 1257db96d56Sopenharmony_ci self.after_idle(self.set_is_dangerous) 1267db96d56Sopenharmony_ci 1277db96d56Sopenharmony_ci def set_is_dangerous(self): 1287db96d56Sopenharmony_ci dangerous_line_len = 50 * self.text.winfo_width() 1297db96d56Sopenharmony_ci self.is_dangerous = ( 1307db96d56Sopenharmony_ci self.numoflines > 1000 or 1317db96d56Sopenharmony_ci len(self.s) > 50000 or 1327db96d56Sopenharmony_ci any( 1337db96d56Sopenharmony_ci len(line_match.group(0)) >= dangerous_line_len 1347db96d56Sopenharmony_ci for line_match in re.finditer(r'[^\n]+', self.s) 1357db96d56Sopenharmony_ci ) 1367db96d56Sopenharmony_ci ) 1377db96d56Sopenharmony_ci 1387db96d56Sopenharmony_ci def expand(self, event=None): 1397db96d56Sopenharmony_ci """expand event handler 1407db96d56Sopenharmony_ci 1417db96d56Sopenharmony_ci This inserts the original text in place of the button in the Text 1427db96d56Sopenharmony_ci widget, removes the button and updates the Squeezer instance. 1437db96d56Sopenharmony_ci 1447db96d56Sopenharmony_ci If the original text is dangerously long, i.e. expanding it could 1457db96d56Sopenharmony_ci cause a performance degradation, ask the user for confirmation. 1467db96d56Sopenharmony_ci """ 1477db96d56Sopenharmony_ci if self.is_dangerous is None: 1487db96d56Sopenharmony_ci self.set_is_dangerous() 1497db96d56Sopenharmony_ci if self.is_dangerous: 1507db96d56Sopenharmony_ci confirm = messagebox.askokcancel( 1517db96d56Sopenharmony_ci title="Expand huge output?", 1527db96d56Sopenharmony_ci message="\n\n".join([ 1537db96d56Sopenharmony_ci "The squeezed output is very long: %d lines, %d chars.", 1547db96d56Sopenharmony_ci "Expanding it could make IDLE slow or unresponsive.", 1557db96d56Sopenharmony_ci "It is recommended to view or copy the output instead.", 1567db96d56Sopenharmony_ci "Really expand?" 1577db96d56Sopenharmony_ci ]) % (self.numoflines, len(self.s)), 1587db96d56Sopenharmony_ci default=messagebox.CANCEL, 1597db96d56Sopenharmony_ci parent=self.text) 1607db96d56Sopenharmony_ci if not confirm: 1617db96d56Sopenharmony_ci return "break" 1627db96d56Sopenharmony_ci 1637db96d56Sopenharmony_ci index = self.text.index(self) 1647db96d56Sopenharmony_ci self.base_text.insert(index, self.s, self.tags) 1657db96d56Sopenharmony_ci self.base_text.delete(self) 1667db96d56Sopenharmony_ci self.editwin.on_squeezed_expand(index, self.s, self.tags) 1677db96d56Sopenharmony_ci self.squeezer.expandingbuttons.remove(self) 1687db96d56Sopenharmony_ci 1697db96d56Sopenharmony_ci def copy(self, event=None): 1707db96d56Sopenharmony_ci """copy event handler 1717db96d56Sopenharmony_ci 1727db96d56Sopenharmony_ci Copy the original text to the clipboard. 1737db96d56Sopenharmony_ci """ 1747db96d56Sopenharmony_ci self.clipboard_clear() 1757db96d56Sopenharmony_ci self.clipboard_append(self.s) 1767db96d56Sopenharmony_ci 1777db96d56Sopenharmony_ci def view(self, event=None): 1787db96d56Sopenharmony_ci """view event handler 1797db96d56Sopenharmony_ci 1807db96d56Sopenharmony_ci View the original text in a separate text viewer window. 1817db96d56Sopenharmony_ci """ 1827db96d56Sopenharmony_ci view_text(self.text, "Squeezed Output Viewer", self.s, 1837db96d56Sopenharmony_ci modal=False, wrap='none') 1847db96d56Sopenharmony_ci 1857db96d56Sopenharmony_ci rmenu_specs = ( 1867db96d56Sopenharmony_ci # Item structure: (label, method_name). 1877db96d56Sopenharmony_ci ('copy', 'copy'), 1887db96d56Sopenharmony_ci ('view', 'view'), 1897db96d56Sopenharmony_ci ) 1907db96d56Sopenharmony_ci 1917db96d56Sopenharmony_ci def context_menu_event(self, event): 1927db96d56Sopenharmony_ci self.text.mark_set("insert", "@%d,%d" % (event.x, event.y)) 1937db96d56Sopenharmony_ci rmenu = tk.Menu(self.text, tearoff=0) 1947db96d56Sopenharmony_ci for label, method_name in self.rmenu_specs: 1957db96d56Sopenharmony_ci rmenu.add_command(label=label, command=getattr(self, method_name)) 1967db96d56Sopenharmony_ci rmenu.tk_popup(event.x_root, event.y_root) 1977db96d56Sopenharmony_ci return "break" 1987db96d56Sopenharmony_ci 1997db96d56Sopenharmony_ci 2007db96d56Sopenharmony_ciclass Squeezer: 2017db96d56Sopenharmony_ci """Replace long outputs in the shell with a simple button. 2027db96d56Sopenharmony_ci 2037db96d56Sopenharmony_ci This avoids IDLE's shell slowing down considerably, and even becoming 2047db96d56Sopenharmony_ci completely unresponsive, when very long outputs are written. 2057db96d56Sopenharmony_ci """ 2067db96d56Sopenharmony_ci @classmethod 2077db96d56Sopenharmony_ci def reload(cls): 2087db96d56Sopenharmony_ci """Load class variables from config.""" 2097db96d56Sopenharmony_ci cls.auto_squeeze_min_lines = idleConf.GetOption( 2107db96d56Sopenharmony_ci "main", "PyShell", "auto-squeeze-min-lines", 2117db96d56Sopenharmony_ci type="int", default=50, 2127db96d56Sopenharmony_ci ) 2137db96d56Sopenharmony_ci 2147db96d56Sopenharmony_ci def __init__(self, editwin): 2157db96d56Sopenharmony_ci """Initialize settings for Squeezer. 2167db96d56Sopenharmony_ci 2177db96d56Sopenharmony_ci editwin is the shell's Editor window. 2187db96d56Sopenharmony_ci self.text is the editor window text widget. 2197db96d56Sopenharmony_ci self.base_test is the actual editor window Tk text widget, rather than 2207db96d56Sopenharmony_ci EditorWindow's wrapper. 2217db96d56Sopenharmony_ci self.expandingbuttons is the list of all buttons representing 2227db96d56Sopenharmony_ci "squeezed" output. 2237db96d56Sopenharmony_ci """ 2247db96d56Sopenharmony_ci self.editwin = editwin 2257db96d56Sopenharmony_ci self.text = text = editwin.text 2267db96d56Sopenharmony_ci 2277db96d56Sopenharmony_ci # Get the base Text widget of the PyShell object, used to change 2287db96d56Sopenharmony_ci # text before the iomark. PyShell deliberately disables changing 2297db96d56Sopenharmony_ci # text before the iomark via its 'text' attribute, which is 2307db96d56Sopenharmony_ci # actually a wrapper for the actual Text widget. Squeezer, 2317db96d56Sopenharmony_ci # however, needs to make such changes. 2327db96d56Sopenharmony_ci self.base_text = editwin.per.bottom 2337db96d56Sopenharmony_ci 2347db96d56Sopenharmony_ci # Twice the text widget's border width and internal padding; 2357db96d56Sopenharmony_ci # pre-calculated here for the get_line_width() method. 2367db96d56Sopenharmony_ci self.window_width_delta = 2 * ( 2377db96d56Sopenharmony_ci int(text.cget('border')) + 2387db96d56Sopenharmony_ci int(text.cget('padx')) 2397db96d56Sopenharmony_ci ) 2407db96d56Sopenharmony_ci 2417db96d56Sopenharmony_ci self.expandingbuttons = [] 2427db96d56Sopenharmony_ci 2437db96d56Sopenharmony_ci # Replace the PyShell instance's write method with a wrapper, 2447db96d56Sopenharmony_ci # which inserts an ExpandingButton instead of a long text. 2457db96d56Sopenharmony_ci def mywrite(s, tags=(), write=editwin.write): 2467db96d56Sopenharmony_ci # Only auto-squeeze text which has just the "stdout" tag. 2477db96d56Sopenharmony_ci if tags != "stdout": 2487db96d56Sopenharmony_ci return write(s, tags) 2497db96d56Sopenharmony_ci 2507db96d56Sopenharmony_ci # Only auto-squeeze text with at least the minimum 2517db96d56Sopenharmony_ci # configured number of lines. 2527db96d56Sopenharmony_ci auto_squeeze_min_lines = self.auto_squeeze_min_lines 2537db96d56Sopenharmony_ci # First, a very quick check to skip very short texts. 2547db96d56Sopenharmony_ci if len(s) < auto_squeeze_min_lines: 2557db96d56Sopenharmony_ci return write(s, tags) 2567db96d56Sopenharmony_ci # Now the full line-count check. 2577db96d56Sopenharmony_ci numoflines = self.count_lines(s) 2587db96d56Sopenharmony_ci if numoflines < auto_squeeze_min_lines: 2597db96d56Sopenharmony_ci return write(s, tags) 2607db96d56Sopenharmony_ci 2617db96d56Sopenharmony_ci # Create an ExpandingButton instance. 2627db96d56Sopenharmony_ci expandingbutton = ExpandingButton(s, tags, numoflines, self) 2637db96d56Sopenharmony_ci 2647db96d56Sopenharmony_ci # Insert the ExpandingButton into the Text widget. 2657db96d56Sopenharmony_ci text.mark_gravity("iomark", tk.RIGHT) 2667db96d56Sopenharmony_ci text.window_create("iomark", window=expandingbutton, 2677db96d56Sopenharmony_ci padx=3, pady=5) 2687db96d56Sopenharmony_ci text.see("iomark") 2697db96d56Sopenharmony_ci text.update() 2707db96d56Sopenharmony_ci text.mark_gravity("iomark", tk.LEFT) 2717db96d56Sopenharmony_ci 2727db96d56Sopenharmony_ci # Add the ExpandingButton to the Squeezer's list. 2737db96d56Sopenharmony_ci self.expandingbuttons.append(expandingbutton) 2747db96d56Sopenharmony_ci 2757db96d56Sopenharmony_ci editwin.write = mywrite 2767db96d56Sopenharmony_ci 2777db96d56Sopenharmony_ci def count_lines(self, s): 2787db96d56Sopenharmony_ci """Count the number of lines in a given text. 2797db96d56Sopenharmony_ci 2807db96d56Sopenharmony_ci Before calculation, the tab width and line length of the text are 2817db96d56Sopenharmony_ci fetched, so that up-to-date values are used. 2827db96d56Sopenharmony_ci 2837db96d56Sopenharmony_ci Lines are counted as if the string was wrapped so that lines are never 2847db96d56Sopenharmony_ci over linewidth characters long. 2857db96d56Sopenharmony_ci 2867db96d56Sopenharmony_ci Tabs are considered tabwidth characters long. 2877db96d56Sopenharmony_ci """ 2887db96d56Sopenharmony_ci return count_lines_with_wrapping(s, self.editwin.width) 2897db96d56Sopenharmony_ci 2907db96d56Sopenharmony_ci def squeeze_current_text(self): 2917db96d56Sopenharmony_ci """Squeeze the text block where the insertion cursor is. 2927db96d56Sopenharmony_ci 2937db96d56Sopenharmony_ci If the cursor is not in a squeezable block of text, give the 2947db96d56Sopenharmony_ci user a small warning and do nothing. 2957db96d56Sopenharmony_ci """ 2967db96d56Sopenharmony_ci # Set tag_name to the first valid tag found on the "insert" cursor. 2977db96d56Sopenharmony_ci tag_names = self.text.tag_names(tk.INSERT) 2987db96d56Sopenharmony_ci for tag_name in ("stdout", "stderr"): 2997db96d56Sopenharmony_ci if tag_name in tag_names: 3007db96d56Sopenharmony_ci break 3017db96d56Sopenharmony_ci else: 3027db96d56Sopenharmony_ci # The insert cursor doesn't have a "stdout" or "stderr" tag. 3037db96d56Sopenharmony_ci self.text.bell() 3047db96d56Sopenharmony_ci return "break" 3057db96d56Sopenharmony_ci 3067db96d56Sopenharmony_ci # Find the range to squeeze. 3077db96d56Sopenharmony_ci start, end = self.text.tag_prevrange(tag_name, tk.INSERT + "+1c") 3087db96d56Sopenharmony_ci s = self.text.get(start, end) 3097db96d56Sopenharmony_ci 3107db96d56Sopenharmony_ci # If the last char is a newline, remove it from the range. 3117db96d56Sopenharmony_ci if len(s) > 0 and s[-1] == '\n': 3127db96d56Sopenharmony_ci end = self.text.index("%s-1c" % end) 3137db96d56Sopenharmony_ci s = s[:-1] 3147db96d56Sopenharmony_ci 3157db96d56Sopenharmony_ci # Delete the text. 3167db96d56Sopenharmony_ci self.base_text.delete(start, end) 3177db96d56Sopenharmony_ci 3187db96d56Sopenharmony_ci # Prepare an ExpandingButton. 3197db96d56Sopenharmony_ci numoflines = self.count_lines(s) 3207db96d56Sopenharmony_ci expandingbutton = ExpandingButton(s, tag_name, numoflines, self) 3217db96d56Sopenharmony_ci 3227db96d56Sopenharmony_ci # insert the ExpandingButton to the Text 3237db96d56Sopenharmony_ci self.text.window_create(start, window=expandingbutton, 3247db96d56Sopenharmony_ci padx=3, pady=5) 3257db96d56Sopenharmony_ci 3267db96d56Sopenharmony_ci # Insert the ExpandingButton to the list of ExpandingButtons, 3277db96d56Sopenharmony_ci # while keeping the list ordered according to the position of 3287db96d56Sopenharmony_ci # the buttons in the Text widget. 3297db96d56Sopenharmony_ci i = len(self.expandingbuttons) 3307db96d56Sopenharmony_ci while i > 0 and self.text.compare(self.expandingbuttons[i-1], 3317db96d56Sopenharmony_ci ">", expandingbutton): 3327db96d56Sopenharmony_ci i -= 1 3337db96d56Sopenharmony_ci self.expandingbuttons.insert(i, expandingbutton) 3347db96d56Sopenharmony_ci 3357db96d56Sopenharmony_ci return "break" 3367db96d56Sopenharmony_ci 3377db96d56Sopenharmony_ci 3387db96d56Sopenharmony_ciSqueezer.reload() 3397db96d56Sopenharmony_ci 3407db96d56Sopenharmony_ci 3417db96d56Sopenharmony_ciif __name__ == "__main__": 3427db96d56Sopenharmony_ci from unittest import main 3437db96d56Sopenharmony_ci main('idlelib.idle_test.test_squeezer', verbosity=2, exit=False) 3447db96d56Sopenharmony_ci 3457db96d56Sopenharmony_ci # Add htest. 346