diff options
Diffstat (limited to 'Lib/idlelib/squeezer.py')
-rw-r--r-- | Lib/idlelib/squeezer.py | 345 |
1 files changed, 0 insertions, 345 deletions
diff --git a/Lib/idlelib/squeezer.py b/Lib/idlelib/squeezer.py deleted file mode 100644 index be1538a..0000000 --- a/Lib/idlelib/squeezer.py +++ /dev/null @@ -1,345 +0,0 @@ -"""An IDLE extension to avoid having very long texts printed in the shell. - -A common problem in IDLE's interactive shell is printing of large amounts of -text into the shell. This makes looking at the previous history difficult. -Worse, this can cause IDLE to become very slow, even to the point of being -completely unusable. - -This extension will automatically replace long texts with a small button. -Double-clicking this button will remove it and insert the original text instead. -Middle-clicking will copy the text to the clipboard. Right-clicking will open -the text in a separate viewing window. - -Additionally, any output can be manually "squeezed" by the user. This includes -output written to the standard error stream ("stderr"), such as exception -messages and their tracebacks. -""" -import re - -import tkinter as tk -import tkinter.messagebox as tkMessageBox - -from idlelib.config import idleConf -from idlelib.textview import view_text -from idlelib.tooltip import Hovertip -from idlelib import macosx - - -def count_lines_with_wrapping(s, linewidth=80): - """Count the number of lines in a given string. - - Lines are counted as if the string was wrapped so that lines are never over - linewidth characters long. - - Tabs are considered tabwidth characters long. - """ - tabwidth = 8 # Currently always true in Shell. - pos = 0 - linecount = 1 - current_column = 0 - - for m in re.finditer(r"[\t\n]", s): - # Process the normal chars up to tab or newline. - numchars = m.start() - pos - pos += numchars - current_column += numchars - - # Deal with tab or newline. - if s[pos] == '\n': - # Avoid the `current_column == 0` edge-case, and while we're - # at it, don't bother adding 0. - if current_column > linewidth: - # If the current column was exactly linewidth, divmod - # would give (1,0), even though a new line hadn't yet - # been started. The same is true if length is any exact - # multiple of linewidth. Therefore, subtract 1 before - # dividing a non-empty line. - linecount += (current_column - 1) // linewidth - linecount += 1 - current_column = 0 - else: - assert s[pos] == '\t' - current_column += tabwidth - (current_column % tabwidth) - - # If a tab passes the end of the line, consider the entire - # tab as being on the next line. - if current_column > linewidth: - linecount += 1 - current_column = tabwidth - - pos += 1 # After the tab or newline. - - # Process remaining chars (no more tabs or newlines). - current_column += len(s) - pos - # Avoid divmod(-1, linewidth). - if current_column > 0: - linecount += (current_column - 1) // linewidth - else: - # Text ended with newline; don't count an extra line after it. - linecount -= 1 - - return linecount - - -class ExpandingButton(tk.Button): - """Class for the "squeezed" text buttons used by Squeezer - - These buttons are displayed inside a Tk Text widget in place of text. A - user can then use the button to replace it with the original text, copy - the original text to the clipboard or view the original text in a separate - window. - - Each button is tied to a Squeezer instance, and it knows to update the - Squeezer instance when it is expanded (and therefore removed). - """ - def __init__(self, s, tags, numoflines, squeezer): - self.s = s - self.tags = tags - self.numoflines = numoflines - self.squeezer = squeezer - self.editwin = editwin = squeezer.editwin - self.text = text = editwin.text - # The base Text widget is needed to change text before iomark. - self.base_text = editwin.per.bottom - - line_plurality = "lines" if numoflines != 1 else "line" - button_text = f"Squeezed text ({numoflines} {line_plurality})." - tk.Button.__init__(self, text, text=button_text, - background="#FFFFC0", activebackground="#FFFFE0") - - button_tooltip_text = ( - "Double-click to expand, right-click for more options." - ) - Hovertip(self, button_tooltip_text, hover_delay=80) - - self.bind("<Double-Button-1>", self.expand) - if macosx.isAquaTk(): - # AquaTk defines <2> as the right button, not <3>. - self.bind("<Button-2>", self.context_menu_event) - else: - self.bind("<Button-3>", self.context_menu_event) - self.selection_handle( # X windows only. - lambda offset, length: s[int(offset):int(offset) + int(length)]) - - self.is_dangerous = None - self.after_idle(self.set_is_dangerous) - - def set_is_dangerous(self): - dangerous_line_len = 50 * self.text.winfo_width() - self.is_dangerous = ( - self.numoflines > 1000 or - len(self.s) > 50000 or - any( - len(line_match.group(0)) >= dangerous_line_len - for line_match in re.finditer(r'[^\n]+', self.s) - ) - ) - - def expand(self, event=None): - """expand event handler - - This inserts the original text in place of the button in the Text - widget, removes the button and updates the Squeezer instance. - - If the original text is dangerously long, i.e. expanding it could - cause a performance degradation, ask the user for confirmation. - """ - if self.is_dangerous is None: - self.set_is_dangerous() - if self.is_dangerous: - confirm = tkMessageBox.askokcancel( - title="Expand huge output?", - message="\n\n".join([ - "The squeezed output is very long: %d lines, %d chars.", - "Expanding it could make IDLE slow or unresponsive.", - "It is recommended to view or copy the output instead.", - "Really expand?" - ]) % (self.numoflines, len(self.s)), - default=tkMessageBox.CANCEL, - parent=self.text) - if not confirm: - return "break" - - self.base_text.insert(self.text.index(self), self.s, self.tags) - self.base_text.delete(self) - self.squeezer.expandingbuttons.remove(self) - - def copy(self, event=None): - """copy event handler - - Copy the original text to the clipboard. - """ - self.clipboard_clear() - self.clipboard_append(self.s) - - def view(self, event=None): - """view event handler - - View the original text in a separate text viewer window. - """ - view_text(self.text, "Squeezed Output Viewer", self.s, - modal=False, wrap='none') - - rmenu_specs = ( - # Item structure: (label, method_name). - ('copy', 'copy'), - ('view', 'view'), - ) - - def context_menu_event(self, event): - self.text.mark_set("insert", "@%d,%d" % (event.x, event.y)) - rmenu = tk.Menu(self.text, tearoff=0) - for label, method_name in self.rmenu_specs: - rmenu.add_command(label=label, command=getattr(self, method_name)) - rmenu.tk_popup(event.x_root, event.y_root) - return "break" - - -class Squeezer: - """Replace long outputs in the shell with a simple button. - - This avoids IDLE's shell slowing down considerably, and even becoming - completely unresponsive, when very long outputs are written. - """ - @classmethod - def reload(cls): - """Load class variables from config.""" - cls.auto_squeeze_min_lines = idleConf.GetOption( - "main", "PyShell", "auto-squeeze-min-lines", - type="int", default=50, - ) - - def __init__(self, editwin): - """Initialize settings for Squeezer. - - editwin is the shell's Editor window. - self.text is the editor window text widget. - self.base_test is the actual editor window Tk text widget, rather than - EditorWindow's wrapper. - self.expandingbuttons is the list of all buttons representing - "squeezed" output. - """ - self.editwin = editwin - self.text = text = editwin.text - - # Get the base Text widget of the PyShell object, used to change - # text before the iomark. PyShell deliberately disables changing - # text before the iomark via its 'text' attribute, which is - # actually a wrapper for the actual Text widget. Squeezer, - # however, needs to make such changes. - self.base_text = editwin.per.bottom - - # Twice the text widget's border width and internal padding; - # pre-calculated here for the get_line_width() method. - self.window_width_delta = 2 * ( - int(text.cget('border')) + - int(text.cget('padx')) - ) - - self.expandingbuttons = [] - - # Replace the PyShell instance's write method with a wrapper, - # which inserts an ExpandingButton instead of a long text. - def mywrite(s, tags=(), write=editwin.write): - # Only auto-squeeze text which has just the "stdout" tag. - if tags != "stdout": - return write(s, tags) - - # Only auto-squeeze text with at least the minimum - # configured number of lines. - auto_squeeze_min_lines = self.auto_squeeze_min_lines - # First, a very quick check to skip very short texts. - if len(s) < auto_squeeze_min_lines: - return write(s, tags) - # Now the full line-count check. - numoflines = self.count_lines(s) - if numoflines < auto_squeeze_min_lines: - return write(s, tags) - - # Create an ExpandingButton instance. - expandingbutton = ExpandingButton(s, tags, numoflines, self) - - # Insert the ExpandingButton into the Text widget. - text.mark_gravity("iomark", tk.RIGHT) - text.window_create("iomark", window=expandingbutton, - padx=3, pady=5) - text.see("iomark") - text.update() - text.mark_gravity("iomark", tk.LEFT) - - # Add the ExpandingButton to the Squeezer's list. - self.expandingbuttons.append(expandingbutton) - - editwin.write = mywrite - - def count_lines(self, s): - """Count the number of lines in a given text. - - Before calculation, the tab width and line length of the text are - fetched, so that up-to-date values are used. - - Lines are counted as if the string was wrapped so that lines are never - over linewidth characters long. - - Tabs are considered tabwidth characters long. - """ - return count_lines_with_wrapping(s, self.editwin.width) - - def squeeze_current_text_event(self, event): - """squeeze-current-text event handler - - Squeeze the block of text inside which contains the "insert" cursor. - - If the insert cursor is not in a squeezable block of text, give the - user a small warning and do nothing. - """ - # Set tag_name to the first valid tag found on the "insert" cursor. - tag_names = self.text.tag_names(tk.INSERT) - for tag_name in ("stdout", "stderr"): - if tag_name in tag_names: - break - else: - # The insert cursor doesn't have a "stdout" or "stderr" tag. - self.text.bell() - return "break" - - # Find the range to squeeze. - start, end = self.text.tag_prevrange(tag_name, tk.INSERT + "+1c") - s = self.text.get(start, end) - - # If the last char is a newline, remove it from the range. - if len(s) > 0 and s[-1] == '\n': - end = self.text.index("%s-1c" % end) - s = s[:-1] - - # Delete the text. - self.base_text.delete(start, end) - - # Prepare an ExpandingButton. - numoflines = self.count_lines(s) - expandingbutton = ExpandingButton(s, tag_name, numoflines, self) - - # insert the ExpandingButton to the Text - self.text.window_create(start, window=expandingbutton, - padx=3, pady=5) - - # Insert the ExpandingButton to the list of ExpandingButtons, - # while keeping the list ordered according to the position of - # the buttons in the Text widget. - i = len(self.expandingbuttons) - while i > 0 and self.text.compare(self.expandingbuttons[i-1], - ">", expandingbutton): - i -= 1 - self.expandingbuttons.insert(i, expandingbutton) - - return "break" - - -Squeezer.reload() - - -if __name__ == "__main__": - from unittest import main - main('idlelib.idle_test.test_squeezer', verbosity=2, exit=False) - - # Add htest. |