]> git.saurik.com Git - wxWidgets.git/blame_incremental - wxPython/wx/py/introspect.py
Applied patch [ 827011 ] Event-based processing of item tooltips in wxTreeCtrl
[wxWidgets.git] / wxPython / wx / py / introspect.py
... / ...
CommitLineData
1"""Provides a variety of introspective-type support functions for
2things 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
8from __future__ import nested_scopes
9
10import cStringIO
11import inspect
12import sys
13import tokenize
14import types
15
16try:
17 True
18except NameError:
19 True = 1==1
20 False = 1==0
21
22def 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
42def 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
75def hasattrAlwaysReturnsTrue(object):
76 return hasattr(object, 'bogu5_123_aTTri8ute')
77
78def 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
137def 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 = '()'
172 elif temp[0][:2] == '(*': # first param is like *args, not self
173 pass
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()
189 if tip1 == firstline or firstline[:len(name)+1] == name+'(':
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
202def 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
283def 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
309def 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
317def 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
356def 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