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