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>
This commit is contained in:
parent
74fa464b81
commit
da7bb7b4d7
|
@ -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])
|
||||
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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue