Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Lib/idlelib/calltip.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def get_entity(expression):
_first_param = re.compile(r'(?<=\()\w*\,?\s*')
_default_callable_argspec = "See source or doc"
_invalid_method = "invalid method signature"
_argument_positional = "\n['/' marks preceding arguments as positional-only]\n"
_argument_positional = " # '/' marks preceding args as positional-only."

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

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

lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT)
Expand Down
120 changes: 63 additions & 57 deletions Lib/idlelib/idle_test/test_calltip.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import unittest
import textwrap
import types

default_tip = calltip._default_callable_argspec
import re


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


tc = TC()
signature = calltip.get_argspec # 2.7 and 3.x use different functions
default_tip = calltip._default_callable_argspec
get_spec = calltip.get_argspec


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

def test_builtins(self):

def tiptest(obj, out):
self.assertEqual(get_spec(obj), out)

# Python class that inherits builtin methods
class List(list): "List() doc"

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

def gtest(obj, out):
self.assertEqual(signature(obj), out)

if List.__doc__ is not None:
gtest(List, '(iterable=(), /)' + calltip._argument_positional
+ '\n' + List.__doc__)
gtest(list.__new__,
tiptest(List,
f'(iterable=(), /){calltip._argument_positional}'
f'\n{List.__doc__}')
tiptest(list.__new__,
'(*args, **kwargs)\n'
'Create and return a new object. '
'See help(type) for accurate signature.')
gtest(list.__init__,
tiptest(list.__init__,
'(self, /, *args, **kwargs)'
+ calltip._argument_positional + '\n' +
'Initialize self. See help(type(self)) for accurate signature.')
append_doc = (calltip._argument_positional
+ "\nAppend object to the end of the list.")
gtest(list.append, '(self, object, /)' + append_doc)
gtest(List.append, '(self, object, /)' + append_doc)
gtest([].append, '(object, /)' + append_doc)
tiptest(list.append, '(self, object, /)' + append_doc)
tiptest(List.append, '(self, object, /)' + append_doc)
tiptest([].append, '(object, /)' + append_doc)

tiptest(types.MethodType, "method(function, instance)")
tiptest(SB(), default_tip)

gtest(types.MethodType, "method(function, instance)")
gtest(SB(), default_tip)
import re
p = re.compile('')
gtest(re.sub, '''\
tiptest(re.sub, '''\
(pattern, repl, string, count=0, flags=0)
Return the string obtained by replacing the leftmost
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''')
gtest(p.sub, '''\
tiptest(p.sub, '''\
(repl, string, count=0)
Return the string obtained by replacing the leftmost \
non-overlapping occurrences o...''')

def test_signature_wrap(self):
if textwrap.TextWrapper.__doc__ is not None:
self.assertEqual(signature(textwrap.TextWrapper), '''\
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=' [...]')''')

def test_properly_formated(self):

def foo(s='a'*100):
pass

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

indent = calltip._INDENT

str_foo = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
"aaaaaaaaaa')"
str_bar = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
"aaaaaaaaaa')\nHello Guido"
str_baz = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
"aaaaaaaaaa', z='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"\
"bbbbbbbbbbbbbbbbb\n" + indent + "bbbbbbbbbbbbbbbbbbbbbb"\
"bbbbbbbbbbbbbbbbbbbbbb')"

self.assertEqual(calltip.get_argspec(foo), str_foo)
self.assertEqual(calltip.get_argspec(bar), str_bar)
self.assertEqual(calltip.get_argspec(baz), str_baz)
sfoo = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
"aaaaaaaaaa')"
sbar = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
"aaaaaaaaaa')\nHello Guido"
sbaz = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
"aaaaaaaaaa', z='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"\
"bbbbbbbbbbbbbbbbb\n" + indent + "bbbbbbbbbbbbbbbbbbbbbb"\
"bbbbbbbbbbbbbbbbbbbbbb')"

for func,doc in [(foo, sfoo), (bar, sbar), (baz, sbaz)]:
with self.subTest(func=func, doc=doc):
self.assertEqual(get_spec(func), doc)

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

def test_multiline_docstring(self):
# Test fewer lines than max.
self.assertEqual(signature(range),
self.assertEqual(get_spec(range),
"range(stop) -> range object\n"
"range(start, stop[, step]) -> range object")

# Test max lines
self.assertEqual(signature(bytes), '''\
self.assertEqual(get_spec(bytes), '''\
bytes(iterable_of_ints) -> bytes
bytes(string, encoding[, errors]) -> bytes
bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer
Expand All @@ -150,7 +153,7 @@ def test_multiline_docstring(self):
# Test more than max lines
def f(): pass
f.__doc__ = 'a\n' * 15
self.assertEqual(signature(f), '()' + '\na' * calltip._MAX_LINES)
self.assertEqual(get_spec(f), '()' + '\na' * calltip._MAX_LINES)

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

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

def test_methods(self):
doc = '\ndoc' if TC.__doc__ is not None else ''
for meth in (TC.t1, TC.t2, TC.t3, TC.t4, TC.t5, TC.t6, TC.__call__):
self.assertEqual(signature(meth), meth.tip + doc)
self.assertEqual(signature(TC.cm), "(a)" + doc)
self.assertEqual(signature(TC.sm), "(b)" + doc)
with self.subTest(meth=meth):
self.assertEqual(get_spec(meth), meth.tip + doc)
self.assertEqual(get_spec(TC.cm), "(a)" + doc)
self.assertEqual(get_spec(TC.sm), "(b)" + doc)

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

def test_starred_parameter(self):
# test that starred first parameter is *not* removed from argspec
class C:
def m1(*args): pass
c = C()
for meth, mtip in ((C.m1, '(*args)'), (c.m1, "(*args)"),):
self.assertEqual(signature(meth), mtip)
with self.subTest(meth=meth, mtip=mtip):
self.assertEqual(get_spec(meth), mtip)

def test_invalid_method_signature(self):
def test_invalid_method_get_spec(self):
class C:
def m2(**kwargs): pass
class Test:
def __call__(*, a): pass

mtip = calltip._invalid_method
self.assertEqual(signature(C().m2), mtip)
self.assertEqual(signature(Test()), mtip)
self.assertEqual(get_spec(C().m2), mtip)
self.assertEqual(get_spec(Test()), mtip)

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

def test_no_docstring(self):
def nd(s):
pass
TC.nd = nd
self.assertEqual(signature(nd), "(s)")
self.assertEqual(signature(TC.nd), "(s)")
self.assertEqual(signature(tc.nd), "()")
for meth, mtip in ((TC.nd, "(self)"), (tc.nd, "()")):
with self.subTest(meth=meth, mtip=mtip):
self.assertEqual(get_spec(meth), mtip)

def test_attribute_exception(self):
class NoCall:
Expand All @@ -229,11 +233,13 @@ def __call__(self, ci):
for meth, mtip in ((NoCall, default_tip), (CallA, default_tip),
(NoCall(), ''), (CallA(), '(a, b, c)'),
(CallB(), '(ci)')):
self.assertEqual(signature(meth), mtip)
with self.subTest(meth=meth, mtip=mtip):
self.assertEqual(get_spec(meth), mtip)

def test_non_callables(self):
for obj in (0, 0.0, '0', b'0', [], {}):
self.assertEqual(signature(obj), '')
with self.subTest(obj=obj):
self.assertEqual(get_spec(obj), '')


class Get_entityTest(unittest.TestCase):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Make calltip reminder about '/' meaning positional-only less obtrusive by
only adding it when there is room on the first line.