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