Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
squeezer.py346 linesDownload Raw Back to idlelib
1"""An IDLE extension to avoid having very long texts printed in the shell.2 3A common problem in IDLE's interactive shell is printing of large amounts of4text into the shell. This makes looking at the previous history difficult.5Worse, this can cause IDLE to become very slow, even to the point of being6completely unusable.7 8This extension will automatically replace long texts with a small button.9Double-clicking this button will remove it and insert the original text instead.10Middle-clicking will copy the text to the clipboard. Right-clicking will open11the text in a separate viewing window.12 13Additionally, any output can be manually "squeezed" by the user. This includes14output written to the standard error stream ("stderr"), such as exception15messages and their tracebacks.16"""17import re18 19import tkinter as tk20from tkinter import messagebox21 22from idlelib.config import idleConf23from idlelib.textview import view_text24from idlelib.tooltip import Hovertip25from idlelib import macosx26 27 28def count_lines_with_wrapping(s, linewidth=80):29    """Count the number of lines in a given string.30 31    Lines are counted as if the string was wrapped so that lines are never over32    linewidth characters long.33 34    Tabs are considered tabwidth characters long.35    """36    tabwidth = 8  # Currently always true in Shell.37    pos = 038    linecount = 139    current_column = 040 41    for m in re.finditer(r"[\t\n]", s):42        # Process the normal chars up to tab or newline.43        numchars = m.start() - pos44        pos += numchars45        current_column += numchars46 47        # Deal with tab or newline.48        if s[pos] == '\n':49            # Avoid the `current_column == 0` edge-case, and while we're50            # at it, don't bother adding 0.51            if current_column > linewidth:52                # If the current column was exactly linewidth, divmod53                # would give (1,0), even though a new line hadn't yet54                # been started. The same is true if length is any exact55                # multiple of linewidth. Therefore, subtract 1 before56                # dividing a non-empty line.57                linecount += (current_column - 1) // linewidth58            linecount += 159            current_column = 060        else:61            assert s[pos] == '\t'62            current_column += tabwidth - (current_column % tabwidth)63 64            # If a tab passes the end of the line, consider the entire65            # tab as being on the next line.66            if current_column > linewidth:67                linecount += 168                current_column = tabwidth69 70        pos += 1 # After the tab or newline.71 72    # Process remaining chars (no more tabs or newlines).73    current_column += len(s) - pos74    # Avoid divmod(-1, linewidth).75    if current_column > 0:76        linecount += (current_column - 1) // linewidth77    else:78        # Text ended with newline; don't count an extra line after it.79        linecount -= 180 81    return linecount82 83 84class ExpandingButton(tk.Button):85    """Class for the "squeezed" text buttons used by Squeezer86 87    These buttons are displayed inside a Tk Text widget in place of text. A88    user can then use the button to replace it with the original text, copy89    the original text to the clipboard or view the original text in a separate90    window.91 92    Each button is tied to a Squeezer instance, and it knows to update the93    Squeezer instance when it is expanded (and therefore removed).94    """95    def __init__(self, s, tags, numoflines, squeezer):96        self.s = s97        self.tags = tags98        self.numoflines = numoflines99        self.squeezer = squeezer100        self.editwin = editwin = squeezer.editwin101        self.text = text = editwin.text102        # The base Text widget is needed to change text before iomark.103        self.base_text = editwin.per.bottom104 105        line_plurality = "lines" if numoflines != 1 else "line"106        button_text = f"Squeezed text ({numoflines} {line_plurality})."107        tk.Button.__init__(self, text, text=button_text,108                           background="#FFFFC0", activebackground="#FFFFE0")109 110        button_tooltip_text = (111            "Double-click to expand, right-click for more options."112        )113        Hovertip(self, button_tooltip_text, hover_delay=80)114 115        self.bind("<Double-Button-1>", self.expand)116        if macosx.isAquaTk():117            # AquaTk defines <2> as the right button, not <3>.118            self.bind("<Button-2>", self.context_menu_event)119        else:120            self.bind("<Button-3>", self.context_menu_event)121        self.selection_handle(  # X windows only.122            lambda offset, length: s[int(offset):int(offset) + int(length)])123 124        self.is_dangerous = None125        self.after_idle(self.set_is_dangerous)126 127    def set_is_dangerous(self):128        dangerous_line_len = 50 * self.text.winfo_width()129        self.is_dangerous = (130            self.numoflines > 1000 or131            len(self.s) > 50000 or132            any(133                len(line_match.group(0)) >= dangerous_line_len134                for line_match in re.finditer(r'[^\n]+', self.s)135            )136        )137 138    def expand(self, event=None):139        """expand event handler140 141        This inserts the original text in place of the button in the Text142        widget, removes the button and updates the Squeezer instance.143 144        If the original text is dangerously long, i.e. expanding it could145        cause a performance degradation, ask the user for confirmation.146        """147        if self.is_dangerous is None:148            self.set_is_dangerous()149        if self.is_dangerous:150            confirm = messagebox.askokcancel(151                title="Expand huge output?",152                message="\n\n".join([153                    "The squeezed output is very long: %d lines, %d chars.",154                    "Expanding it could make IDLE slow or unresponsive.",155                    "It is recommended to view or copy the output instead.",156                    "Really expand?"157                ]) % (self.numoflines, len(self.s)),158                default=messagebox.CANCEL,159                parent=self.text)160            if not confirm:161                return "break"162 163        index = self.text.index(self)164        self.base_text.insert(index, self.s, self.tags)165        self.base_text.delete(self)166        self.editwin.on_squeezed_expand(index, self.s, self.tags)167        self.squeezer.expandingbuttons.remove(self)168 169    def copy(self, event=None):170        """copy event handler171 172        Copy the original text to the clipboard.173        """174        self.clipboard_clear()175        self.clipboard_append(self.s)176 177    def view(self, event=None):178        """view event handler179 180        View the original text in a separate text viewer window.181        """182        view_text(self.text, "Squeezed Output Viewer", self.s,183                  modal=False, wrap='none')184 185    rmenu_specs = (186        # Item structure: (label, method_name).187        ('copy', 'copy'),188        ('view', 'view'),189    )190 191    def context_menu_event(self, event):192        self.text.mark_set("insert", "@%d,%d" % (event.x, event.y))193        rmenu = tk.Menu(self.text, tearoff=0)194        for label, method_name in self.rmenu_specs:195            rmenu.add_command(label=label, command=getattr(self, method_name))196        rmenu.tk_popup(event.x_root, event.y_root)197        return "break"198 199 200class Squeezer:201    """Replace long outputs in the shell with a simple button.202 203    This avoids IDLE's shell slowing down considerably, and even becoming204    completely unresponsive, when very long outputs are written.205    """206    @classmethod207    def reload(cls):208        """Load class variables from config."""209        cls.auto_squeeze_min_lines = idleConf.GetOption(210            "main", "PyShell", "auto-squeeze-min-lines",211            type="int", default=50,212        )213 214    def __init__(self, editwin):215        """Initialize settings for Squeezer.216 217        editwin is the shell's Editor window.218        self.text is the editor window text widget.219        self.base_test is the actual editor window Tk text widget, rather than220            EditorWindow's wrapper.221        self.expandingbuttons is the list of all buttons representing222            "squeezed" output.223        """224        self.editwin = editwin225        self.text = text = editwin.text226 227        # Get the base Text widget of the PyShell object, used to change228        # text before the iomark. PyShell deliberately disables changing229        # text before the iomark via its 'text' attribute, which is230        # actually a wrapper for the actual Text widget. Squeezer,231        # however, needs to make such changes.232        self.base_text = editwin.per.bottom233 234        # Twice the text widget's border width and internal padding;235        # pre-calculated here for the get_line_width() method.236        self.window_width_delta = 2 * (237            int(text.cget('border')) +238            int(text.cget('padx'))239        )240 241        self.expandingbuttons = []242 243        # Replace the PyShell instance's write method with a wrapper,244        # which inserts an ExpandingButton instead of a long text.245        def mywrite(s, tags=(), write=editwin.write):246            # Only auto-squeeze text which has just the "stdout" tag.247            if tags != "stdout":248                return write(s, tags)249 250            # Only auto-squeeze text with at least the minimum251            # configured number of lines.252            auto_squeeze_min_lines = self.auto_squeeze_min_lines253            # First, a very quick check to skip very short texts.254            if len(s) < auto_squeeze_min_lines:255                return write(s, tags)256            # Now the full line-count check.257            numoflines = self.count_lines(s)258            if numoflines < auto_squeeze_min_lines:259                return write(s, tags)260 261            # Create an ExpandingButton instance.262            expandingbutton = ExpandingButton(s, tags, numoflines, self)263 264            # Insert the ExpandingButton into the Text widget.265            text.mark_gravity("iomark", tk.RIGHT)266            text.window_create("iomark", window=expandingbutton,267                               padx=3, pady=5)268            text.see("iomark")269            text.update()270            text.mark_gravity("iomark", tk.LEFT)271 272            # Add the ExpandingButton to the Squeezer's list.273            self.expandingbuttons.append(expandingbutton)274 275        editwin.write = mywrite276 277    def count_lines(self, s):278        """Count the number of lines in a given text.279 280        Before calculation, the tab width and line length of the text are281        fetched, so that up-to-date values are used.282 283        Lines are counted as if the string was wrapped so that lines are never284        over linewidth characters long.285 286        Tabs are considered tabwidth characters long.287        """288        return count_lines_with_wrapping(s, self.editwin.width)289 290    def squeeze_current_text(self):291        """Squeeze the text block where the insertion cursor is.292 293        If the cursor is not in a squeezable block of text, give the294        user a small warning and do nothing.295        """296        # Set tag_name to the first valid tag found on the "insert" cursor.297        tag_names = self.text.tag_names(tk.INSERT)298        for tag_name in ("stdout", "stderr"):299            if tag_name in tag_names:300                break301        else:302            # The insert cursor doesn't have a "stdout" or "stderr" tag.303            self.text.bell()304            return "break"305 306        # Find the range to squeeze.307        start, end = self.text.tag_prevrange(tag_name, tk.INSERT + "+1c")308        s = self.text.get(start, end)309 310        # If the last char is a newline, remove it from the range.311        if len(s) > 0 and s[-1] == '\n':312            end = self.text.index("%s-1c" % end)313            s = s[:-1]314 315        # Delete the text.316        self.base_text.delete(start, end)317 318        # Prepare an ExpandingButton.319        numoflines = self.count_lines(s)320        expandingbutton = ExpandingButton(s, tag_name, numoflines, self)321 322        # insert the ExpandingButton to the Text323        self.text.window_create(start, window=expandingbutton,324                                padx=3, pady=5)325 326        # Insert the ExpandingButton to the list of ExpandingButtons,327        # while keeping the list ordered according to the position of328        # the buttons in the Text widget.329        i = len(self.expandingbuttons)330        while i > 0 and self.text.compare(self.expandingbuttons[i-1],331                                          ">", expandingbutton):332            i -= 1333        self.expandingbuttons.insert(i, expandingbutton)334 335        return "break"336 337 338Squeezer.reload()339 340 341if __name__ == "__main__":342    from unittest import main343    main('idlelib.idle_test.test_squeezer', verbosity=2, exit=False)344 345    # Add htest.346 
codekingpro/portable-devtools · Team Ai