This commit is contained in:
Shixian Li 2026-05-04 00:29:41 +00:00 committed by GitHub
commit feff24cded
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 64 additions and 34 deletions

View file

@ -182,19 +182,12 @@ def get_argspec(ob):
# If fob has no argument, use default callable argspec.
argspec = _default_callable_argspec
lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT)
if len(argspec) > _MAX_COLS else [argspec] if argspec else [])
lines = [argspec] if argspec else []
# Augment lines from docstring, if any, and join to get argspec.
doc = inspect.getdoc(ob)
if doc:
for line in doc.split('\n', _MAX_LINES)[:_MAX_LINES]:
line = line.strip()
if not line:
break
if len(line) > _MAX_COLS:
line = line[: _MAX_COLS - 3] + '...'
lines.append(line)
lines.extend(map(str.strip, doc.split('\n')))
argspec = '\n'.join(lines)
return argspec or _default_callable_argspec

View file

@ -4,11 +4,12 @@
Used by calltip.py.
"""
from tkinter import Label, LEFT, SOLID, TclError
from tkinter.scrolledtext import ScrolledText
from idlelib.tooltip import TooltipBase
HIDE_EVENT = "<<calltipwindow-hide>>"
HIDE_SEQUENCES = ("<Key-Escape>", "<FocusOut>")
HIDE_SEQUENCES = ("<Key-Escape>",)
CHECKHIDE_EVENT = "<<calltipwindow-checkhide>>"
CHECKHIDE_SEQUENCES = ("<KeyRelease>", "<ButtonRelease>")
CHECKHIDE_TIME = 100 # milliseconds
@ -16,6 +17,13 @@
MARK_RIGHT = "calltipwindowregion_right"
def _widget_size(widget):
widget.update()
width = widget.winfo_width()
height = widget.winfo_height()
return width, height
class CalltipWindow(TooltipBase):
"""A call-tip widget for tkinter text widgets."""
@ -74,16 +82,31 @@ def showtip(self, text, parenleft, parenright):
int, self.anchor_widget.index(parenleft).split("."))
super().showtip()
self.tipwindow.wm_attributes("-topmost", 1)
self._bind_events()
def showcontents(self):
"""Create the call-tip widget."""
self.label = Label(self.tipwindow, text=self.text, justify=LEFT,
self.label = Label(self.tipwindow, text=self.text, font=self.anchor_widget['font'])
self.label.pack()
old_w, old_h = _widget_size(self.label)
self.label.forget()
self.label = ScrolledText(self.tipwindow, wrap="word",
background="#ffffd0", foreground="black",
relief=SOLID, borderwidth=1,
font=self.anchor_widget['font'])
self.label.insert("1.0", self.text)
self.label.config(state="disabled")
self.label.pack()
new_w, new_h = _widget_size(self.label)
if self.label.yview()[1] == 1: # already shown entire text
self.label.vbar.forget()
w, h = min(old_w, new_w), min(old_h, new_h)
self.tipwindow.geometry("%dx%d" % (w, h))
def checkhide_event(self, event=None):
"""Handle CHECK_HIDE_EVENT: call hidetip or reschedule."""
@ -156,6 +179,8 @@ def _bind_events(self):
self.hide_event)
for seq in HIDE_SEQUENCES:
self.anchor_widget.event_add(HIDE_EVENT, seq)
if self.tipwindow:
self.tipwindow.bind("<Key-Escape>", self.hide_event)
def _unbind_events(self):
"""Unbind event handlers."""

View file

@ -93,19 +93,20 @@ class SB: __call__ = None
non-overlapping occurrences of the pattern in string by the
replacement repl. repl can be either a string or a callable;
if a string, backslash escapes in it are processed. If it is
a callable, it's passed the Match object and must return''')
a callable, it's passed the Match object and must return
a replacement string to be used.''')
tiptest(p.sub, '''\
(repl, string, count=0)
Return the string obtained by replacing the leftmost \
non-overlapping occurrences o...''')
non-overlapping occurrences of pattern in string by the replacement repl.''')
def test_signature_wrap(self):
if textwrap.TextWrapper.__doc__ is not None:
self.assertEqual(get_spec(textwrap.TextWrapper), '''\
(width=70, initial_indent='', subsequent_indent='', expand_tabs=True,
replace_whitespace=True, fix_sentence_endings=False, break_long_words=True,
drop_whitespace=True, break_on_hyphens=True, tabsize=8, *, max_lines=None,
placeholder=' [...]')
self.assertEqual(get_spec(textwrap.TextWrapper).split('\n\n')[0], '''\
(width=70, initial_indent='', subsequent_indent='', expand_tabs=True, \
replace_whitespace=True, fix_sentence_endings=False, break_long_words=True, \
drop_whitespace=True, break_on_hyphens=True, tabsize=8, *, max_lines=None, \
placeholder=' [...]')
Object for wrapping/filling text. The public interface consists of
the wrap() and fill() methods; the other methods are just there for
subclasses to override in order to tweak the default behaviour.
@ -124,19 +125,15 @@ def bar(s='a'*100):
def baz(s='a'*100, z='b'*100):
pass
indent = calltip._INDENT
sfoo = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
"aaaaaaaaaa')"
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')"
sbar = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
"aaaaaaaaaa')\nHello Guido"
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')"\
"\nHello Guido"
sbaz = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
"aaaaaaaaaa', z='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"\
"bbbbbbbbbbbbbbbbb\n" + indent + "bbbbbbbbbbbbbbbbbbbbbb"\
"bbbbbbbbbbbbbbbbbbbbbb')"
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',"\
" z='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"\
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb')"
for func,doc in [(foo, sfoo), (bar, sbar), (baz, sbaz)]:
with self.subTest(func=func, doc=doc):
@ -145,15 +142,21 @@ def baz(s='a'*100, z='b'*100):
def test_docline_truncation(self):
def f(): pass
f.__doc__ = 'a'*300
self.assertEqual(get_spec(f), f"()\n{'a'*(calltip._MAX_COLS-3) + '...'}")
self.assertEqual(get_spec(f), f"()\n{f.__doc__}")
@unittest.skipIf(MISSING_C_DOCSTRINGS,
"Signature information for builtins requires docstrings")
def test_multiline_docstring(self):
# Test fewer lines than max.
self.assertEqual(get_spec(range),
"range(stop) -> range object\n"
"range(start, stop[, step]) -> range object")
self.assertEqual(get_spec(range), '''\
range(stop) -> range object
range(start, stop[, step]) -> range object
Return an object that produces a sequence of integers from start (inclusive)
to stop (exclusive) by step. range(i, j) produces i, i+1, i+2, ..., j-1.
start defaults to 0, and stop is omitted! range(4) produces 0, 1, 2, 3.
These are exactly the valid indices for a list of 4 elements.
When step is given, it specifies the increment (or decrement).''')
# Test max lines
self.assertEqual(get_spec(bytes), '''\
@ -161,13 +164,19 @@ def test_multiline_docstring(self):
bytes(string, encoding[, errors]) -> bytes
bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer
bytes(int) -> bytes object of size given by the parameter initialized with null bytes
bytes() -> empty bytes object''')
bytes() -> empty bytes object
Construct an immutable array of bytes from:
- an iterable yielding integers in range(256)
- a text string encoded using the specified encoding
- any object implementing the buffer API.
- an integer''')
def test_multiline_docstring_2(self):
# Test more than max lines
def f(): pass
f.__doc__ = 'a\n' * 15
self.assertEqual(get_spec(f), '()' + '\na' * calltip._MAX_LINES)
self.assertEqual(get_spec(f), '()\n' + f.__doc__[:-1])
def test_functions(self):
def t1(): 'doc'

View file

@ -0,0 +1,3 @@
"Calltip" windows now support text selection, scrolling and
avoid truncating their content (in particular, docstrings
are shown in full). Patch by Shixian Li.