Skip to content

Commit 949fe97

Browse files
authored
bpo-35763: Make IDLE calltip note about '/' less obtrusive (GH-13791)
Add it to the end of the first line if there is room. Tests were reworked.
1 parent 59e7bbc commit 949fe97

File tree

3 files changed

+69
-61
lines changed

3 files changed

+69
-61
lines changed

Lib/idlelib/calltip.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def get_entity(expression):
118118
_first_param = re.compile(r'(?<=\()\w*\,?\s*')
119119
_default_callable_argspec = "See source or doc"
120120
_invalid_method = "invalid method signature"
121-
_argument_positional = "\n['/' marks preceding arguments as positional-only]\n"
121+
_argument_positional = " # '/' marks preceding args as positional-only."
122122

123123
def get_argspec(ob):
124124
'''Return a string describing the signature of a callable object, or ''.
@@ -144,11 +144,11 @@ def get_argspec(ob):
144144
if msg.startswith(_invalid_method):
145145
return _invalid_method
146146

147-
if '/' in argspec:
148-
"""Using AC's positional argument should add the explain"""
147+
if '/' in argspec and len(argspec) < _MAX_COLS - len(_argument_positional):
148+
# Add explanation TODO remove after 3.7, before 3.9.
149149
argspec += _argument_positional
150150
if isinstance(fob, type) and argspec == '()':
151-
"""fob with no argument, use default callable argspec"""
151+
# If fob has no argument, use default callable argspec.
152152
argspec = _default_callable_argspec
153153

154154
lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT)

Lib/idlelib/idle_test/test_calltip.py

Lines changed: 63 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
import unittest
55
import textwrap
66
import types
7-
8-
default_tip = calltip._default_callable_argspec
7+
import re
98

109

1110
# Test Class TC is used in multiple get_argspec test methods
@@ -28,6 +27,7 @@ def t6(no, self): 'doc'
2827
t6.tip = "(no, self)"
2928
def __call__(self, ci): 'doc'
3029
__call__.tip = "(self, ci)"
30+
def nd(self): pass # No doc.
3131
# attaching .tip to wrapped methods does not work
3232
@classmethod
3333
def cm(cls, a): 'doc'
@@ -36,11 +36,12 @@ def sm(b): 'doc'
3636

3737

3838
tc = TC()
39-
signature = calltip.get_argspec # 2.7 and 3.x use different functions
39+
default_tip = calltip._default_callable_argspec
40+
get_spec = calltip.get_argspec
4041

4142

42-
class Get_signatureTest(unittest.TestCase):
43-
# The signature function must return a string, even if blank.
43+
class Get_argspecTest(unittest.TestCase):
44+
# The get_spec function must return a string, even if blank.
4445
# Test a variety of objects to be sure that none cause it to raise
4546
# (quite aside from getting as correct an answer as possible).
4647
# The tests of builtins may break if inspect or the docstrings change,
@@ -49,57 +50,59 @@ class Get_signatureTest(unittest.TestCase):
4950

5051
def test_builtins(self):
5152

53+
def tiptest(obj, out):
54+
self.assertEqual(get_spec(obj), out)
55+
5256
# Python class that inherits builtin methods
5357
class List(list): "List() doc"
5458

5559
# Simulate builtin with no docstring for default tip test
5660
class SB: __call__ = None
5761

58-
def gtest(obj, out):
59-
self.assertEqual(signature(obj), out)
60-
6162
if List.__doc__ is not None:
62-
gtest(List, '(iterable=(), /)' + calltip._argument_positional
63-
+ '\n' + List.__doc__)
64-
gtest(list.__new__,
63+
tiptest(List,
64+
f'(iterable=(), /){calltip._argument_positional}'
65+
f'\n{List.__doc__}')
66+
tiptest(list.__new__,
6567
'(*args, **kwargs)\n'
6668
'Create and return a new object. '
6769
'See help(type) for accurate signature.')
68-
gtest(list.__init__,
70+
tiptest(list.__init__,
6971
'(self, /, *args, **kwargs)'
7072
+ calltip._argument_positional + '\n' +
7173
'Initialize self. See help(type(self)) for accurate signature.')
7274
append_doc = (calltip._argument_positional
7375
+ "\nAppend object to the end of the list.")
74-
gtest(list.append, '(self, object, /)' + append_doc)
75-
gtest(List.append, '(self, object, /)' + append_doc)
76-
gtest([].append, '(object, /)' + append_doc)
76+
tiptest(list.append, '(self, object, /)' + append_doc)
77+
tiptest(List.append, '(self, object, /)' + append_doc)
78+
tiptest([].append, '(object, /)' + append_doc)
79+
80+
tiptest(types.MethodType, "method(function, instance)")
81+
tiptest(SB(), default_tip)
7782

78-
gtest(types.MethodType, "method(function, instance)")
79-
gtest(SB(), default_tip)
80-
import re
8183
p = re.compile('')
82-
gtest(re.sub, '''\
84+
tiptest(re.sub, '''\
8385
(pattern, repl, string, count=0, flags=0)
8486
Return the string obtained by replacing the leftmost
8587
non-overlapping occurrences of the pattern in string by the
8688
replacement repl. repl can be either a string or a callable;
8789
if a string, backslash escapes in it are processed. If it is
8890
a callable, it's passed the Match object and must return''')
89-
gtest(p.sub, '''\
91+
tiptest(p.sub, '''\
9092
(repl, string, count=0)
9193
Return the string obtained by replacing the leftmost \
9294
non-overlapping occurrences o...''')
9395

9496
def test_signature_wrap(self):
9597
if textwrap.TextWrapper.__doc__ is not None:
96-
self.assertEqual(signature(textwrap.TextWrapper), '''\
98+
self.assertEqual(get_spec(textwrap.TextWrapper), '''\
9799
(width=70, initial_indent='', subsequent_indent='', expand_tabs=True,
98100
replace_whitespace=True, fix_sentence_endings=False, break_long_words=True,
99101
drop_whitespace=True, break_on_hyphens=True, tabsize=8, *, max_lines=None,
100102
placeholder=' [...]')''')
101103

102104
def test_properly_formated(self):
105+
103106
def foo(s='a'*100):
104107
pass
105108

@@ -112,35 +115,35 @@ def baz(s='a'*100, z='b'*100):
112115

113116
indent = calltip._INDENT
114117

115-
str_foo = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
116-
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
117-
"aaaaaaaaaa')"
118-
str_bar = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
119-
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
120-
"aaaaaaaaaa')\nHello Guido"
121-
str_baz = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
122-
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
123-
"aaaaaaaaaa', z='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"\
124-
"bbbbbbbbbbbbbbbbb\n" + indent + "bbbbbbbbbbbbbbbbbbbbbb"\
125-
"bbbbbbbbbbbbbbbbbbbbbb')"
126-
127-
self.assertEqual(calltip.get_argspec(foo), str_foo)
128-
self.assertEqual(calltip.get_argspec(bar), str_bar)
129-
self.assertEqual(calltip.get_argspec(baz), str_baz)
118+
sfoo = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
119+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
120+
"aaaaaaaaaa')"
121+
sbar = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
122+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
123+
"aaaaaaaaaa')\nHello Guido"
124+
sbaz = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
125+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
126+
"aaaaaaaaaa', z='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"\
127+
"bbbbbbbbbbbbbbbbb\n" + indent + "bbbbbbbbbbbbbbbbbbbbbb"\
128+
"bbbbbbbbbbbbbbbbbbbbbb')"
129+
130+
for func,doc in [(foo, sfoo), (bar, sbar), (baz, sbaz)]:
131+
with self.subTest(func=func, doc=doc):
132+
self.assertEqual(get_spec(func), doc)
130133

131134
def test_docline_truncation(self):
132135
def f(): pass
133136
f.__doc__ = 'a'*300
134-
self.assertEqual(signature(f), '()\n' + 'a' * (calltip._MAX_COLS-3) + '...')
137+
self.assertEqual(get_spec(f), f"()\n{'a'*(calltip._MAX_COLS-3) + '...'}")
135138

136139
def test_multiline_docstring(self):
137140
# Test fewer lines than max.
138-
self.assertEqual(signature(range),
141+
self.assertEqual(get_spec(range),
139142
"range(stop) -> range object\n"
140143
"range(start, stop[, step]) -> range object")
141144

142145
# Test max lines
143-
self.assertEqual(signature(bytes), '''\
146+
self.assertEqual(get_spec(bytes), '''\
144147
bytes(iterable_of_ints) -> bytes
145148
bytes(string, encoding[, errors]) -> bytes
146149
bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer
@@ -150,7 +153,7 @@ def test_multiline_docstring(self):
150153
# Test more than max lines
151154
def f(): pass
152155
f.__doc__ = 'a\n' * 15
153-
self.assertEqual(signature(f), '()' + '\na' * calltip._MAX_LINES)
156+
self.assertEqual(get_spec(f), '()' + '\na' * calltip._MAX_LINES)
154157

155158
def test_functions(self):
156159
def t1(): 'doc'
@@ -166,40 +169,44 @@ def t5(a, b=None, *args, **kw): 'doc'
166169

167170
doc = '\ndoc' if t1.__doc__ is not None else ''
168171
for func in (t1, t2, t3, t4, t5, TC):
169-
self.assertEqual(signature(func), func.tip + doc)
172+
with self.subTest(func=func):
173+
self.assertEqual(get_spec(func), func.tip + doc)
170174

171175
def test_methods(self):
172176
doc = '\ndoc' if TC.__doc__ is not None else ''
173177
for meth in (TC.t1, TC.t2, TC.t3, TC.t4, TC.t5, TC.t6, TC.__call__):
174-
self.assertEqual(signature(meth), meth.tip + doc)
175-
self.assertEqual(signature(TC.cm), "(a)" + doc)
176-
self.assertEqual(signature(TC.sm), "(b)" + doc)
178+
with self.subTest(meth=meth):
179+
self.assertEqual(get_spec(meth), meth.tip + doc)
180+
self.assertEqual(get_spec(TC.cm), "(a)" + doc)
181+
self.assertEqual(get_spec(TC.sm), "(b)" + doc)
177182

178183
def test_bound_methods(self):
179184
# test that first parameter is correctly removed from argspec
180185
doc = '\ndoc' if TC.__doc__ is not None else ''
181186
for meth, mtip in ((tc.t1, "()"), (tc.t4, "(*args)"),
182187
(tc.t6, "(self)"), (tc.__call__, '(ci)'),
183188
(tc, '(ci)'), (TC.cm, "(a)"),):
184-
self.assertEqual(signature(meth), mtip + doc)
189+
with self.subTest(meth=meth, mtip=mtip):
190+
self.assertEqual(get_spec(meth), mtip + doc)
185191

186192
def test_starred_parameter(self):
187193
# test that starred first parameter is *not* removed from argspec
188194
class C:
189195
def m1(*args): pass
190196
c = C()
191197
for meth, mtip in ((C.m1, '(*args)'), (c.m1, "(*args)"),):
192-
self.assertEqual(signature(meth), mtip)
198+
with self.subTest(meth=meth, mtip=mtip):
199+
self.assertEqual(get_spec(meth), mtip)
193200

194-
def test_invalid_method_signature(self):
201+
def test_invalid_method_get_spec(self):
195202
class C:
196203
def m2(**kwargs): pass
197204
class Test:
198205
def __call__(*, a): pass
199206

200207
mtip = calltip._invalid_method
201-
self.assertEqual(signature(C().m2), mtip)
202-
self.assertEqual(signature(Test()), mtip)
208+
self.assertEqual(get_spec(C().m2), mtip)
209+
self.assertEqual(get_spec(Test()), mtip)
203210

204211
def test_non_ascii_name(self):
205212
# test that re works to delete a first parameter name that
@@ -208,12 +215,9 @@ def test_non_ascii_name(self):
208215
assert calltip._first_param.sub('', uni) == '(a)'
209216

210217
def test_no_docstring(self):
211-
def nd(s):
212-
pass
213-
TC.nd = nd
214-
self.assertEqual(signature(nd), "(s)")
215-
self.assertEqual(signature(TC.nd), "(s)")
216-
self.assertEqual(signature(tc.nd), "()")
218+
for meth, mtip in ((TC.nd, "(self)"), (tc.nd, "()")):
219+
with self.subTest(meth=meth, mtip=mtip):
220+
self.assertEqual(get_spec(meth), mtip)
217221

218222
def test_attribute_exception(self):
219223
class NoCall:
@@ -229,11 +233,13 @@ def __call__(self, ci):
229233
for meth, mtip in ((NoCall, default_tip), (CallA, default_tip),
230234
(NoCall(), ''), (CallA(), '(a, b, c)'),
231235
(CallB(), '(ci)')):
232-
self.assertEqual(signature(meth), mtip)
236+
with self.subTest(meth=meth, mtip=mtip):
237+
self.assertEqual(get_spec(meth), mtip)
233238

234239
def test_non_callables(self):
235240
for obj in (0, 0.0, '0', b'0', [], {}):
236-
self.assertEqual(signature(obj), '')
241+
with self.subTest(obj=obj):
242+
self.assertEqual(get_spec(obj), '')
237243

238244

239245
class Get_entityTest(unittest.TestCase):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Make calltip reminder about '/' meaning positional-only less obtrusive by
2+
only adding it when there is room on the first line.

0 commit comments

Comments
 (0)