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