Home | History | Annotate | Download | only in idlelib
      1 """CallTips.py - An IDLE Extension to Jog Your Memory
      2 
      3 Call Tips are floating windows which display function, class, and method
      4 parameter and docstring information when you type an opening parenthesis, and
      5 which disappear when you type a closing parenthesis.
      6 
      7 """
      8 import re
      9 import sys
     10 import types
     11 
     12 from idlelib import CallTipWindow
     13 from idlelib.HyperParser import HyperParser
     14 
     15 import __main__
     16 
     17 class CallTips:
     18 
     19     menudefs = [
     20         ('edit', [
     21             ("Show call tip", "<<force-open-calltip>>"),
     22         ])
     23     ]
     24 
     25     def __init__(self, editwin=None):
     26         if editwin is None:  # subprocess and test
     27             self.editwin = None
     28             return
     29         self.editwin = editwin
     30         self.text = editwin.text
     31         self.calltip = None
     32         self._make_calltip_window = self._make_tk_calltip_window
     33 
     34     def close(self):
     35         self._make_calltip_window = None
     36 
     37     def _make_tk_calltip_window(self):
     38         # See __init__ for usage
     39         return CallTipWindow.CallTip(self.text)
     40 
     41     def _remove_calltip_window(self, event=None):
     42         if self.calltip:
     43             self.calltip.hidetip()
     44             self.calltip = None
     45 
     46     def force_open_calltip_event(self, event):
     47         """Happens when the user really wants to open a CallTip, even if a
     48         function call is needed.
     49         """
     50         self.open_calltip(True)
     51 
     52     def try_open_calltip_event(self, event):
     53         """Happens when it would be nice to open a CallTip, but not really
     54         necessary, for example after an opening bracket, so function calls
     55         won't be made.
     56         """
     57         self.open_calltip(False)
     58 
     59     def refresh_calltip_event(self, event):
     60         """If there is already a calltip window, check if it is still needed,
     61         and if so, reload it.
     62         """
     63         if self.calltip and self.calltip.is_active():
     64             self.open_calltip(False)
     65 
     66     def open_calltip(self, evalfuncs):
     67         self._remove_calltip_window()
     68 
     69         hp = HyperParser(self.editwin, "insert")
     70         sur_paren = hp.get_surrounding_brackets('(')
     71         if not sur_paren:
     72             return
     73         hp.set_index(sur_paren[0])
     74         expression = hp.get_expression()
     75         if not expression or (not evalfuncs and expression.find('(') != -1):
     76             return
     77         arg_text = self.fetch_tip(expression)
     78         if not arg_text:
     79             return
     80         self.calltip = self._make_calltip_window()
     81         self.calltip.showtip(arg_text, sur_paren[0], sur_paren[1])
     82 
     83     def fetch_tip(self, expression):
     84         """Return the argument list and docstring of a function or class
     85 
     86         If there is a Python subprocess, get the calltip there.  Otherwise,
     87         either fetch_tip() is running in the subprocess itself or it was called
     88         in an IDLE EditorWindow before any script had been run.
     89 
     90         The subprocess environment is that of the most recently run script.  If
     91         two unrelated modules are being edited some calltips in the current
     92         module may be inoperative if the module was not the last to run.
     93 
     94         To find methods, fetch_tip must be fed a fully qualified name.
     95 
     96         """
     97         try:
     98             rpcclt = self.editwin.flist.pyshell.interp.rpcclt
     99         except AttributeError:
    100             rpcclt = None
    101         if rpcclt:
    102             return rpcclt.remotecall("exec", "get_the_calltip",
    103                                      (expression,), {})
    104         else:
    105             entity = self.get_entity(expression)
    106             return get_arg_text(entity)
    107 
    108     def get_entity(self, expression):
    109         """Return the object corresponding to expression evaluated
    110         in a namespace spanning sys.modules and __main.dict__.
    111         """
    112         if expression:
    113             namespace = sys.modules.copy()
    114             namespace.update(__main__.__dict__)
    115             try:
    116                 return eval(expression, namespace)
    117             except BaseException:
    118                 # An uncaught exception closes idle, and eval can raise any
    119                 # exception, especially if user classes are involved.
    120                 return None
    121 
    122 def _find_constructor(class_ob):
    123     # Given a class object, return a function object used for the
    124     # constructor (ie, __init__() ) or None if we can't find one.
    125     try:
    126         return class_ob.__init__.im_func
    127     except AttributeError:
    128         for base in class_ob.__bases__:
    129             rc = _find_constructor(base)
    130             if rc is not None: return rc
    131     return None
    132 
    133 def get_arg_text(ob):
    134     """Get a string describing the arguments for the given object,
    135        only if it is callable."""
    136     arg_text = ""
    137     if ob is not None and hasattr(ob, '__call__'):
    138         arg_offset = 0
    139         if type(ob) in (types.ClassType, types.TypeType):
    140             # Look for the highest __init__ in the class chain.
    141             fob = _find_constructor(ob)
    142             if fob is None:
    143                 fob = lambda: None
    144             else:
    145                 arg_offset = 1
    146         elif type(ob)==types.MethodType:
    147             # bit of a hack for methods - turn it into a function
    148             # but we drop the "self" param.
    149             fob = ob.im_func
    150             arg_offset = 1
    151         else:
    152             fob = ob
    153         # Try to build one for Python defined functions
    154         if type(fob) in [types.FunctionType, types.LambdaType]:
    155             argcount = fob.func_code.co_argcount
    156             real_args = fob.func_code.co_varnames[arg_offset:argcount]
    157             defaults = fob.func_defaults or []
    158             defaults = list(map(lambda name: "=%s" % repr(name), defaults))
    159             defaults = [""] * (len(real_args) - len(defaults)) + defaults
    160             items = map(lambda arg, dflt: arg + dflt, real_args, defaults)
    161             if fob.func_code.co_flags & 0x4:
    162                 items.append("...")
    163             if fob.func_code.co_flags & 0x8:
    164                 items.append("***")
    165             arg_text = ", ".join(items)
    166             arg_text = "(%s)" % re.sub("\.\d+", "<tuple>", arg_text)
    167         # See if we can use the docstring
    168         doc = getattr(ob, "__doc__", "")
    169         if doc:
    170             doc = doc.lstrip()
    171             pos = doc.find("\n")
    172             if pos < 0 or pos > 70:
    173                 pos = 70
    174             if arg_text:
    175                 arg_text += "\n"
    176             arg_text += doc[:pos]
    177     return arg_text
    178 
    179 #################################################
    180 #
    181 # Test code
    182 #
    183 if __name__=='__main__':
    184 
    185     def t1(): "()"
    186     def t2(a, b=None): "(a, b=None)"
    187     def t3(a, *args): "(a, ...)"
    188     def t4(*args): "(...)"
    189     def t5(a, *args): "(a, ...)"
    190     def t6(a, b=None, *args, **kw): "(a, b=None, ..., ***)"
    191     def t7((a, b), c, (d, e)): "(<tuple>, c, <tuple>)"
    192 
    193     class TC(object):
    194         "(ai=None, ...)"
    195         def __init__(self, ai=None, *b): "(ai=None, ...)"
    196         def t1(self): "()"
    197         def t2(self, ai, b=None): "(ai, b=None)"
    198         def t3(self, ai, *args): "(ai, ...)"
    199         def t4(self, *args): "(...)"
    200         def t5(self, ai, *args): "(ai, ...)"
    201         def t6(self, ai, b=None, *args, **kw): "(ai, b=None, ..., ***)"
    202         def t7(self, (ai, b), c, (d, e)): "(<tuple>, c, <tuple>)"
    203 
    204     def test(tests):
    205         ct = CallTips()
    206         failed=[]
    207         for t in tests:
    208             expected = t.__doc__ + "\n" + t.__doc__
    209             name = t.__name__
    210             # exercise fetch_tip(), not just get_arg_text()
    211             try:
    212                 qualified_name = "%s.%s" % (t.im_class.__name__, name)
    213             except AttributeError:
    214                 qualified_name = name
    215             arg_text = ct.fetch_tip(qualified_name)
    216             if arg_text != expected:
    217                 failed.append(t)
    218                 fmt = "%s - expected %s, but got %s"
    219                 print  fmt % (t.__name__, expected, get_arg_text(t))
    220         print "%d of %d tests failed" % (len(failed), len(tests))
    221 
    222     tc = TC()
    223     tests = (t1, t2, t3, t4, t5, t6, t7,
    224              TC, tc.t1, tc.t2, tc.t3, tc.t4, tc.t5, tc.t6, tc.t7)
    225 
    226     test(tests)
    227