Mercurial > repos > rliterman > csp2
comparison 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 |
comparison
equal
deleted
inserted
replaced
67:0e9998148a16 | 69:33d812a61356 |
---|---|
1 """An IDLE extension to avoid having very long texts printed in the shell. | |
2 | |
3 A common problem in IDLE's interactive shell is printing of large amounts of | |
4 text into the shell. This makes looking at the previous history difficult. | |
5 Worse, this can cause IDLE to become very slow, even to the point of being | |
6 completely unusable. | |
7 | |
8 This extension will automatically replace long texts with a small button. | |
9 Double-clicking this button will remove it and insert the original text instead. | |
10 Middle-clicking will copy the text to the clipboard. Right-clicking will open | |
11 the text in a separate viewing window. | |
12 | |
13 Additionally, any output can be manually "squeezed" by the user. This includes | |
14 output written to the standard error stream ("stderr"), such as exception | |
15 messages and their tracebacks. | |
16 """ | |
17 import re | |
18 | |
19 import tkinter as tk | |
20 import tkinter.messagebox as tkMessageBox | |
21 | |
22 from idlelib.config import idleConf | |
23 from idlelib.textview import view_text | |
24 from idlelib.tooltip import Hovertip | |
25 from idlelib import macosx | |
26 | |
27 | |
28 def 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 over | |
32 linewidth characters long. | |
33 | |
34 Tabs are considered tabwidth characters long. | |
35 """ | |
36 tabwidth = 8 # Currently always true in Shell. | |
37 pos = 0 | |
38 linecount = 1 | |
39 current_column = 0 | |
40 | |
41 for m in re.finditer(r"[\t\n]", s): | |
42 # Process the normal chars up to tab or newline. | |
43 numchars = m.start() - pos | |
44 pos += numchars | |
45 current_column += numchars | |
46 | |
47 # Deal with tab or newline. | |
48 if s[pos] == '\n': | |
49 # Avoid the `current_column == 0` edge-case, and while we're | |
50 # at it, don't bother adding 0. | |
51 if current_column > linewidth: | |
52 # If the current column was exactly linewidth, divmod | |
53 # would give (1,0), even though a new line hadn't yet | |
54 # been started. The same is true if length is any exact | |
55 # multiple of linewidth. Therefore, subtract 1 before | |
56 # dividing a non-empty line. | |
57 linecount += (current_column - 1) // linewidth | |
58 linecount += 1 | |
59 current_column = 0 | |
60 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 entire | |
65 # tab as being on the next line. | |
66 if current_column > linewidth: | |
67 linecount += 1 | |
68 current_column = tabwidth | |
69 | |
70 pos += 1 # After the tab or newline. | |
71 | |
72 # Process remaining chars (no more tabs or newlines). | |
73 current_column += len(s) - pos | |
74 # Avoid divmod(-1, linewidth). | |
75 if current_column > 0: | |
76 linecount += (current_column - 1) // linewidth | |
77 else: | |
78 # Text ended with newline; don't count an extra line after it. | |
79 linecount -= 1 | |
80 | |
81 return linecount | |
82 | |
83 | |
84 class ExpandingButton(tk.Button): | |
85 """Class for the "squeezed" text buttons used by Squeezer | |
86 | |
87 These buttons are displayed inside a Tk Text widget in place of text. A | |
88 user can then use the button to replace it with the original text, copy | |
89 the original text to the clipboard or view the original text in a separate | |
90 window. | |
91 | |
92 Each button is tied to a Squeezer instance, and it knows to update the | |
93 Squeezer instance when it is expanded (and therefore removed). | |
94 """ | |
95 def __init__(self, s, tags, numoflines, squeezer): | |
96 self.s = s | |
97 self.tags = tags | |
98 self.numoflines = numoflines | |
99 self.squeezer = squeezer | |
100 self.editwin = editwin = squeezer.editwin | |
101 self.text = text = editwin.text | |
102 # The base Text widget is needed to change text before iomark. | |
103 self.base_text = editwin.per.bottom | |
104 | |
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 = None | |
125 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 or | |
131 len(self.s) > 50000 or | |
132 any( | |
133 len(line_match.group(0)) >= dangerous_line_len | |
134 for line_match in re.finditer(r'[^\n]+', self.s) | |
135 ) | |
136 ) | |
137 | |
138 def expand(self, event=None): | |
139 """expand event handler | |
140 | |
141 This inserts the original text in place of the button in the Text | |
142 widget, removes the button and updates the Squeezer instance. | |
143 | |
144 If the original text is dangerously long, i.e. expanding it could | |
145 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 = tkMessageBox.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=tkMessageBox.CANCEL, | |
159 parent=self.text) | |
160 if not confirm: | |
161 return "break" | |
162 | |
163 self.base_text.insert(self.text.index(self), self.s, self.tags) | |
164 self.base_text.delete(self) | |
165 self.squeezer.expandingbuttons.remove(self) | |
166 | |
167 def copy(self, event=None): | |
168 """copy event handler | |
169 | |
170 Copy the original text to the clipboard. | |
171 """ | |
172 self.clipboard_clear() | |
173 self.clipboard_append(self.s) | |
174 | |
175 def view(self, event=None): | |
176 """view event handler | |
177 | |
178 View the original text in a separate text viewer window. | |
179 """ | |
180 view_text(self.text, "Squeezed Output Viewer", self.s, | |
181 modal=False, wrap='none') | |
182 | |
183 rmenu_specs = ( | |
184 # Item structure: (label, method_name). | |
185 ('copy', 'copy'), | |
186 ('view', 'view'), | |
187 ) | |
188 | |
189 def context_menu_event(self, event): | |
190 self.text.mark_set("insert", "@%d,%d" % (event.x, event.y)) | |
191 rmenu = tk.Menu(self.text, tearoff=0) | |
192 for label, method_name in self.rmenu_specs: | |
193 rmenu.add_command(label=label, command=getattr(self, method_name)) | |
194 rmenu.tk_popup(event.x_root, event.y_root) | |
195 return "break" | |
196 | |
197 | |
198 class Squeezer: | |
199 """Replace long outputs in the shell with a simple button. | |
200 | |
201 This avoids IDLE's shell slowing down considerably, and even becoming | |
202 completely unresponsive, when very long outputs are written. | |
203 """ | |
204 @classmethod | |
205 def reload(cls): | |
206 """Load class variables from config.""" | |
207 cls.auto_squeeze_min_lines = idleConf.GetOption( | |
208 "main", "PyShell", "auto-squeeze-min-lines", | |
209 type="int", default=50, | |
210 ) | |
211 | |
212 def __init__(self, editwin): | |
213 """Initialize settings for Squeezer. | |
214 | |
215 editwin is the shell's Editor window. | |
216 self.text is the editor window text widget. | |
217 self.base_test is the actual editor window Tk text widget, rather than | |
218 EditorWindow's wrapper. | |
219 self.expandingbuttons is the list of all buttons representing | |
220 "squeezed" output. | |
221 """ | |
222 self.editwin = editwin | |
223 self.text = text = editwin.text | |
224 | |
225 # Get the base Text widget of the PyShell object, used to change | |
226 # text before the iomark. PyShell deliberately disables changing | |
227 # text before the iomark via its 'text' attribute, which is | |
228 # actually a wrapper for the actual Text widget. Squeezer, | |
229 # however, needs to make such changes. | |
230 self.base_text = editwin.per.bottom | |
231 | |
232 # Twice the text widget's border width and internal padding; | |
233 # pre-calculated here for the get_line_width() method. | |
234 self.window_width_delta = 2 * ( | |
235 int(text.cget('border')) + | |
236 int(text.cget('padx')) | |
237 ) | |
238 | |
239 self.expandingbuttons = [] | |
240 | |
241 # Replace the PyShell instance's write method with a wrapper, | |
242 # which inserts an ExpandingButton instead of a long text. | |
243 def mywrite(s, tags=(), write=editwin.write): | |
244 # Only auto-squeeze text which has just the "stdout" tag. | |
245 if tags != "stdout": | |
246 return write(s, tags) | |
247 | |
248 # Only auto-squeeze text with at least the minimum | |
249 # configured number of lines. | |
250 auto_squeeze_min_lines = self.auto_squeeze_min_lines | |
251 # First, a very quick check to skip very short texts. | |
252 if len(s) < auto_squeeze_min_lines: | |
253 return write(s, tags) | |
254 # Now the full line-count check. | |
255 numoflines = self.count_lines(s) | |
256 if numoflines < auto_squeeze_min_lines: | |
257 return write(s, tags) | |
258 | |
259 # Create an ExpandingButton instance. | |
260 expandingbutton = ExpandingButton(s, tags, numoflines, self) | |
261 | |
262 # Insert the ExpandingButton into the Text widget. | |
263 text.mark_gravity("iomark", tk.RIGHT) | |
264 text.window_create("iomark", window=expandingbutton, | |
265 padx=3, pady=5) | |
266 text.see("iomark") | |
267 text.update() | |
268 text.mark_gravity("iomark", tk.LEFT) | |
269 | |
270 # Add the ExpandingButton to the Squeezer's list. | |
271 self.expandingbuttons.append(expandingbutton) | |
272 | |
273 editwin.write = mywrite | |
274 | |
275 def count_lines(self, s): | |
276 """Count the number of lines in a given text. | |
277 | |
278 Before calculation, the tab width and line length of the text are | |
279 fetched, so that up-to-date values are used. | |
280 | |
281 Lines are counted as if the string was wrapped so that lines are never | |
282 over linewidth characters long. | |
283 | |
284 Tabs are considered tabwidth characters long. | |
285 """ | |
286 return count_lines_with_wrapping(s, self.editwin.width) | |
287 | |
288 def squeeze_current_text_event(self, event): | |
289 """squeeze-current-text event handler | |
290 | |
291 Squeeze the block of text inside which contains the "insert" cursor. | |
292 | |
293 If the insert cursor is not in a squeezable block of text, give the | |
294 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 break | |
301 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 Text | |
323 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 of | |
328 # 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 -= 1 | |
333 self.expandingbuttons.insert(i, expandingbutton) | |
334 | |
335 return "break" | |
336 | |
337 | |
338 Squeezer.reload() | |
339 | |
340 | |
341 if __name__ == "__main__": | |
342 from unittest import main | |
343 main('idlelib.idle_test.test_squeezer', verbosity=2, exit=False) | |
344 | |
345 # Add htest. |