[Python-checkins] bpo-40511: Stop unwanted flashing of IDLE calltips (GH-20910)

miss-islington webhook-mailer at python.org
Sun Nov 1 23:49:47 EST 2020


https://github.com/python/cpython/commit/79e9f06149f92798a8e11e3f1c62dad171312ab3
commit: 79e9f06149f92798a8e11e3f1c62dad171312ab3
branch: 3.9
author: Miss Skeleton (bot) <31488909+miss-islington at users.noreply.github.com>
committer: miss-islington <31488909+miss-islington at users.noreply.github.com>
date: 2020年11月01日T20:49:39-08:00
summary:
bpo-40511: Stop unwanted flashing of IDLE calltips (GH-20910)
They were occurring with both repeated 'force-calltip' invocations and by typing parentheses
 in expressions, strings, and comments in the argument code.
Co-authored-by: Terry Jan Reedy <tjreedy at udel.edu>
(cherry picked from commit da7bb7b4d769350c5fd03e6cfb16b23dc265ed72)
Co-authored-by: Tal Einat <taleinat+github at gmail.com>
files:
A Misc/NEWS.d/next/IDLE/2020-06-16-12-16-13.bpo-40511.XkihpM.rst
M Lib/idlelib/calltip.py
M Lib/idlelib/idle_test/mock_tk.py
M Lib/idlelib/idle_test/test_calltip.py
diff --git a/Lib/idlelib/calltip.py b/Lib/idlelib/calltip.py
index b02f87207d8db..549e224015ccc 100644
--- a/Lib/idlelib/calltip.py
+++ b/Lib/idlelib/calltip.py
@@ -55,18 +55,50 @@ def refresh_calltip_event(self, event):
 self.open_calltip(False)
 
 def open_calltip(self, evalfuncs):
- self.remove_calltip_window()
+ """Maybe close an existing calltip and maybe open a new calltip.
 
+ Called from (force_open|try_open|refresh)_calltip_event functions.
+ """
 hp = HyperParser(self.editwin, "insert")
 sur_paren = hp.get_surrounding_brackets('(')
+
+ # If not inside parentheses, no calltip.
 if not sur_paren:
+ self.remove_calltip_window()
 return
+
+ # If a calltip is shown for the current parentheses, do
+ # nothing.
+ if self.active_calltip:
+ opener_line, opener_col = map(int, sur_paren[0].split('.'))
+ if (
+ (opener_line, opener_col) ==
+ (self.active_calltip.parenline, self.active_calltip.parencol)
+ ):
+ return
+
 hp.set_index(sur_paren[0])
- expression = hp.get_expression()
+ try:
+ expression = hp.get_expression()
+ except ValueError:
+ expression = None
 if not expression:
+ # No expression before the opening parenthesis, e.g.
+ # because it's in a string or the opener for a tuple:
+ # Do nothing.
 return
+
+ # At this point, the current index is after an opening
+ # parenthesis, in a section of code, preceded by a valid
+ # expression. If there is a calltip shown, it's not for the
+ # same index and should be closed.
+ self.remove_calltip_window()
+
+ # Simple, fast heuristic: If the preceding expression includes
+ # an opening parenthesis, it likely includes a function call.
 if not evalfuncs and (expression.find('(') != -1):
 return
+
 argspec = self.fetch_tip(expression)
 if not argspec:
 return
diff --git a/Lib/idlelib/idle_test/mock_tk.py b/Lib/idlelib/idle_test/mock_tk.py
index 576f7d5d609e4..b736bd001da87 100644
--- a/Lib/idlelib/idle_test/mock_tk.py
+++ b/Lib/idlelib/idle_test/mock_tk.py
@@ -3,6 +3,9 @@
 A gui object is anything with a master or parent parameter, which is
 typically required in spite of what the doc strings say.
 """
+import re
+from _tkinter import TclError
+
 
 class Event:
 '''Minimal mock with attributes for testing event handlers.
@@ -22,6 +25,7 @@ def __init__(self, **kwds):
 "Create event with attributes needed for test"
 self.__dict__.update(kwds)
 
+
 class Var:
 "Use for String/Int/BooleanVar: incomplete"
 def __init__(self, master=None, value=None, name=None):
@@ -33,6 +37,7 @@ def set(self, value):
 def get(self):
 return self.value
 
+
 class Mbox_func:
 """Generic mock for messagebox functions, which all have the same signature.
 
@@ -50,6 +55,7 @@ def __call__(self, title, message, *args, **kwds):
 self.kwds = kwds
 return self.result # Set by tester for ask functions
 
+
 class Mbox:
 """Mock for tkinter.messagebox with an Mbox_func for each function.
 
@@ -85,7 +91,6 @@ def tearDownClass(cls):
 showinfo = Mbox_func() # None
 showwarning = Mbox_func() # None
 
-from _tkinter import TclError
 
 class Text:
 """A semi-functional non-gui replacement for tkinter.Text text editors.
@@ -154,6 +159,8 @@ def _decode(self, index, endflag=0):
 if char.endswith(' lineend') or char == 'end':
 return line, linelength
 # Tk requires that ignored chars before ' lineend' be valid int
+ if m := re.fullmatch(r'end-(\d*)c', char, re.A): # Used by hyperparser.
+ return line, linelength - int(m.group(1))
 
 # Out of bounds char becomes first or last index of line
 char = int(char)
@@ -177,7 +184,6 @@ def _endex(self, endflag):
 n -= 1
 return n, len(self.data[n]) + endflag
 
