summaryrefslogtreecommitdiffstats
path: root/Lib/idlelib
diff options
context:
space:
mode:
authorTal Einat <taleinat+github@gmail.com>2020-11-02 03:59:52 (GMT)
committerGitHub <noreply@github.com>2020-11-02 03:59:52 (GMT)
commitda7bb7b4d769350c5fd03e6cfb16b23dc265ed72 (patch)
tree90825dd55ad9782a1375e4594fd067ebf665c345 /Lib/idlelib
parent74fa464b81ebf3312ae6efa11e618c00c2a9fd34 (diff)
downloadcpython-da7bb7b4d769350c5fd03e6cfb16b23dc265ed72.zip
cpython-da7bb7b4d769350c5fd03e6cfb16b23dc265ed72.tar.gz
cpython-da7bb7b4d769350c5fd03e6cfb16b23dc265ed72.tar.bz2
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@udel.edu>
Diffstat (limited to 'Lib/idlelib')
-rw-r--r--Lib/idlelib/calltip.py36
-rw-r--r--Lib/idlelib/idle_test/mock_tk.py13
-rw-r--r--Lib/idlelib/idle_test/test_calltip.py99
3 files changed, 141 insertions, 7 deletions
diff --git a/Lib/idlelib/calltip.py b/Lib/idlelib/calltip.py
index b02f872..549e224 100644
--- a/Lib/idlelib/calltip.py
+++ b/Lib/idlelib/calltip.py
@@ -55,18 +55,50 @@ class Calltip:
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 576f7d5..b736bd0 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 @@ class Event:
"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 @@ class Var:
def get(self):
return self.value
+
class Mbox_func:
"""Generic mock for messagebox functions, which all have the same signature.
@@ -50,6 +55,7 @@ class Mbox_func:
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 @@ class Test(unittest.TestCase):
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 @@ class Text:
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 @@ class Text:
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 @@ class Text:
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 @@ class Text:
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 @@ class Text:
"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 4d53df1..489b689 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 @@ class Get_entityTest(unittest.TestCase):
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)