From 21334e72fd8c88b6cc875a4696fb2b5e295c7e08 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Tue, 21 Jan 2014 15:36:36 -0500 Subject: [PATCH] Issue #16630: Make Idle calltips work even when __getattr__ raises. Initial patch by Roger Serwy. --- Lib/idlelib/CallTips.py | 100 +++++++++++++------------ Lib/idlelib/idle_test/test_calltips.py | 19 +++-- 2 files changed, 66 insertions(+), 53 deletions(-) diff --git a/Lib/idlelib/CallTips.py b/Lib/idlelib/CallTips.py index 0349199963..4ac28edc2b 100644 --- a/Lib/idlelib/CallTips.py +++ b/Lib/idlelib/CallTips.py @@ -134,56 +134,60 @@ def get_arg_text(ob): """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 - 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("(?", arg_text) - # See if we can use the docstring - if isinstance(ob.__call__, types.MethodType): - doc = ob.__call__.__doc__ + 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: - 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] + 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 + 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("(?", 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 0560a51223..a75d88df94 100644 --- a/Lib/idlelib/idle_test/test_calltips.py +++ b/Lib/idlelib/idle_test/test_calltips.py @@ -3,6 +3,7 @@ import idlelib.CallTips as ct 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 @@ class Get_signatureTest(unittest.TestCase): # 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 @@ class Get_signatureTest(unittest.TestCase): 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 @@ class Get_signatureTest(unittest.TestCase): 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 @@ class Get_signatureTest(unittest.TestCase): 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), '') -- 2.50.1