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