Commit | Line | Data |
---|---|---|
920dae64 AT |
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 | Future plans include extending the functionality to include class attributes. | |
8 | ||
9 | """ | |
10 | import sys | |
11 | import string | |
12 | import types | |
13 | ||
14 | import CallTipWindow | |
15 | ||
16 | import __main__ | |
17 | ||
18 | class CallTips: | |
19 | ||
20 | menudefs = [ | |
21 | ] | |
22 | ||
23 | def __init__(self, editwin=None): | |
24 | if editwin is None: # subprocess and test | |
25 | self.editwin = None | |
26 | return | |
27 | self.editwin = editwin | |
28 | self.text = editwin.text | |
29 | self.calltip = None | |
30 | self._make_calltip_window = self._make_tk_calltip_window | |
31 | ||
32 | def close(self): | |
33 | self._make_calltip_window = None | |
34 | ||
35 | def _make_tk_calltip_window(self): | |
36 | # See __init__ for usage | |
37 | return CallTipWindow.CallTip(self.text) | |
38 | ||
39 | def _remove_calltip_window(self): | |
40 | if self.calltip: | |
41 | self.calltip.hidetip() | |
42 | self.calltip = None | |
43 | ||
44 | def paren_open_event(self, event): | |
45 | self._remove_calltip_window() | |
46 | name = self.get_name_at_cursor() | |
47 | arg_text = self.fetch_tip(name) | |
48 | if arg_text: | |
49 | self.calltip_start = self.text.index("insert") | |
50 | self.calltip = self._make_calltip_window() | |
51 | self.calltip.showtip(arg_text) | |
52 | return "" #so the event is handled normally. | |
53 | ||
54 | def paren_close_event(self, event): | |
55 | # Now just hides, but later we should check if other | |
56 | # paren'd expressions remain open. | |
57 | self._remove_calltip_window() | |
58 | return "" #so the event is handled normally. | |
59 | ||
60 | def check_calltip_cancel_event(self, event): | |
61 | if self.calltip: | |
62 | # If we have moved before the start of the calltip, | |
63 | # or off the calltip line, then cancel the tip. | |
64 | # (Later need to be smarter about multi-line, etc) | |
65 | if self.text.compare("insert", "<=", self.calltip_start) or \ | |
66 | self.text.compare("insert", ">", self.calltip_start | |
67 | + " lineend"): | |
68 | self._remove_calltip_window() | |
69 | return "" #so the event is handled normally. | |
70 | ||
71 | def calltip_cancel_event(self, event): | |
72 | self._remove_calltip_window() | |
73 | return "" #so the event is handled normally. | |
74 | ||
75 | __IDCHARS = "._" + string.ascii_letters + string.digits | |
76 | ||
77 | def get_name_at_cursor(self): | |
78 | idchars = self.__IDCHARS | |
79 | str = self.text.get("insert linestart", "insert") | |
80 | i = len(str) | |
81 | while i and str[i-1] in idchars: | |
82 | i -= 1 | |
83 | return str[i:] | |
84 | ||
85 | def fetch_tip(self, name): | |
86 | """Return the argument list and docstring of a function or class | |
87 | ||
88 | If there is a Python subprocess, get the calltip there. Otherwise, | |
89 | either fetch_tip() is running in the subprocess itself or it was called | |
90 | in an IDLE EditorWindow before any script had been run. | |
91 | ||
92 | The subprocess environment is that of the most recently run script. If | |
93 | two unrelated modules are being edited some calltips in the current | |
94 | module may be inoperative if the module was not the last to run. | |
95 | ||
96 | """ | |
97 | try: | |
98 | rpcclt = self.editwin.flist.pyshell.interp.rpcclt | |
99 | except: | |
100 | rpcclt = None | |
101 | if rpcclt: | |
102 | return rpcclt.remotecall("exec", "get_the_calltip", | |
103 | (name,), {}) | |
104 | else: | |
105 | entity = self.get_entity(name) | |
106 | return get_arg_text(entity) | |
107 | ||
108 | def get_entity(self, name): | |
109 | "Lookup name in a namespace spanning sys.modules and __main.dict__" | |
110 | if name: | |
111 | namespace = sys.modules.copy() | |
112 | namespace.update(__main__.__dict__) | |
113 | try: | |
114 | return eval(name, namespace) | |
115 | except: | |
116 | return None | |
117 | ||
118 | def _find_constructor(class_ob): | |
119 | # Given a class object, return a function object used for the | |
120 | # constructor (ie, __init__() ) or None if we can't find one. | |
121 | try: | |
122 | return class_ob.__init__.im_func | |
123 | except AttributeError: | |
124 | for base in class_ob.__bases__: | |
125 | rc = _find_constructor(base) | |
126 | if rc is not None: return rc | |
127 | return None | |
128 | ||
129 | def get_arg_text(ob): | |
130 | "Get a string describing the arguments for the given object" | |
131 | argText = "" | |
132 | if ob is not None: | |
133 | argOffset = 0 | |
134 | if type(ob)==types.ClassType: | |
135 | # Look for the highest __init__ in the class chain. | |
136 | fob = _find_constructor(ob) | |
137 | if fob is None: | |
138 | fob = lambda: None | |
139 | else: | |
140 | argOffset = 1 | |
141 | elif type(ob)==types.MethodType: | |
142 | # bit of a hack for methods - turn it into a function | |
143 | # but we drop the "self" param. | |
144 | fob = ob.im_func | |
145 | argOffset = 1 | |
146 | else: | |
147 | fob = ob | |
148 | # Try and build one for Python defined functions | |
149 | if type(fob) in [types.FunctionType, types.LambdaType]: | |
150 | try: | |
151 | realArgs = fob.func_code.co_varnames[argOffset:fob.func_code.co_argcount] | |
152 | defaults = fob.func_defaults or [] | |
153 | defaults = list(map(lambda name: "=%s" % name, defaults)) | |
154 | defaults = [""] * (len(realArgs)-len(defaults)) + defaults | |
155 | items = map(lambda arg, dflt: arg+dflt, realArgs, defaults) | |
156 | if fob.func_code.co_flags & 0x4: | |
157 | items.append("...") | |
158 | if fob.func_code.co_flags & 0x8: | |
159 | items.append("***") | |
160 | argText = ", ".join(items) | |
161 | argText = "(%s)" % argText | |
162 | except: | |
163 | pass | |
164 | # See if we can use the docstring | |
165 | doc = getattr(ob, "__doc__", "") | |
166 | if doc: | |
167 | doc = doc.lstrip() | |
168 | pos = doc.find("\n") | |
169 | if pos < 0 or pos > 70: | |
170 | pos = 70 | |
171 | if argText: | |
172 | argText += "\n" | |
173 | argText += doc[:pos] | |
174 | return argText | |
175 | ||
176 | ################################################# | |
177 | # | |
178 | # Test code | |
179 | # | |
180 | if __name__=='__main__': | |
181 | ||
182 | def t1(): "()" | |
183 | def t2(a, b=None): "(a, b=None)" | |
184 | def t3(a, *args): "(a, ...)" | |
185 | def t4(*args): "(...)" | |
186 | def t5(a, *args): "(a, ...)" | |
187 | def t6(a, b=None, *args, **kw): "(a, b=None, ..., ***)" | |
188 | ||
189 | class TC: | |
190 | "(a=None, ...)" | |
191 | def __init__(self, a=None, *b): "(a=None, ...)" | |
192 | def t1(self): "()" | |
193 | def t2(self, a, b=None): "(a, b=None)" | |
194 | def t3(self, a, *args): "(a, ...)" | |
195 | def t4(self, *args): "(...)" | |
196 | def t5(self, a, *args): "(a, ...)" | |
197 | def t6(self, a, b=None, *args, **kw): "(a, b=None, ..., ***)" | |
198 | ||
199 | def test(tests): | |
200 | ct = CallTips() | |
201 | failed=[] | |
202 | for t in tests: | |
203 | expected = t.__doc__ + "\n" + t.__doc__ | |
204 | name = t.__name__ | |
205 | arg_text = ct.fetch_tip(name) | |
206 | if arg_text != expected: | |
207 | failed.append(t) | |
208 | print "%s - expected %s, but got %s" % (t, expected, | |
209 | get_arg_text(entity)) | |
210 | print "%d of %d tests failed" % (len(failed), len(tests)) | |
211 | ||
212 | tc = TC() | |
213 | tests = (t1, t2, t3, t4, t5, t6, | |
214 | TC, tc.t1, tc.t2, tc.t3, tc.t4, tc.t5, tc.t6) | |
215 | ||
216 | test(tests) |