Skip to content
12 changes: 3 additions & 9 deletions Lib/idlelib/calltip.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,19 +182,13 @@ 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)
for line in doc.split('\n'):
lines.append(line.strip())
Comment thread
znsoooo marked this conversation as resolved.
Outdated
argspec = '\n'.join(lines)

return argspec or _default_callable_argspec
Expand Down
30 changes: 27 additions & 3 deletions Lib/idlelib/calltip_w.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,26 @@
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

MARK_RIGHT = "calltipwindowregion_right"


def widget_size(widget):
Comment thread
znsoooo marked this conversation as resolved.
Outdated
widget.update()
width = widget.winfo_width()
height = widget.winfo_height()
return width, height


class CalltipWindow(TooltipBase):
"""A call-tip widget for tkinter text widgets."""

Expand Down Expand Up @@ -74,16 +82,30 @@ 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()
label_w, label_h = widget_size(self.label) # get the old version of tooltip window size
Comment thread
znsoooo marked this conversation as resolved.
Outdated
self.label.forget()

self.label = ScrolledText(self.tipwindow, wrap="word",
background="#ffffd0", foreground="black",
relief=SOLID, borderwidth=1,
font=self.anchor_widget['font'])
font=self.anchor_widget["font"])
self.label.insert("1.0", self.text)
self.label.config(state="disabled")
self.label.pack()
max_w, max_h = widget_size(self.label)
Comment thread
znsoooo marked this conversation as resolved.
Outdated

if self.label.yview()[1] == 1: # already shown entire text
self.label.vbar.forget()

self.tipwindow.geometry("%dx%d" % (min(label_w, max_w), min(label_h, max_h)))
Comment thread
znsoooo marked this conversation as resolved.
Outdated

def checkhide_event(self, event=None):
"""Handle CHECK_HIDE_EVENT: call hidetip or reschedule."""
Expand Down Expand Up @@ -156,6 +178,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."""
Expand Down
62 changes: 17 additions & 45 deletions Lib/idlelib/idle_test/test_calltip.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,24 +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...''')
Return the string obtained by replacing the leftmost non-overlapping \
occurrences of pattern in string by the replacement repl.''')

def test_signature_wrap(self):
Comment thread
picnixz marked this conversation as resolved.
def test_signature(self):
if textwrap.TextWrapper.__doc__ is not None:
self.assertEqual(get_spec(textwrap.TextWrapper), '''\
Comment thread
picnixz marked this conversation as resolved.
(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.
If you want to completely replace the main wrapping algorithm,
you\'ll probably have to override _wrap_chunks().''')
self.assertEqual(get_spec(textwrap.TextWrapper).split('\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=' [...]')''')

def test_properly_formatted(self):

Expand All @@ -127,16 +123,14 @@ def baz(s='a'*100, z='b'*100):
indent = calltip._INDENT

sfoo = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
"aaaaaaaaaa')"
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')"
sbar = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
Comment thread
picnixz marked this conversation as resolved.
"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):
Expand All @@ -145,29 +139,7 @@ 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) + '...'}")

@unittest.skipIf(MISSING_C_DOCSTRINGS,
"Signature information for builtins requires docstrings")
def test_multiline_docstring(self):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we removing this test?

Copy link
Copy Markdown
Contributor Author

@znsoooo znsoooo Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the old tests, the first blank line was used as a separator, and the text after the first blank line was not displayed. However, now that I use the "ScrolledText" widget to display the document. So it is easy to show very long text, so I'm not limit the display to the content before the first blank line.
Do you have any suggestions? Do I need to keep this feature unchanged?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, to say:

  • Before we only showed partial signature.
  • Now we can show the full signature. So old tests are no more needed.

I would advise to keep tests that check whether multiline signatures from builtins are also correctly shown

# Test fewer lines than max.
self.assertEqual(get_spec(range),
"range(stop) -> range object\n"
"range(start, stop[, step]) -> range object")

# Test max lines
self.assertEqual(get_spec(bytes), '''\
bytes(iterable_of_ints) -> bytes
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''')

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%s" % ('a'*300))

def test_functions(self):
def t1(): 'doc'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Make CallTips selectable

The text display widget in the "CalltipWindow" has been changed from
"tk.Label" to "ScrolledText", and now the text in the "Calltip" window can
be selected with mouse. The display size of the "CalltipWindow" is set to
the smaller value between the size when using the "tk.Label" widget and the
default size of "tk.Text". When the displayed text exceeds the display area
of the "ScrolledText" window, showing the vertical scrollbar; otherwise,
hiding the scrollbar. Since more text can be displayed, "argspec" is no
longer truncated, and the tests related to the max lines or text truncation
have been removed from the unit tests. Contributed by Shixian Li.
Comment thread
znsoooo marked this conversation as resolved.
Outdated
Loading