diff options
author | Tal Einat <taleinat+github@gmail.com> | 2019-01-13 17:01:50 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-01-13 17:01:50 +0200 |
commit | 39a33e99270848d34628cdbb1fdb727f9ede502a (patch) | |
tree | 37e17a396741a0a98790b2bbcd6a24e2e730adca /Lib/idlelib/squeezer.py | |
parent | 995d9b92979768125ced4da3a56f755bcdf80f6e (diff) | |
download | cpython-git-39a33e99270848d34628cdbb1fdb727f9ede502a.tar.gz |
bpo-35196: Optimize Squeezer's write() interception (GH-10454)
The new functionality of Squeezer.reload() is also tested, along with some general
re-working of the tests in test_squeezer.py.
Diffstat (limited to 'Lib/idlelib/squeezer.py')
-rw-r--r-- | Lib/idlelib/squeezer.py | 190 |
1 files changed, 107 insertions, 83 deletions
diff --git a/Lib/idlelib/squeezer.py b/Lib/idlelib/squeezer.py index 8960356799..869498d753 100644 --- a/Lib/idlelib/squeezer.py +++ b/Lib/idlelib/squeezer.py @@ -15,6 +15,7 @@ output written to the standard error stream ("stderr"), such as exception messages and their tracebacks. """ import re +import weakref import tkinter as tk from tkinter.font import Font @@ -26,7 +27,7 @@ from idlelib.tooltip import Hovertip from idlelib import macosx -def count_lines_with_wrapping(s, linewidth=80, tabwidth=8): +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 @@ -34,25 +35,27 @@ def count_lines_with_wrapping(s, linewidth=80, tabwidth=8): 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 + # Process the normal chars up to tab or newline. numchars = m.start() - pos pos += numchars current_column += numchars - # deal with tab or newline + # 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. + # 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. + # 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 @@ -60,21 +63,21 @@ def count_lines_with_wrapping(s, linewidth=80, tabwidth=8): 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 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 + pos += 1 # After the tab or newline. - # process remaining chars (no more tabs or newlines) + # Process remaining chars (no more tabs or newlines). current_column += len(s) - pos - # avoid divmod(-1, linewidth) + # Avoid divmod(-1, linewidth). if current_column > 0: linecount += (current_column - 1) // linewidth else: - # the text ended with a newline; don't count an extra line after it + # Text ended with newline; don't count an extra line after it. linecount -= 1 return linecount @@ -98,9 +101,7 @@ class ExpandingButton(tk.Button): self.squeezer = squeezer self.editwin = editwin = squeezer.editwin self.text = text = editwin.text - - # the base Text widget of the PyShell object, used to change text - # before the iomark + # 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" @@ -119,7 +120,7 @@ class ExpandingButton(tk.Button): self.bind("<Button-2>", self.context_menu_event) else: self.bind("<Button-3>", self.context_menu_event) - self.selection_handle( + self.selection_handle( # X windows only. lambda offset, length: s[int(offset):int(offset) + int(length)]) self.is_dangerous = None @@ -182,7 +183,7 @@ class ExpandingButton(tk.Button): modal=False, wrap='none') rmenu_specs = ( - # item structure: (label, method_name) + # Item structure: (label, method_name). ('copy', 'copy'), ('view', 'view'), ) @@ -202,6 +203,8 @@ class Squeezer: This avoids IDLE's shell slowing down considerably, and even becoming completely unresponsive, when very long outputs are written. """ + _instance_weakref = None + @classmethod def reload(cls): """Load class variables from config.""" @@ -210,6 +213,14 @@ class Squeezer: type="int", default=50, ) + # Loading the font info requires a Tk root. IDLE doesn't rely + # on Tkinter's "default root", so the instance will reload + # font info using its editor windows's Tk root. + if cls._instance_weakref is not None: + instance = cls._instance_weakref() + if instance is not None: + instance.load_font() + def __init__(self, editwin): """Initialize settings for Squeezer. @@ -223,44 +234,58 @@ class Squeezer: 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. + # 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 + Squeezer._instance_weakref = weakref.ref(self) + self.load_font() + + # 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 = [] - from idlelib.pyshell import PyShell # done here to avoid import cycle - if isinstance(editwin, PyShell): - # If we get a PyShell instance, replace its 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 - numoflines = self.count_lines(s) - if numoflines < self.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 + + # 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. @@ -273,25 +298,24 @@ class Squeezer: Tabs are considered tabwidth characters long. """ - # Tab width is configurable - tabwidth = self.editwin.get_tk_tabwidth() - - # Get the Text widget's size - linewidth = self.editwin.text.winfo_width() - # Deduct the border and padding - linewidth -= 2*sum([int(self.editwin.text.cget(opt)) - for opt in ('border', 'padx')]) - - # Get the Text widget's font - font = Font(self.editwin.text, name=self.editwin.text.cget('font')) - # Divide the size of the Text widget by the font's width. - # According to Tk8.5 docs, the Text widget's width is set - # according to the width of its font's '0' (zero) character, - # so we will use this as an approximation. - # see: http://www.tcl.tk/man/tcl8.5/TkCmd/text.htm#M-width - linewidth //= font.measure('0') - - return count_lines_with_wrapping(s, linewidth, tabwidth) + linewidth = self.get_line_width() + return count_lines_with_wrapping(s, linewidth) + + def get_line_width(self): + # The maximum line length in pixels: The width of the text + # widget, minus twice the border width and internal padding. + linewidth_pixels = \ + self.base_text.winfo_width() - self.window_width_delta + + # Divide the width of the Text widget by the font width, + # which is taken to be the width of '0' (zero). + # http://www.tcl.tk/man/tcl8.6/TkCmd/text.htm#M21 + return linewidth_pixels // self.zero_char_width + + def load_font(self): + text = self.base_text + self.zero_char_width = \ + Font(text, font=text.cget('font')).measure('0') def squeeze_current_text_event(self, event): """squeeze-current-text event handler @@ -301,29 +325,29 @@ class Squeezer: 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 + # 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 + # The insert cursor doesn't have a "stdout" or "stderr" tag. self.text.bell() return "break" - # find the range to squeeze + # 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 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 + # Delete the text. self.base_text.delete(start, end) - # prepare an ExpandingButton + # Prepare an ExpandingButton. numoflines = self.count_lines(s) expandingbutton = ExpandingButton(s, tag_name, numoflines, self) @@ -331,9 +355,9 @@ class Squeezer: 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 + # 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): |