summaryrefslogtreecommitdiff
path: root/Lib/idlelib/squeezer.py
diff options
context:
space:
mode:
authorTal Einat <taleinat+github@gmail.com>2019-01-13 17:01:50 +0200
committerGitHub <noreply@github.com>2019-01-13 17:01:50 +0200
commit39a33e99270848d34628cdbb1fdb727f9ede502a (patch)
tree37e17a396741a0a98790b2bbcd6a24e2e730adca /Lib/idlelib/squeezer.py
parent995d9b92979768125ced4da3a56f755bcdf80f6e (diff)
downloadcpython-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.py190
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):