]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/py/introspect.py
Make InheritAttributes and ShouldInheritColours public so they can be
[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 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 = '()'
166 elif temp[0][:2] == '(*': # first param is like *args, not self
167 pass
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()
183 if tip1 == firstline or firstline[:len(name)+1] == name+'(':
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