Index: CallTips.py =================================================================== --- CallTips.py (revision 59481) +++ CallTips.py (working copy) @@ -5,9 +5,10 @@ which disappear when you type a closing parenthesis. """ -import re import sys import types +import inspect +from repr import repr as limited_len_repr import CallTipWindow from HyperParser import HyperParser @@ -74,11 +75,12 @@ name = hp.get_expression() if not name or (not evalfuncs and name.find('(') != -1): return - arg_text = self.fetch_tip(name) - if not arg_text: + arg_text, doc = self.fetch_tip(name) + if not (arg_text or doc): return self.calltip = self._make_calltip_window() - self.calltip.showtip(arg_text, sur_paren[0], sur_paren[1]) + self.calltip.showtip(name, arg_text, doc, + sur_paren[0], sur_paren[1]) def fetch_tip(self, name): """Return the argument list and docstring of a function or class @@ -103,7 +105,7 @@ (name,), {}) else: entity = self.get_entity(name) - return get_arg_text(entity) + return get_arg_text_and_doc(entity) def get_entity(self, name): "Lookup name in a namespace spanning sys.modules and __main.dict__" @@ -119,58 +121,62 @@ # 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 + return class_ob.__init__ 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""" - arg_text = "" - if ob is not None: - 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 +def get_arg_text_and_doc(ob): + """Get the call signature and the doc-string for the given object""" + if ob is None: + return "", "" + + fob = ob + arg_offset = 0 # set to 1 to drop initial argument, e.g. "self" + doc = inspect.getdoc(ob) + if isinstance(ob, (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 + # use __init__'s doc-string if available, otherwise use the + # class's doc-string + doc = inspect.getdoc(fob) or doc + elif isinstance(ob, (types.FunctionType, + types.LambdaType, + types.BuiltinFunctionType)): + pass + elif isinstance(ob, (types.MethodType, types.BuiltinMethodType)): + arg_offset = 1 + elif callable(ob): + try: + fob = ob.__call__ + except AttributeError: + pass 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+", "", arg_text) - # See if we can use the docstring - 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 + arg_offset = 1 + doc = inspect.getdoc(fob) + if not doc: + doc = "" + arg_text = "" + # Try to get the call signature for Python defined functions/methods + if isinstance(fob, (types.FunctionType, + types.LambdaType, + types.MethodType)): + args, varargs, varkw, defaults = inspect.getargspec(fob) + def formatvalue(obj): + return "=" + limited_len_repr(obj) + arg_text = inspect.formatargspec(args[arg_offset:], + varargs, varkw, defaults, + formatvalue=formatvalue) + + return arg_text, doc + ################################################# # # Test code @@ -179,43 +185,80 @@ 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)): "(, c, )" + def t3(a, *args): "(a, *args)" + def t4(*args): "(*args)" + def t5(a, *args): "(a, *args)" + def t6(a, b=None, *args, **kw): "(a, b=None, *args, **kw)" + def t7((a, b), c, (d, e)): "((a, b), c, (d, e))" - class TC(object): - "(ai=None, ...)" - def __init__(self, ai=None, *b): "(ai=None, ...)" + class TC1(object): + "this should not be used at all" + def __init__(self, a=None, *args, **kw): "(a=None, *args, **kw)" 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)): "(, c, )" + def t2(self, a, b=None): "(a, b=None)" + def t3(self, a, *args): "(a, *args)" + def t4(self, *args): "(*args)" + def t5(self, a, *args): "(a, *args)" + def t6(self, a, b=None, *args, **kw): "(a, b=None, *args, **kw)" + def t7(self, (a, b), c, (d, e)): "((a, b), c, (d, e))" + @classmethod + def t8(klass, a, b=None, *args, **kw): "(a, b=None, *args, **kw)" + @staticmethod + def t9(a, b=None, *args, **kw): "(a, b=None, *args, **kw)" + def __call__(self, a, b=None, *args, **kw): "(a, b=None, *args, **kw)" - def test(tests): + class TC2(): + "this should not be used at all" + def __init__(self, a=None, *args, **kw): "(a=None, *args, **kw)" + def t1(self): "()" + def t2(self, a, b=None): "(a, b=None)" + def t3(self, a, *args): "(a, *args)" + def t4(self, *args): "(*args)" + def t5(self, a, *args): "(a, *args)" + def t6(self, a, b=None, *args, **kw): "(a, b=None, *args, **kw)" + def t7(self, (a, b), c, (d, e)): "((a, b), c, (d, e))" + @classmethod + def t8(klass, a, b=None, *args, **kw): "(a, b=None, *args, **kw)" + @staticmethod + def t9(a, b=None, *args, **kw): "(a, b=None, *args, **kw)" + def __call__(self, a, b=None, *args, **kw): "(a, b=None, *args, **kw)" + + tc1 = TC1() + tc2 = TC2() + + tests = """t1 t2 t3 t4 t5 t6 t7 + tc1.t1 tc1.t2 tc1.t3 tc1.t4 tc1.t5 tc1.t6 tc1.t7 + TC1 TC1.t8 TC1.t9 tc1 + tc2.t1 tc2.t2 tc2.t3 tc2.t4 tc2.t5 tc2.t6 tc2.t7 + TC2 TC2.t8 TC2.t9 tc2""".split() + builtin_tests = ["int", "dir", "execfile", "[].append", "sys.exit"] + + def test(tests, builtin_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)) + for name in tests: + t = eval(name) + if isinstance(t, (types.ClassType, types.TypeType)): + expected = t.__init__.__doc__ + elif isinstance(t, (types.InstanceType, TC1, TC2)): + expected = t.__call__.__doc__ + else: + expected = t.__doc__ + expected = (expected, expected) + # exercise fetch_tip(), not just get_arg_text_and_doc() + arg_text, doc = ct.fetch_tip(name) + if (arg_text, doc) != expected: + failed.append(name) + print "%s - expected %r, but got %r" % (name, expected, + (arg_text, doc)) + for name in builtin_tests: + arg_text, doc = ct.fetch_tip(name) + if arg_text != '' or not doc: + failed.append(name) + expected = "('', )" + print "%s - expected %s, but got %r" % (name, expected, + (arg_text, doc)) + print "%d of %d tests failed" % (len(failed), + len(tests) + len(builtin_tests)) - 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) + test(tests, builtin_tests) Index: CallTipWindow.py =================================================================== --- CallTipWindow.py (revision 59481) +++ CallTipWindow.py (working copy) @@ -6,11 +6,15 @@ """ from Tkinter import * +from configHandler import idleConf +import textView + HIDE_VIRTUAL_EVENT_NAME = "<>" HIDE_SEQUENCES = ("", "") CHECKHIDE_VIRTUAL_EVENT_NAME = "<>" CHECKHIDE_SEQUENCES = ("", "") CHECKHIDE_TIME = 100 # miliseconds +SHOW_FULL_DOC_EVENT_NAME = "<>" MARK_RIGHT = "calltipwindowregion_right" @@ -22,6 +26,7 @@ self.parenline = self.parencol = None self.lastline = None self.hideid = self.checkhideid = None + self.showfulldoc_seqs_and_id = None def position_window(self): """Check if needs to reposition the window, and if so - do it.""" @@ -44,10 +49,12 @@ y = box[1] + box[3] + self.widget.winfo_rooty() self.tipwindow.wm_geometry("+%d+%d" % (x, y)) - def showtip(self, text, parenleft, parenright): + def showtip(self, name, arg_text, doc, + parenleft, parenright): """Show the calltip, bind events which will close it and reposition it. """ # truncate overly long calltip + text = '\n'.join([x for x in [arg_text] + doc.splitlines()[:1] if x]) if len(text) >= 79: textlines = text.splitlines() for i, line in enumerate(textlines): @@ -89,6 +96,28 @@ for seq in HIDE_SEQUENCES: self.widget.event_add(HIDE_VIRTUAL_EVENT_NAME, seq) + if doc and (len(doc) >= 79 or '\n' in doc): + # bind the force-open-calltip sequence(s) to open the full + # doc-string in a new window + text = '' + if arg_text: + text += name + arg_text + if text and doc: + text += '\n\n' + text += doc + title = "In-code documentation for %s" % name + def show_full_doc_event(event): + textView.view_text(self.widget, title, text) + return "break" + seqs = idleConf.GetOption( + 'extensions', 'CallTips_cfgBindings', 'force-open-calltip', + type='str', default='') + seqs = seqs.split() + for seq in seqs: + self.widget.event_add(SHOW_FULL_DOC_EVENT_NAME, seq) + id = self.widget.bind(SHOW_FULL_DOC_EVENT_NAME, show_full_doc_event) + self.showfulldoc_seqs_and_id = (seqs, id) + def checkhide_event(self, event=None): if not self.tipwindow: # If the event was triggered by the same event that unbinded @@ -122,6 +151,12 @@ self.widget.event_delete(HIDE_VIRTUAL_EVENT_NAME, seq) self.widget.unbind(HIDE_VIRTUAL_EVENT_NAME, self.hideid) self.hideid = None + if self.showfulldoc_seqs_and_id is not None: + seqs, id = self.showfulldoc_seqs_and_id + for seq in seqs: + self.widget.event_delete(SHOW_FULL_DOC_EVENT_NAME, seq) + self.widget.unbind(SHOW_FULL_DOC_EVENT_NAME, id) + self.showfulldoc_seqs_and_id = None self.label.destroy() self.label = None @@ -158,8 +193,19 @@ root.mainloop() def calltip_show(self, event): - self.calltip.showtip("Hello world") + paren_left = self.text.index("insert") + paren_right = self.text.search(")", paren_left) + if not paren_right: + paren_right = "end" + self.calltip.showtip("string.strip", "(s, chars=None)", + """strip(s [,chars]) -> string +Return a copy of the string s with leading and trailing +whitespace removed. +If chars is given and not None, remove characters in chars instead. +If chars is unicode, S will be converted to unicode before stripping.""", + paren_left, paren_right) + def calltip_hide(self, event): self.calltip.hidetip()