From 715476d8e325c41b8fbdcd3fb22d6538aafc0c3e Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Tue, 21 Jan 2014 15:36:51 -0500 Subject: Issue #16630: Make Idle calltips work even when __getattr__ raises. Initial patch by Roger Serwy. --- Lib/idlelib/CallTips.py | 64 ++++++++++++++++++---------------- Lib/idlelib/idle_test/test_calltips.py | 24 +++++++++---- 2 files changed, 51 insertions(+), 37 deletions(-) diff --git a/Lib/idlelib/CallTips.py b/Lib/idlelib/CallTips.py index cb46a89..83bb638 100644 --- a/Lib/idlelib/CallTips.py +++ b/Lib/idlelib/CallTips.py @@ -118,47 +118,49 @@ def get_entity(expression): # The following are used in both get_argspec and tests _first_param = re.compile('(?<=\()\w*\,?\s*') -_default_callable_argspec = "No docstring, see docs." +_default_callable_argspec = "See source or doc" 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. Delete 'self' parameter for classes (.__init__) and bound methods. The last line is the first line of the doc string. For builtins, this typically includes the arguments in addition to the return value. - ''' argspec = "" - if hasattr(ob, '__call__'): - if isinstance(ob, type): - fob = getattr(ob, '__init__', None) - elif isinstance(ob.__call__, types.MethodType): - fob = ob.__call__ - else: - fob = ob - if isinstance(fob, (types.FunctionType, types.MethodType)): - 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): - 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 argspec: - argspec += "\n" - argspec += doc[:pos] - if not argspec: - argspec = _default_callable_argspec + try: + ob_call = ob.__call__ + except BaseException: + return argspec + if isinstance(ob, type): + fob = ob.__init__ + elif isinstance(ob_call, types.MethodType): + fob = ob_call + else: + fob = ob + if isinstance(fob, (types.FunctionType, types.MethodType)): + 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): + 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 argspec: + argspec += "\n" + argspec += doc[:pos] + if not argspec: + argspec = _default_callable_argspec return argspec 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 da52e88..18cc35c 100644 --- a/Lib/idlelib/idle_test/test_calltips.py +++ b/Lib/idlelib/idle_test/test_calltips.py @@ -2,6 +2,7 @@ import unittest import idlelib.CallTips as ct import types +default_tip = ct._default_callable_argspec # Test Class TC is used in multiple get_argspec test methods class TC(): @@ -63,7 +64,7 @@ class Get_signatureTest(unittest.TestCase): gtest(List.append, append_doc) gtest(types.MethodType, "method(function, instance)") - gtest(SB(), ct._default_callable_argspec) + gtest(SB(), default_tip) def test_functions(self): def t1(): 'doc' @@ -88,13 +89,13 @@ class Get_signatureTest(unittest.TestCase): def test_bound_methods(self): # 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)"), - (TC.cm, "(a)"),): + (tc.__call__, '(ci)'), (tc, '(ci)'), (TC.cm, "(a)"),): self.assertEqual(signature(meth), mtip + "\ndoc") - self.assertEqual(signature(tc), "(ci)\ndoc") - # directly test that re works to delete first parameter even when it - # non-ascii chars, such as various forms of A. + + def test_non_ascii_name(self): + # 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)" 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), "()") + 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): for obj in (0, 0.0, '0', b'0', [], {}): self.assertEqual(signature(obj), '') -- cgit v0.12