1"""CallTips.py - An IDLE Extension to Jog Your Memory
2
3Call Tips are floating windows which display function, class, and method
4parameter and docstring information when you type an opening parenthesis, and
5which disappear when you type a closing parenthesis.
6
7"""
8import re
9import sys
10import types
11
12from idlelib import CallTipWindow
13from idlelib.HyperParser import HyperParser
14
15import __main__
16
17class 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
122def _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
133def 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#
183if __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