diff options
Diffstat (limited to 'Lib/idlelib/CallTips.py')
| -rw-r--r-- | Lib/idlelib/CallTips.py | 297 |
1 files changed, 169 insertions, 128 deletions
diff --git a/Lib/idlelib/CallTips.py b/Lib/idlelib/CallTips.py index d533ce1..3c8c096 100644 --- a/Lib/idlelib/CallTips.py +++ b/Lib/idlelib/CallTips.py @@ -8,6 +8,7 @@ which disappear when you type a closing parenthesis. import re import sys import types +import inspect from idlelib import CallTipWindow from idlelib.HyperParser import HyperParser @@ -25,28 +26,26 @@ class CallTips: def __init__(self, editwin=None): if editwin is None: # subprocess and test self.editwin = None - return - self.editwin = editwin - self.text = editwin.text - self.calltip = None - self._make_calltip_window = self._make_tk_calltip_window + else: + self.editwin = editwin + self.text = editwin.text + self.active_calltip = None + self._calltip_window = self._make_tk_calltip_window def close(self): - self._make_calltip_window = None + self._calltip_window = None def _make_tk_calltip_window(self): # See __init__ for usage return CallTipWindow.CallTip(self.text) def _remove_calltip_window(self, event=None): - if self.calltip: - self.calltip.hidetip() - self.calltip = None + if self.active_calltip: + self.active_calltip.hidetip() + self.active_calltip = None def force_open_calltip_event(self, event): - """Happens when the user really wants to open a CallTip, even if a - function call is needed. - """ + "The user selected the menu entry or hotkey, open the tip." self.open_calltip(True) def try_open_calltip_event(self, event): @@ -57,10 +56,7 @@ class CallTips: self.open_calltip(False) def refresh_calltip_event(self, event): - """If there is already a calltip window, check if it is still needed, - and if so, reload it. - """ - if self.calltip and self.calltip.is_active(): + if self.active_calltip and self.active_calltip.is_active(): self.open_calltip(False) def open_calltip(self, evalfuncs): @@ -71,21 +67,23 @@ class CallTips: if not sur_paren: return hp.set_index(sur_paren[0]) - expression = hp.get_expression() - if not expression or (not evalfuncs and expression.find('(') != -1): + expression = hp.get_expression() + if not expression: + return + if not evalfuncs and (expression.find('(') != -1): return - arg_text = self.fetch_tip(expression) - if not arg_text: + argspec = self.fetch_tip(expression) + if not argspec: return - self.calltip = self._make_calltip_window() - self.calltip.showtip(arg_text, sur_paren[0], sur_paren[1]) + self.active_calltip = self._calltip_window() + self.active_calltip.showtip(argspec, sur_paren[0], sur_paren[1]) def fetch_tip(self, expression): - """Return the argument list and docstring of a function or class + """Return the argument list and docstring of a function or class. If there is a Python subprocess, get the calltip there. Otherwise, - either fetch_tip() is running in the subprocess itself or it was called - in an IDLE EditorWindow before any script had been run. + either this fetch_tip() is running in the subprocess or it was + called in an IDLE running without the subprocess. The subprocess environment is that of the most recently run script. If two unrelated modules are being edited some calltips in the current @@ -102,125 +100,168 @@ class CallTips: return rpcclt.remotecall("exec", "get_the_calltip", (expression,), {}) else: - entity = self.get_entity(expression) - return get_arg_text(entity) + return get_argspec(get_entity(expression)) - def get_entity(self, expression): - """Return the object corresponding to expression evaluated - in a namespace spanning sys.modules and __main.dict__. - """ - if expression: - namespace = sys.modules.copy() - namespace.update(__main__.__dict__) - try: - return eval(expression, namespace) - except BaseException: - # An uncaught exception closes idle, and eval can raise any - # exception, especially if user classes are involved. - return None - -def _find_constructor(class_ob): - # Given a class object, return a function object used for the - # constructor (ie, __init__() ) or None if we can't find one. - try: - return class_ob.__init__.im_func - except AttributeError: - for base in class_ob.__bases__: - rc = _find_constructor(base) - if rc is not None: return rc - return None - -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 - # but we drop the "self" param. - fob = ob.im_func - arg_offset = 1 +def get_entity(expression): + """Return the object corresponding to expression evaluated + in a namespace spanning sys.modules and __main.dict__. + """ + if expression: + namespace = sys.modules.copy() + namespace.update(__main__.__dict__) + try: + return eval(expression, namespace) + except BaseException: + # An uncaught exception closes idle, and eval can raise any + # exception, especially if user classes are involved. + return None + +# The following are used in both get_argspec and tests +_first_param = re.compile('(?<=\()\w*\,?\s*') +_default_callable_argspec = "No docstring, see docs." + +def get_argspec(ob): + '''Return a string describing the arguments and return of a callable object. + + 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 - # 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("...") - if fob.func_code.co_flags & 0x8: - items.append("***") - arg_text = ", ".join(items) - arg_text = "(%s)" % re.sub("\.\d+", "<tuple>", arg_text) - # See if we can use the docstring - doc = getattr(ob, "__doc__", "") + 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 arg_text: - arg_text += "\n" - arg_text += doc[:pos] - return arg_text + if argspec: + argspec += "\n" + argspec += doc[:pos] + if not argspec: + argspec = _default_callable_argspec + return argspec ################################################# # -# Test code -# -if __name__=='__main__': +# Test code tests CallTips.fetch_tip, get_entity, and get_argspec +def main(): + # Putting expected in docstrings results in doubled tips for test def t1(): "()" def t2(a, b=None): "(a, b=None)" - def t3(a, *args): "(a, ...)" - def t4(*args): "(...)" - def t5(a, *args): "(a, ...)" - def t6(a, b=None, *args, **kw): "(a, b=None, ..., ***)" - def t7((a, b), c, (d, e)): "(<tuple>, c, <tuple>)" + def t3(a, *args): "(a, *args)" + def t4(*args): "(*args)" + def t5(a, b=None, *args, **kw): "(a, b=None, *args, **kw)" class TC(object): - "(ai=None, ...)" - def __init__(self, ai=None, *b): "(ai=None, ...)" - def t1(self): "()" - def t2(self, ai, b=None): "(ai, b=None)" - def t3(self, ai, *args): "(ai, ...)" - def t4(self, *args): "(...)" - def t5(self, ai, *args): "(ai, ...)" - def t6(self, ai, b=None, *args, **kw): "(ai, b=None, ..., ***)" - def t7(self, (ai, b), c, (d, e)): "(<tuple>, c, <tuple>)" - - def test(tests): - ct = CallTips() - failed=[] - for t in tests: - expected = t.__doc__ + "\n" + t.__doc__ - name = t.__name__ - # exercise fetch_tip(), not just get_arg_text() - try: - qualified_name = "%s.%s" % (t.im_class.__name__, name) - except AttributeError: - qualified_name = name - arg_text = ct.fetch_tip(qualified_name) - if arg_text != expected: - failed.append(t) - fmt = "%s - expected %s, but got %s" - print fmt % (t.__name__, expected, get_arg_text(t)) - print "%d of %d tests failed" % (len(failed), len(tests)) + "(ai=None, *b)" + def __init__(self, ai=None, *b): "(self, ai=None, *b)" + def t1(self): "(self)" + def t2(self, ai, b=None): "(self, ai, b=None)" + def t3(self, ai, *args): "(self, ai, *args)" + def t4(self, *args): "(self, *args)" + def t5(self, ai, b=None, *args, **kw): "(self, ai, b=None, *args, **kw)" + def t6(no, self): "(no, self)" + @classmethod + def cm(cls, a): "(cls, a)" + @staticmethod + def sm(b): "(b)" + def __call__(self, ci): "(self, ci)" tc = TC() - tests = (t1, t2, t3, t4, t5, t6, t7, - TC, tc.t1, tc.t2, tc.t3, tc.t4, tc.t5, tc.t6, tc.t7) - test(tests) + # Python classes that inherit builtin methods + class Int(int): "Int(x[, base]) -> integer" + class List(list): "List() -> new empty list" + # Simulate builtin with no docstring for default argspec test + class SB: __call__ = None + + __main__.__dict__.update(locals()) # required for get_entity eval() + + num_tests = num_fail = 0 + tip = CallTips().fetch_tip + + def test(expression, expected): + nonlocal num_tests, num_fail + num_tests += 1 + argspec = tip(expression) + if argspec != expected: + num_fail += 1 + fmt = "%s - expected\n%r\n - but got\n%r" + print(fmt % (expression, expected, argspec)) + + def test_builtins(): + # if first line of a possibly multiline compiled docstring changes, + # must change corresponding test string + test('int', "int(x=0) -> integer") + test('Int', Int.__doc__) + test('types.MethodType', "method(function, instance)") + test('list', "list() -> new empty list") + test('List', List.__doc__) + test('list.__new__', + 'T.__new__(S, ...) -> a new object with type S, a subtype of T') + test('list.__init__', + 'x.__init__(...) initializes x; see help(type(x)) for signature') + append_doc = "L.append(object) -> None -- append object to end" + test('list.append', append_doc) + test('[].append', append_doc) + test('List.append', append_doc) + test('SB()', _default_callable_argspec) + + def test_funcs(): + for func in (t1, t2, t3, t4, t5, TC,): + fdoc = func.__doc__ + test(func.__name__, fdoc + "\n" + fdoc) + for func in (TC.t1, TC.t2, TC.t3, TC.t4, TC.t5, TC.t6, TC.sm, + TC.__call__): + fdoc = func.__doc__ + test('TC.'+func.__name__, fdoc + "\n" + fdoc) + fdoc = TC.cm.__func__.__doc__ + test('TC.cm.__func__', fdoc + "\n" + fdoc) + + def test_methods(): + # test that first parameter is correctly removed from argspec + # using _first_param re to calculate expected masks re errors + for meth, mdoc in ((tc.t1, "()"), (tc.t4, "(*args)"), (tc.t6, "(self)"), + (TC.cm, "(a)"),): + test('tc.'+meth.__name__, mdoc + "\n" + meth.__doc__) + test('tc', "(ci)" + "\n" + tc.__call__.__doc__) + # directly test that re works to delete unicode parameter name + uni = "(A\u0391\u0410\u05d0\u0627\u0905\u1e00\u3042, a)" # various As + assert _first_param.sub('', uni) == '(a)' + + def test_non_callables(): + # expression evaluates, but not to a callable + for expr in ('0', '0.0' 'num_tests', b'num_tests', '[]', '{}'): + test(expr, '') + # expression does not evaluate, but raises an exception + for expr in ('1a', 'xyx', 'num_tests.xyz', '[int][1]', '{0:int}[1]'): + test(expr, '') + + test_builtins() + test_funcs() + test_non_callables() + test_methods() + + print("%d of %d tests failed" % (num_fail, num_tests)) + +if __name__ == '__main__': + main() |
