Issue #16630: Make Idle calltips work even when __getattr__ raises.
Initial patch by Roger Serwy.
diff --git a/Lib/idlelib/CallTips.py b/Lib/idlelib/CallTips.py
index 0349199..4ac28ed 100644
--- a/Lib/idlelib/CallTips.py
+++ b/Lib/idlelib/CallTips.py
@@ -134,56 +134,60 @@
"""Get a string describing the arguments for the given object,
only if it is callable."""
arg_text = ""
- if ob is not None and hasattr(ob, '__call__'):
- arg_offset = 0
- if type(ob) in (types.ClassType, types.TypeType):
- # Look for the highest __init__ in the class chain.
- fob = _find_constructor(ob)
- if fob is None:
- fob = lambda: None
- else:
- arg_offset = 1
- elif type(ob) == types.MethodType:
- # bit of a hack for methods - turn it into a function
- # and drop the "self" param for bound methods
- fob = ob.im_func
- if ob.im_self:
- arg_offset = 1
- elif type(ob.__call__) == types.MethodType:
- # a callable class instance
- fob = ob.__call__.im_func
+ try:
+ ob_call = ob.__call__
+ except BaseException:
+ return arg_text
+
+ arg_offset = 0
+ if type(ob) in (types.ClassType, types.TypeType):
+ # Look for the highest __init__ in the class chain.
+ fob = _find_constructor(ob)
+ if fob is None:
+ fob = lambda: None
+ else:
arg_offset = 1
- else:
- fob = ob
- # Try to build one for Python defined functions
- if type(fob) in [types.FunctionType, types.LambdaType]:
- argcount = fob.func_code.co_argcount
- real_args = fob.func_code.co_varnames[arg_offset:argcount]
- defaults = fob.func_defaults or []
- defaults = list(map(lambda name: "=%s" % repr(name), defaults))
- defaults = [""] * (len(real_args) - len(defaults)) + defaults
- items = map(lambda arg, dflt: arg + dflt, real_args, defaults)
- if fob.func_code.co_flags & 0x4:
- items.append("*args")
- if fob.func_code.co_flags & 0x8:
- items.append("**kwds")
- arg_text = ", ".join(items)
- arg_text = "(%s)" % re.sub("(?<!\d)\.\d+", "<tuple>", arg_text)
- # See if we can use the docstring
- if isinstance(ob.__call__, types.MethodType):
- doc = ob.__call__.__doc__
- else:
- doc = getattr(ob, "__doc__", "")
- if doc:
- doc = doc.lstrip()
- pos = doc.find("\n")
- if pos < 0 or pos > 70:
- pos = 70
- if arg_text:
- arg_text += "\n"
- arg_text += doc[:pos]
+ elif type(ob) == types.MethodType:
+ # bit of a hack for methods - turn it into a function
+ # and drop the "self" param for bound methods
+ fob = ob.im_func
+ if ob.im_self:
+ arg_offset = 1
+ elif type(ob_call) == types.MethodType:
+ # a callable class instance
+ fob = ob_call.im_func
+ arg_offset = 1
+ else:
+ fob = ob
+ # Try to build one for Python defined functions
+ if type(fob) in [types.FunctionType, types.LambdaType]:
+ argcount = fob.func_code.co_argcount
+ real_args = fob.func_code.co_varnames[arg_offset:argcount]
+ defaults = fob.func_defaults or []
+ defaults = list(map(lambda name: "=%s" % repr(name), defaults))
+ defaults = [""] * (len(real_args) - len(defaults)) + defaults
+ items = map(lambda arg, dflt: arg + dflt, real_args, defaults)
+ if fob.func_code.co_flags & 0x4:
+ items.append("*args")
+ if fob.func_code.co_flags & 0x8:
+ items.append("**kwds")
+ arg_text = ", ".join(items)
+ arg_text = "(%s)" % re.sub("(?<!\d)\.\d+", "<tuple>", arg_text)
+ # See if we can use the docstring
+ if isinstance(ob_call, types.MethodType):
+ doc = ob_call.__doc__
+ else:
+ doc = getattr(ob, "__doc__", "")
+ if doc:
+ doc = doc.lstrip()
+ pos = doc.find("\n")
+ if pos < 0 or pos > 70:
+ pos = 70
+ if arg_text:
+ arg_text += "\n"
+ arg_text += doc[:pos]
return arg_text
if __name__ == '__main__':
from unittest import main
- main('idlelib.idle_test.test_calltips', verbosity=2, exit=False)
+ main('idlelib.idle_test.test_calltips', verbosity=2)
diff --git a/Lib/idlelib/idle_test/test_calltips.py b/Lib/idlelib/idle_test/test_calltips.py
index 0560a51..a75d88d 100644
--- a/Lib/idlelib/idle_test/test_calltips.py
+++ b/Lib/idlelib/idle_test/test_calltips.py
@@ -3,6 +3,7 @@
CTi = ct.CallTips() # needed for get_entity test in 2.7
import types
+default_tip = ''
# Test Class TC is used in multiple get_argspec test methods
class TC(object):
@@ -41,7 +42,6 @@
# but a red buildbot is better than a user crash (as has happened).
# For a simple mismatch, change the expected output to the actual.
-
def test_builtins(self):
# 2.7 puts '()\n' where 3.x does not, other minor differences
@@ -65,8 +65,7 @@
gtest(List.append, append_doc)
gtest(types.MethodType, '()\ninstancemethod(function, instance, class)')
- gtest(SB(), '')
-
+ gtest(SB(), default_tip)
def test_functions(self):
def t1(): 'doc'
@@ -92,9 +91,8 @@
def test_bound_methods(self):
# test that first parameter is correctly removed from argspec
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(tc), "(ci)\ndoc")
def test_no_docstring(self):
def nd(s): pass
@@ -103,6 +101,17 @@
self.assertEqual(signature(TC.nd), "(s)")
self.assertEqual(signature(tc.nd), "()")
+ def test_attribute_exception(self):
+ class NoCall(object):
+ def __getattr__(self, name):
+ raise BaseException
+ class Call(NoCall):
+ def __call__(self, ci):
+ pass
+ for meth, mtip in ((NoCall, '()'), (Call, '()'),
+ (NoCall(), ''), (Call(), '(ci)')):
+ self.assertEqual(signature(meth), mtip)
+
def test_non_callables(self):
for obj in (0, 0.0, '0', b'0', [], {}):
self.assertEqual(signature(obj), '')