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