diff CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/squeezer.py @ 69:33d812a61356

planemo upload commit 2e9511a184a1ca667c7be0c6321a36dc4e3d116d
author jpayne
date Tue, 18 Mar 2025 17:55:14 -0400
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/squeezer.py	Tue Mar 18 17:55:14 2025 -0400
@@ -0,0 +1,345 @@
+"""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.