]>
Commit | Line | Data |
---|---|---|
d14a1e28 RD |
1 | """Provides a variety of introspective-type support functions for |
2 | things like call tips and command auto completion.""" | |
1fded56b | 3 | |
d14a1e28 RD |
4 | __author__ = "Patrick K. O'Brien <pobrien@orbtech.com>" |
5 | __cvsid__ = "$Id$" | |
6 | __revision__ = "$Revision$"[11:-2] | |
1fded56b | 7 | |
d14a1e28 RD |
8 | from __future__ import nested_scopes |
9 | ||
10 | import cStringIO | |
11 | import inspect | |
12 | import sys | |
13 | import tokenize | |
14 | import types | |
1fed7eda | 15 | import wx |
d14a1e28 | 16 | |
d14a1e28 RD |
17 | def getAutoCompleteList(command='', locals=None, includeMagic=1, |
18 | includeSingle=1, includeDouble=1): | |
19 | """Return list of auto-completion options for command. | |
20 | ||
21 | The list of options will be based on the locals namespace.""" | |
22 | attributes = [] | |
23 | # Get the proper chunk of code from the command. | |
24 | root = getRoot(command, terminator='.') | |
25 | try: | |
26 | if locals is not None: | |
27 | object = eval(root, locals) | |
28 | else: | |
29 | object = eval(root) | |
30 | except: | |
31 | pass | |
32 | else: | |
33 | attributes = getAttributeNames(object, includeMagic, | |
34 | includeSingle, includeDouble) | |
35 | return attributes | |
36 | ||
37 | def getAttributeNames(object, includeMagic=1, includeSingle=1, | |
38 | includeDouble=1): | |
39 | """Return list of unique attributes, including inherited, for object.""" | |
40 | attributes = [] | |
41 | dict = {} | |
42 | if not hasattrAlwaysReturnsTrue(object): | |
8d9be42a RD |
43 | # Add some attributes that don't always get picked up. |
44 | special_attrs = ['__bases__', '__class__', '__dict__', '__name__', | |
45 | 'func_closure', 'func_code', 'func_defaults', | |
46 | 'func_dict', 'func_doc', 'func_globals', 'func_name'] | |
47 | attributes += [attr for attr in special_attrs \ | |
48 | if hasattr(object, attr)] | |
d14a1e28 RD |
49 | if includeMagic: |
50 | try: attributes += object._getAttributeNames() | |
51 | except: pass | |
52 | # Get all attribute names. | |
7ab71563 RD |
53 | str_type = str(type(object)) |
54 | if str_type == "<type 'array'>": | |
55 | attributes += dir(object) | |
56 | else: | |
57 | attrdict = getAllAttributeNames(object) | |
58 | # Store the object's dir. | |
59 | object_dir = dir(object) | |
006f3556 | 60 | for (obj_type_name, technique, count), attrlist in attrdict.items(): |
7ab71563 RD |
61 | # This complexity is necessary to avoid accessing all the |
62 | # attributes of the object. This is very handy for objects | |
63 | # whose attributes are lazily evaluated. | |
006f3556 | 64 | if type(object).__name__ == obj_type_name and technique == 'dir': |
7ab71563 RD |
65 | attributes += attrlist |
66 | else: | |
67 | attributes += [attr for attr in attrlist \ | |
68 | if attr not in object_dir and hasattr(object, attr)] | |
8d9be42a | 69 | |
d14a1e28 RD |
70 | # Remove duplicates from the attribute list. |
71 | for item in attributes: | |
72 | dict[item] = None | |
73 | attributes = dict.keys() | |
5020e71e RD |
74 | # new-style swig wrappings can result in non-string attributes |
75 | # e.g. ITK http://www.itk.org/ | |
76 | attributes = [attribute for attribute in attributes \ | |
77 | if type(attribute) == str] | |
d14a1e28 RD |
78 | attributes.sort(lambda x, y: cmp(x.upper(), y.upper())) |
79 | if not includeSingle: | |
80 | attributes = filter(lambda item: item[0]!='_' \ | |
3370ec75 | 81 | or item[1:2]=='_', attributes) |
d14a1e28 RD |
82 | if not includeDouble: |
83 | attributes = filter(lambda item: item[:2]!='__', attributes) | |
d14a1e28 RD |
84 | return attributes |
85 | ||
86 | def hasattrAlwaysReturnsTrue(object): | |
87 | return hasattr(object, 'bogu5_123_aTTri8ute') | |
88 | ||
89 | def getAllAttributeNames(object): | |
90 | """Return dict of all attributes, including inherited, for an object. | |
91 | ||
92 | Recursively walk through a class and all base classes. | |
93 | """ | |
94 | attrdict = {} # (object, technique, count): [list of attributes] | |
95 | # !!! | |
96 | # Do Not use hasattr() as a test anywhere in this function, | |
97 | # because it is unreliable with remote objects: xmlrpc, soap, etc. | |
98 | # They always return true for hasattr(). | |
99 | # !!! | |
100 | try: | |
006f3556 RD |
101 | # This could(?) fail if the type is poorly defined without |
102 | # even a name. | |
103 | key = type(object).__name__ | |
d14a1e28 RD |
104 | except: |
105 | key = 'anonymous' | |
106 | # Wake up sleepy objects - a hack for ZODB objects in "ghost" state. | |
107 | wakeupcall = dir(object) | |
108 | del wakeupcall | |
109 | # Get attributes available through the normal convention. | |
110 | attributes = dir(object) | |
111 | attrdict[(key, 'dir', len(attributes))] = attributes | |
112 | # Get attributes from the object's dictionary, if it has one. | |
113 | try: | |
114 | attributes = object.__dict__.keys() | |
115 | attributes.sort() | |
116 | except: # Must catch all because object might have __getattr__. | |
117 | pass | |
118 | else: | |
119 | attrdict[(key, '__dict__', len(attributes))] = attributes | |
120 | # For a class instance, get the attributes for the class. | |
121 | try: | |
122 | klass = object.__class__ | |
123 | except: # Must catch all because object might have __getattr__. | |
124 | pass | |
125 | else: | |
126 | if klass is object: | |
127 | # Break a circular reference. This happens with extension | |
128 | # classes. | |
129 | pass | |
130 | else: | |
131 | attrdict.update(getAllAttributeNames(klass)) | |
132 | # Also get attributes from any and all parent classes. | |
133 | try: | |
134 | bases = object.__bases__ | |
135 | except: # Must catch all because object might have __getattr__. | |
136 | pass | |
137 | else: | |
138 | if isinstance(bases, types.TupleType): | |
139 | for base in bases: | |
140 | if type(base) is types.TypeType: | |
141 | # Break a circular reference. Happens in Python 2.2. | |
142 | pass | |
143 | else: | |
144 | attrdict.update(getAllAttributeNames(base)) | |
145 | return attrdict | |
146 | ||
147 | def getCallTip(command='', locals=None): | |
148 | """For a command, return a tuple of object name, argspec, tip text. | |
149 | ||
150 | The call tip information will be based on the locals namespace.""" | |
151 | calltip = ('', '', '') # object name, argspec, tip text. | |
152 | # Get the proper chunk of code from the command. | |
153 | root = getRoot(command, terminator='(') | |
154 | try: | |
155 | if locals is not None: | |
156 | object = eval(root, locals) | |
157 | else: | |
158 | object = eval(root) | |
159 | except: | |
160 | return calltip | |
161 | name = '' | |
162 | object, dropSelf = getBaseObject(object) | |
163 | try: | |
164 | name = object.__name__ | |
165 | except AttributeError: | |
166 | pass | |
167 | tip1 = '' | |
168 | argspec = '' | |
169 | if inspect.isbuiltin(object): | |
170 | # Builtin functions don't have an argspec that we can get. | |
171 | pass | |
172 | elif inspect.isfunction(object): | |
173 | # tip1 is a string like: "getCallTip(command='', locals=None)" | |
174 | argspec = apply(inspect.formatargspec, inspect.getargspec(object)) | |
175 | if dropSelf: | |
176 | # The first parameter to a method is a reference to an | |
177 | # instance, usually coded as "self", and is usually passed | |
178 | # automatically by Python; therefore we want to drop it. | |
179 | temp = argspec.split(',') | |
180 | if len(temp) == 1: # No other arguments. | |
181 | argspec = '()' | |
c7ad88ef RD |
182 | elif temp[0][:2] == '(*': # first param is like *args, not self |
183 | pass | |
d14a1e28 RD |
184 | else: # Drop the first argument. |
185 | argspec = '(' + ','.join(temp[1:]).lstrip() | |
186 | tip1 = name + argspec | |
187 | doc = '' | |
188 | if callable(object): | |
189 | try: | |
190 | doc = inspect.getdoc(object) | |
191 | except: | |
192 | pass | |
193 | if doc: | |
194 | # tip2 is the first separated line of the docstring, like: | |
195 | # "Return call tip text for a command." | |
196 | # tip3 is the rest of the docstring, like: | |
197 | # "The call tip information will be based on ... <snip> | |
198 | firstline = doc.split('\n')[0].lstrip() | |
c7ad88ef | 199 | if tip1 == firstline or firstline[:len(name)+1] == name+'(': |
d14a1e28 RD |
200 | tip1 = '' |
201 | else: | |
202 | tip1 += '\n\n' | |
203 | docpieces = doc.split('\n\n') | |
204 | tip2 = docpieces[0] | |
205 | tip3 = '\n\n'.join(docpieces[1:]) | |
206 | tip = '%s%s\n\n%s' % (tip1, tip2, tip3) | |
207 | else: | |
208 | tip = tip1 | |
209 | calltip = (name, argspec[1:-1], tip.strip()) | |
210 | return calltip | |
211 | ||
212 | def getRoot(command, terminator=None): | |
213 | """Return the rightmost root portion of an arbitrary Python command. | |
214 | ||
215 | Return only the root portion that can be eval()'d without side | |
216 | effects. The command would normally terminate with a '(' or | |
217 | '.'. The terminator and anything after the terminator will be | |
218 | dropped.""" | |
219 | command = command.split('\n')[-1] | |
220 | if command.startswith(sys.ps2): | |
221 | command = command[len(sys.ps2):] | |
222 | command = command.lstrip() | |
223 | command = rtrimTerminus(command, terminator) | |
224 | tokens = getTokens(command) | |
225 | if not tokens: | |
226 | return '' | |
227 | if tokens[-1][0] is tokenize.ENDMARKER: | |
228 | # Remove the end marker. | |
229 | del tokens[-1] | |
230 | if not tokens: | |
231 | return '' | |
232 | if terminator == '.' and \ | |
233 | (tokens[-1][1] <> '.' or tokens[-1][0] is not tokenize.OP): | |
234 | # Trap decimals in numbers, versus the dot operator. | |
235 | return '' | |
236 | else: | |
237 | # Strip off the terminator. | |
238 | if terminator and command.endswith(terminator): | |
239 | size = 0 - len(terminator) | |
240 | command = command[:size] | |
241 | command = command.rstrip() | |
242 | tokens = getTokens(command) | |
243 | tokens.reverse() | |
244 | line = '' | |
245 | start = None | |
246 | prefix = '' | |
247 | laststring = '.' | |
248 | emptyTypes = ('[]', '()', '{}') | |
249 | for token in tokens: | |
250 | tokentype = token[0] | |
251 | tokenstring = token[1] | |
252 | line = token[4] | |
253 | if tokentype is tokenize.ENDMARKER: | |
254 | continue | |
501b023b RD |
255 | if tokentype in (tokenize.NAME, tokenize.STRING, tokenize.NUMBER) \ |
256 | and laststring != '.': | |
257 | # We've reached something that's not part of the root. | |
258 | if prefix and line[token[3][1]] != ' ': | |
259 | # If it doesn't have a space after it, remove the prefix. | |
260 | prefix = '' | |
261 | break | |
262 | if tokentype in (tokenize.NAME, tokenize.STRING, tokenize.NUMBER) \ | |
d14a1e28 RD |
263 | or (tokentype is tokenize.OP and tokenstring == '.'): |
264 | if prefix: | |
265 | # The prefix isn't valid because it comes after a dot. | |
266 | prefix = '' | |
267 | break | |
268 | else: | |
269 | # start represents the last known good point in the line. | |
270 | start = token[2][1] | |
271 | elif len(tokenstring) == 1 and tokenstring in ('[({])}'): | |
272 | # Remember, we're working backwords. | |
273 | # So prefix += tokenstring would be wrong. | |
274 | if prefix in emptyTypes and tokenstring in ('[({'): | |
275 | # We've already got an empty type identified so now we | |
276 | # are in a nested situation and we can break out with | |
277 | # what we've got. | |
278 | break | |
279 | else: | |
280 | prefix = tokenstring + prefix | |
281 | else: | |
282 | # We've reached something that's not part of the root. | |
283 | break | |
284 | laststring = tokenstring | |
285 | if start is None: | |
286 | start = len(line) | |
287 | root = line[start:] | |
288 | if prefix in emptyTypes: | |
289 | # Empty types are safe to be eval()'d and introspected. | |
290 | root = prefix + root | |
291 | return root | |
292 | ||
293 | def getTokens(command): | |
294 | """Return list of token tuples for command.""" | |
1fed7eda RD |
295 | |
296 | # In case the command is unicode try encoding it | |
297 | if type(command) == unicode: | |
298 | try: | |
299 | command = command.encode(wx.GetDefaultPyEncoding()) | |
300 | except UnicodeEncodeError: | |
301 | pass # otherwise leave it alone | |
302 | ||
d14a1e28 RD |
303 | f = cStringIO.StringIO(command) |
304 | # tokens is a list of token tuples, each looking like: | |
305 | # (type, string, (srow, scol), (erow, ecol), line) | |
306 | tokens = [] | |
307 | # Can't use list comprehension: | |
308 | # tokens = [token for token in tokenize.generate_tokens(f.readline)] | |
309 | # because of need to append as much as possible before TokenError. | |
310 | try: | |
311 | ## This code wasn't backward compatible with Python 2.1.3. | |
312 | ## | |
313 | ## for token in tokenize.generate_tokens(f.readline): | |
314 | ## tokens.append(token) | |
315 | ||
316 | # This works with Python 2.1.3 (with nested_scopes). | |
317 | def eater(*args): | |
318 | tokens.append(args) | |
319 | tokenize.tokenize_loop(f.readline, eater) | |
320 | except tokenize.TokenError: | |
321 | # This is due to a premature EOF, which we expect since we are | |
322 | # feeding in fragments of Python code. | |
323 | pass | |
324 | return tokens | |
325 | ||
326 | def rtrimTerminus(command, terminator=None): | |
327 | """Return command minus anything that follows the final terminator.""" | |
328 | if terminator: | |
329 | pieces = command.split(terminator) | |
330 | if len(pieces) > 1: | |
331 | command = terminator.join(pieces[:-1]) + terminator | |
332 | return command | |
333 | ||
334 | def getBaseObject(object): | |
335 | """Return base object and dropSelf indicator for an object.""" | |
336 | if inspect.isbuiltin(object): | |
337 | # Builtin functions don't have an argspec that we can get. | |
338 | dropSelf = 0 | |
339 | elif inspect.ismethod(object): | |
340 | # Get the function from the object otherwise | |
341 | # inspect.getargspec() complains that the object isn't a | |
342 | # Python function. | |
343 | try: | |
344 | if object.im_self is None: | |
345 | # This is an unbound method so we do not drop self | |
346 | # from the argspec, since an instance must be passed | |
347 | # as the first arg. | |
348 | dropSelf = 0 | |
349 | else: | |
350 | dropSelf = 1 | |
351 | object = object.im_func | |
352 | except AttributeError: | |
353 | dropSelf = 0 | |
354 | elif inspect.isclass(object): | |
355 | # Get the __init__ method function for the class. | |
356 | constructor = getConstructor(object) | |
357 | if constructor is not None: | |
358 | object = constructor | |
359 | dropSelf = 1 | |
360 | else: | |
361 | dropSelf = 0 | |
362 | elif callable(object): | |
363 | # Get the __call__ method instead. | |
364 | try: | |
365 | object = object.__call__.im_func | |
366 | dropSelf = 1 | |
367 | except AttributeError: | |
368 | dropSelf = 0 | |
369 | else: | |
370 | dropSelf = 0 | |
371 | return object, dropSelf | |
372 | ||
373 | def getConstructor(object): | |
374 | """Return constructor for class object, or None if there isn't one.""" | |
375 | try: | |
376 | return object.__init__.im_func | |
377 | except AttributeError: | |
378 | for base in object.__bases__: | |
379 | constructor = getConstructor(base) | |
380 | if constructor is not None: | |
381 | return constructor | |
382 | return None |