diff --git a/Lib/idlelib/calltip.py b/Lib/idlelib/calltip.py index 40bc5a0ad798fe..46492fe7133e1a 100644 --- a/Lib/idlelib/calltip.py +++ b/Lib/idlelib/calltip.py @@ -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 diff --git a/Lib/idlelib/calltip_w.py b/Lib/idlelib/calltip_w.py index 9386376058c791..83de19486b161b 100644 --- a/Lib/idlelib/calltip_w.py +++ b/Lib/idlelib/calltip_w.py @@ -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 = "<>" -HIDE_SEQUENCES = ("", "") +HIDE_SEQUENCES = ("",) CHECKHIDE_EVENT = "<>" CHECKHIDE_SEQUENCES = ("", "") 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("", self.hide_event) def _unbind_events(self): """Unbind event handlers.""" diff --git a/Lib/idlelib/idle_test/test_calltip.py b/Lib/idlelib/idle_test/test_calltip.py index 28c196a42672fc..ce48cc2e70fc0a 100644 --- a/Lib/idlelib/idle_test/test_calltip.py +++ b/Lib/idlelib/idle_test/test_calltip.py @@ -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' diff --git a/Misc/NEWS.d/next/IDLE/2025-12-21-07-02-44.gh-issue-94520.lqenne.rst b/Misc/NEWS.d/next/IDLE/2025-12-21-07-02-44.gh-issue-94520.lqenne.rst new file mode 100644 index 00000000000000..c5908606505ae6 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2025-12-21-07-02-44.gh-issue-94520.lqenne.rst @@ -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.