-
 def insert(self, index, chars):
 "Insert chars before the character at index."
 
@@ -193,7 +199,6 @@ def insert(self, index, chars):
 self.data[line+1:line+1] = chars[1:]
 self.data[line+len(chars)-1] += after
 
-
 def get(self, index1, index2=None):
 "Return slice from index1 to index2 (default is 'index1+1')."
 
@@ -212,7 +217,6 @@ def get(self, index1, index2=None):
 lines.append(self.data[endline][:endchar])
 return ''.join(lines)
 
-
 def delete(self, index1, index2=None):
 '''Delete slice from index1 to index2 (default is 'index1+1').
 
@@ -297,6 +301,7 @@ def bind(sequence=None, func=None, add=None):
 "Bind to this widget at event sequence a call to function func."
 pass
 
+
 class Entry:
 "Mock for tkinter.Entry."
 def focus_set(self):
diff --git a/Lib/idlelib/idle_test/test_calltip.py b/Lib/idlelib/idle_test/test_calltip.py
index 4d53df17d8cc7..489b6899baf42 100644
--- a/Lib/idlelib/idle_test/test_calltip.py
+++ b/Lib/idlelib/idle_test/test_calltip.py
@@ -1,10 +1,12 @@
-"Test calltip, coverage 60%"
+"Test calltip, coverage 76%"
 
 from idlelib import calltip
 import unittest
+from unittest.mock import Mock
 import textwrap
 import types
 import re
+from idlelib.idle_test.mock_tk import Text
 
 
 # Test Class TC is used in multiple get_argspec test methods
@@ -257,5 +259,100 @@ def test_good_entity(self):
 self.assertIs(calltip.get_entity('int'), int)
 
 
+# Test the 9 Calltip methods.
+# open_calltip is about half the code; the others are fairly trivial.
+# The default mocks are what are needed for open_calltip.
+
+class mock_Shell():
+ "Return mock sufficient to pass to hyperparser."
+ def __init__(self, text):
+ text.tag_prevrange = Mock(return_value=None)
+ self.text = text
+ self.prompt_last_line = ">>> "
+ self.indentwidth = 4
+ self.tabwidth = 8
+
+
+class mock_TipWindow:
+ def __init__(self):
+ pass
+
+ def showtip(self, text, parenleft, parenright):
+ self.args = parenleft, parenright
+ self.parenline, self.parencol = map(int, parenleft.split('.'))
+
+
+class WrappedCalltip(calltip.Calltip):
+ def _make_tk_calltip_window(self):
+ return mock_TipWindow()
+
+ def remove_calltip_window(self, event=None):
+ if self.active_calltip: # Setup to None.
+ self.active_calltip = None
+ self.tips_removed += 1 # Setup to 0.
+
+ def fetch_tip(self, expression):
+ return 'tip'
+
+
+class CalltipTest(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ cls.text = Text()
+ cls.ct = WrappedCalltip(mock_Shell(cls.text))
+
+ def setUp(self):
+ self.text.delete('1.0', 'end') # Insert and call
+ self.ct.active_calltip = None
+ # Test .active_calltip, +args
+ self.ct.tips_removed = 0
+
+ def open_close(self, testfunc):
+ # Open-close template with testfunc called in between.
+ opentip = self.ct.open_calltip
+ self.text.insert(1.0, 'f(')
+ opentip(False)
+ self.tip = self.ct.active_calltip
+ testfunc(self) ###
+ self.text.insert('insert', ')')
+ opentip(False)
+ self.assertIsNone(self.ct.active_calltip, None)
+
+ def test_open_close(self):
+ def args(self):
+ self.assertEqual(self.tip.args, ('1.1', '1.end'))
+ self.open_close(args)
+
+ def test_repeated_force(self):
+ def force(self):
+ for char in 'abc':
+ self.text.insert('insert', 'a')
+ self.ct.open_calltip(True)
+ self.ct.open_calltip(True)
+ self.assertIs(self.ct.active_calltip, self.tip)
+ self.open_close(force)
+
+ def test_repeated_parens(self):
+ def parens(self):
+ for context in "a", "'":
+ with self.subTest(context=context):
+ self.text.insert('insert', context)
+ for char in '(()())':
+ self.text.insert('insert', char)
+ self.assertIs(self.ct.active_calltip, self.tip)
+ self.text.insert('insert', "'")
+ self.open_close(parens)
+
+ def test_comment_parens(self):
+ def comment(self):
+ self.text.insert('insert', "# ")
+ for char in '(()())':
+ self.text.insert('insert', char)
+ self.assertIs(self.ct.active_calltip, self.tip)
+ self.text.insert('insert', "\n")
+ self.open_close(comment)
+
+
 if __name__ == '__main__':
 unittest.main(verbosity=2)
diff --git a/Misc/NEWS.d/next/IDLE/2020-06-16-12-16-13.bpo-40511.XkihpM.rst b/Misc/NEWS.d/next/IDLE/2020-06-16-12-16-13.bpo-40511.XkihpM.rst
new file mode 100644
index 0000000000000..cc96798138176
--- /dev/null
+++ b/Misc/NEWS.d/next/IDLE/2020-06-16-12-16-13.bpo-40511.XkihpM.rst
@@ -0,0 +1,3 @@
+Typing opening and closing parentheses inside the parentheses of a function
+call will no longer cause unnecessary "flashing" off and on of an existing
+open call-tip, e.g. when typed in a string literal.


More information about the Python-checkins mailing list

AltStyle によって変換されたページ (->オリジナル) /