Issue #16630: Make Idle calltips work even when __getattr__ raises.

Initial patch by Roger Serwy.
This commit is contained in:
Terry Jan Reedy 2014-01-21 15:36:51 -05:00
parent 65fd0592fb
commit 715476d8e3
2 changed files with 50 additions and 36 deletions

View File

@ -118,47 +118,49 @@ def get_entity(expression):
# The following are used in both get_argspec and tests # The following are used in both get_argspec and tests
_first_param = re.compile('(?<=\()\w*\,?\s*') _first_param = re.compile('(?<=\()\w*\,?\s*')
_default_callable_argspec = "No docstring, see docs." _default_callable_argspec = "See source or doc"
def get_argspec(ob): def get_argspec(ob):
'''Return a string describing the arguments and return of a callable object. '''Return a string describing the signature of a callable object, or ''.
For Python-coded functions and methods, the first line is introspected. For Python-coded functions and methods, the first line is introspected.
Delete 'self' parameter for classes (.__init__) and bound methods. Delete 'self' parameter for classes (.__init__) and bound methods.
The last line is the first line of the doc string. For builtins, this typically The last line is the first line of the doc string. For builtins, this typically
includes the arguments in addition to the return value. includes the arguments in addition to the return value.
''' '''
argspec = "" argspec = ""
if hasattr(ob, '__call__'): try:
if isinstance(ob, type): ob_call = ob.__call__
fob = getattr(ob, '__init__', None) except BaseException:
elif isinstance(ob.__call__, types.MethodType): return argspec
fob = ob.__call__ if isinstance(ob, type):
else: fob = ob.__init__
fob = ob elif isinstance(ob_call, types.MethodType):
if isinstance(fob, (types.FunctionType, types.MethodType)): fob = ob_call
argspec = inspect.formatargspec(*inspect.getfullargspec(fob)) else:
if (isinstance(ob, (type, types.MethodType)) or fob = ob
isinstance(ob.__call__, types.MethodType)): if isinstance(fob, (types.FunctionType, types.MethodType)):
argspec = _first_param.sub("", argspec) argspec = inspect.formatargspec(*inspect.getfullargspec(fob))
if (isinstance(ob, (type, types.MethodType)) or
isinstance(ob_call, types.MethodType)):
argspec = _first_param.sub("", argspec)
if isinstance(ob.__call__, types.MethodType): if isinstance(ob_call, types.MethodType):
doc = ob.__call__.__doc__ doc = ob_call.__doc__
else: else:
doc = getattr(ob, "__doc__", "") doc = getattr(ob, "__doc__", "")
if doc: if doc:
doc = doc.lstrip() doc = doc.lstrip()
pos = doc.find("\n") pos = doc.find("\n")
if pos < 0 or pos > 70: if pos < 0 or pos > 70:
pos = 70 pos = 70
if argspec: if argspec:
argspec += "\n" argspec += "\n"
argspec += doc[:pos] argspec += doc[:pos]
if not argspec: if not argspec:
argspec = _default_callable_argspec argspec = _default_callable_argspec
return argspec return argspec
if __name__ == '__main__': if __name__ == '__main__':
from unittest import main from unittest import main
main('idlelib.idle_test.test_calltips', verbosity=2, exit=False) main('idlelib.idle_test.test_calltips', verbosity=2)

View File

@ -2,6 +2,7 @@ import unittest
import idlelib.CallTips as ct import idlelib.CallTips as ct
import types import types
default_tip = ct._default_callable_argspec
# Test Class TC is used in multiple get_argspec test methods # Test Class TC is used in multiple get_argspec test methods
class TC(): class TC():
@ -63,7 +64,7 @@ class Get_signatureTest(unittest.TestCase):
gtest(List.append, append_doc) gtest(List.append, append_doc)
gtest(types.MethodType, "method(function, instance)") gtest(types.MethodType, "method(function, instance)")
gtest(SB(), ct._default_callable_argspec) gtest(SB(), default_tip)
def test_functions(self): def test_functions(self):
def t1(): 'doc' def t1(): 'doc'
@ -88,13 +89,13 @@ class Get_signatureTest(unittest.TestCase):
def test_bound_methods(self): def test_bound_methods(self):
# test that first parameter is correctly removed from argspec # test that first parameter is correctly removed from argspec
# using _first_param re to calculate expected masks re errors
for meth, mtip in ((tc.t1, "()"), (tc.t4, "(*args)"), (tc.t6, "(self)"), for meth, mtip in ((tc.t1, "()"), (tc.t4, "(*args)"), (tc.t6, "(self)"),
(TC.cm, "(a)"),): (tc.__call__, '(ci)'), (tc, '(ci)'), (TC.cm, "(a)"),):
self.assertEqual(signature(meth), mtip + "\ndoc") self.assertEqual(signature(meth), mtip + "\ndoc")
self.assertEqual(signature(tc), "(ci)\ndoc")
# directly test that re works to delete first parameter even when it def test_non_ascii_name(self):
# non-ascii chars, such as various forms of A. # test that re works to delete a first parameter name that
# includes non-ascii chars, such as various forms of A.
uni = "(A\u0391\u0410\u05d0\u0627\u0905\u1e00\u3042, a)" uni = "(A\u0391\u0410\u05d0\u0627\u0905\u1e00\u3042, a)"
assert ct._first_param.sub('', uni) == '(a)' assert ct._first_param.sub('', uni) == '(a)'
@ -105,6 +106,17 @@ class Get_signatureTest(unittest.TestCase):
self.assertEqual(signature(TC.nd), "(s)") self.assertEqual(signature(TC.nd), "(s)")
self.assertEqual(signature(tc.nd), "()") self.assertEqual(signature(tc.nd), "()")
def test_attribute_exception(self):
class NoCall:
def __getattr__(self, name):
raise BaseException
class Call(NoCall):
def __call__(self, ci):
pass
for meth, mtip in ((NoCall, default_tip), (Call, default_tip),
(NoCall(), ''), (Call(), '(ci)')):
self.assertEqual(signature(meth), mtip)
def test_non_callables(self): def test_non_callables(self):
for obj in (0, 0.0, '0', b'0', [], {}): for obj in (0, 0.0, '0', b'0', [], {}):
self.assertEqual(signature(obj), '') self.assertEqual(signature(obj), '')