OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.test.test_explorer -*- | |
2 # $Id: explorer.py,v 1.6 2003/02/18 21:15:30 acapnotic Exp $ | |
3 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
4 # See LICENSE for details. | |
5 | |
6 | |
7 """Support for python object introspection and exploration. | |
8 | |
9 Note that Explorers, what with their list of attributes, are much like | |
10 manhole.coil.Configurables. Someone should investigate this further. (TODO) | |
11 | |
12 Also TODO: Determine how much code in here (particularly the function | |
13 signature stuff) can be replaced with functions available in the | |
14 L{inspect} module available in Python 2.1. | |
15 """ | |
16 | |
17 # System Imports | |
18 import inspect, new, string, sys, types | |
19 import UserDict | |
20 | |
21 # Twisted Imports | |
22 from twisted.spread import pb | |
23 from twisted.python import reflect | |
24 | |
25 | |
26 True=(1==1) | |
27 False=not True | |
28 | |
29 class Pool(UserDict.UserDict): | |
30 def getExplorer(self, object, identifier): | |
31 oid = id(object) | |
32 if self.data.has_key(oid): | |
33 # XXX: This potentially returns something with | |
34 # 'identifier' set to a different value. | |
35 return self.data[oid] | |
36 else: | |
37 klass = typeTable.get(type(object), ExplorerGeneric) | |
38 e = new.instance(klass, {}) | |
39 self.data[oid] = e | |
40 klass.__init__(e, object, identifier) | |
41 return e | |
42 | |
43 explorerPool = Pool() | |
44 | |
45 class Explorer(pb.Cacheable): | |
46 properties = ["id", "identifier"] | |
47 attributeGroups = [] | |
48 accessors = ["get_refcount"] | |
49 | |
50 id = None | |
51 identifier = None | |
52 | |
53 def __init__(self, object, identifier): | |
54 self.object = object | |
55 self.identifier = identifier | |
56 self.id = id(object) | |
57 | |
58 self.properties = [] | |
59 reflect.accumulateClassList(self.__class__, 'properties', | |
60 self.properties) | |
61 | |
62 self.attributeGroups = [] | |
63 reflect.accumulateClassList(self.__class__, 'attributeGroups', | |
64 self.attributeGroups) | |
65 | |
66 self.accessors = [] | |
67 reflect.accumulateClassList(self.__class__, 'accessors', | |
68 self.accessors) | |
69 | |
70 def getStateToCopyFor(self, perspective): | |
71 all = ["properties", "attributeGroups", "accessors"] | |
72 all.extend(self.properties) | |
73 all.extend(self.attributeGroups) | |
74 | |
75 state = {} | |
76 for key in all: | |
77 state[key] = getattr(self, key) | |
78 | |
79 state['view'] = pb.ViewPoint(perspective, self) | |
80 state['explorerClass'] = self.__class__.__name__ | |
81 return state | |
82 | |
83 def view_get_refcount(self, perspective): | |
84 return sys.getrefcount(self) | |
85 | |
86 class ExplorerGeneric(Explorer): | |
87 properties = ["str", "repr", "typename"] | |
88 | |
89 def __init__(self, object, identifier): | |
90 Explorer.__init__(self, object, identifier) | |
91 self.str = str(object) | |
92 self.repr = repr(object) | |
93 self.typename = type(object).__name__ | |
94 | |
95 | |
96 class ExplorerImmutable(Explorer): | |
97 properties = ["value"] | |
98 | |
99 def __init__(self, object, identifier): | |
100 Explorer.__init__(self, object, identifier) | |
101 self.value = object | |
102 | |
103 | |
104 class ExplorerSequence(Explorer): | |
105 properties = ["len"] | |
106 attributeGroups = ["elements"] | |
107 accessors = ["get_elements"] | |
108 | |
109 def __init__(self, seq, identifier): | |
110 Explorer.__init__(self, seq, identifier) | |
111 self.seq = seq | |
112 self.len = len(seq) | |
113 | |
114 # Use accessor method to fill me in. | |
115 self.elements = [] | |
116 | |
117 def get_elements(self): | |
118 self.len = len(self.seq) | |
119 l = [] | |
120 for i in xrange(self.len): | |
121 identifier = "%s[%s]" % (self.identifier, i) | |
122 | |
123 # GLOBAL: using global explorerPool | |
124 l.append(explorerPool.getExplorer(self.seq[i], identifier)) | |
125 | |
126 return l | |
127 | |
128 def view_get_elements(self, perspective): | |
129 # XXX: set the .elements member of all my remoteCaches | |
130 return self.get_elements() | |
131 | |
132 | |
133 class ExplorerMapping(Explorer): | |
134 properties = ["len"] | |
135 attributeGroups = ["keys"] | |
136 accessors = ["get_keys", "get_item"] | |
137 | |
138 def __init__(self, dct, identifier): | |
139 Explorer.__init__(self, dct, identifier) | |
140 | |
141 self.dct = dct | |
142 self.len = len(dct) | |
143 | |
144 # Use accessor method to fill me in. | |
145 self.keys = [] | |
146 | |
147 def get_keys(self): | |
148 keys = self.dct.keys() | |
149 self.len = len(keys) | |
150 l = [] | |
151 for i in xrange(self.len): | |
152 identifier = "%s.keys()[%s]" % (self.identifier, i) | |
153 | |
154 # GLOBAL: using global explorerPool | |
155 l.append(explorerPool.getExplorer(keys[i], identifier)) | |
156 | |
157 return l | |
158 | |
159 def view_get_keys(self, perspective): | |
160 # XXX: set the .keys member of all my remoteCaches | |
161 return self.get_keys() | |
162 | |
163 def view_get_item(self, perspective, key): | |
164 if type(key) is types.InstanceType: | |
165 key = key.object | |
166 | |
167 item = self.dct[key] | |
168 | |
169 identifier = "%s[%s]" % (self.identifier, repr(key)) | |
170 # GLOBAL: using global explorerPool | |
171 item = explorerPool.getExplorer(item, identifier) | |
172 return item | |
173 | |
174 | |
175 class ExplorerBuiltin(Explorer): | |
176 """ | |
177 @ivar name: the name the function was defined as | |
178 @ivar doc: function's docstring, or C{None} if unavailable | |
179 @ivar self: if not C{None}, the function is a method of this object. | |
180 """ | |
181 properties = ["doc", "name", "self"] | |
182 def __init__(self, function, identifier): | |
183 Explorer.__init__(self, function, identifier) | |
184 self.doc = function.__doc__ | |
185 self.name = function.__name__ | |
186 self.self = function.__self__ | |
187 | |
188 | |
189 class ExplorerInstance(Explorer): | |
190 """ | |
191 Attribute groups: | |
192 - B{methods} -- dictionary of methods | |
193 - B{data} -- dictionary of data members | |
194 | |
195 Note these are only the *instance* methods and members -- | |
196 if you want the class methods, you'll have to look up the class. | |
197 | |
198 TODO: Detail levels (me, me & class, me & class ancestory) | |
199 | |
200 @ivar klass: the class this is an instance of. | |
201 """ | |
202 properties = ["klass"] | |
203 attributeGroups = ["methods", "data"] | |
204 | |
205 def __init__(self, instance, identifier): | |
206 Explorer.__init__(self, instance, identifier) | |
207 members = {} | |
208 methods = {} | |
209 for i in dir(instance): | |
210 # TODO: Make screening of private attributes configurable. | |
211 if i[0] == '_': | |
212 continue | |
213 mIdentifier = string.join([identifier, i], ".") | |
214 member = getattr(instance, i) | |
215 mType = type(member) | |
216 | |
217 if mType is types.MethodType: | |
218 methods[i] = explorerPool.getExplorer(member, mIdentifier) | |
219 else: | |
220 members[i] = explorerPool.getExplorer(member, mIdentifier) | |
221 | |
222 self.klass = explorerPool.getExplorer(instance.__class__, | |
223 self.identifier + | |
224 '.__class__') | |
225 self.data = members | |
226 self.methods = methods | |
227 | |
228 | |
229 class ExplorerClass(Explorer): | |
230 """ | |
231 @ivar name: the name the class was defined with | |
232 @ivar doc: the class's docstring | |
233 @ivar bases: a list of this class's base classes. | |
234 @ivar module: the module the class is defined in | |
235 | |
236 Attribute groups: | |
237 - B{methods} -- class methods | |
238 - B{data} -- other members of the class | |
239 """ | |
240 properties = ["name", "doc", "bases", "module"] | |
241 attributeGroups = ["methods", "data"] | |
242 def __init__(self, theClass, identifier): | |
243 Explorer.__init__(self, theClass, identifier) | |
244 if not identifier: | |
245 identifier = theClass.__name__ | |
246 members = {} | |
247 methods = {} | |
248 for i in dir(theClass): | |
249 if (i[0] == '_') and (i != '__init__'): | |
250 continue | |
251 | |
252 mIdentifier = string.join([identifier, i], ".") | |
253 member = getattr(theClass, i) | |
254 mType = type(member) | |
255 | |
256 if mType is types.MethodType: | |
257 methods[i] = explorerPool.getExplorer(member, mIdentifier) | |
258 else: | |
259 members[i] = explorerPool.getExplorer(member, mIdentifier) | |
260 | |
261 self.name = theClass.__name__ | |
262 self.doc = inspect.getdoc(theClass) | |
263 self.data = members | |
264 self.methods = methods | |
265 self.bases = explorerPool.getExplorer(theClass.__bases__, | |
266 identifier + ".__bases__") | |
267 self.module = getattr(theClass, '__module__', None) | |
268 | |
269 | |
270 class ExplorerFunction(Explorer): | |
271 properties = ["name", "doc", "file", "line","signature"] | |
272 """ | |
273 name -- the name the function was defined as | |
274 signature -- the function's calling signature (Signature instance) | |
275 doc -- the function's docstring | |
276 file -- the file the function is defined in | |
277 line -- the line in the file the function begins on | |
278 """ | |
279 def __init__(self, function, identifier): | |
280 Explorer.__init__(self, function, identifier) | |
281 code = function.func_code | |
282 argcount = code.co_argcount | |
283 takesList = (code.co_flags & 0x04) and 1 | |
284 takesKeywords = (code.co_flags & 0x08) and 1 | |
285 | |
286 n = (argcount + takesList + takesKeywords) | |
287 signature = Signature(code.co_varnames[:n]) | |
288 | |
289 if function.func_defaults: | |
290 i_d = 0 | |
291 for i in xrange(argcount - len(function.func_defaults), | |
292 argcount): | |
293 default = function.func_defaults[i_d] | |
294 default = explorerPool.getExplorer( | |
295 default, '%s.func_defaults[%d]' % (identifier, i_d)) | |
296 signature.set_default(i, default) | |
297 | |
298 i_d = i_d + 1 | |
299 | |
300 if takesKeywords: | |
301 signature.set_keyword(n - 1) | |
302 | |
303 if takesList: | |
304 signature.set_varlist(n - 1 - takesKeywords) | |
305 | |
306 # maybe also: function.func_globals, | |
307 # or at least func_globals.__name__? | |
308 # maybe the bytecode, for disassembly-view? | |
309 | |
310 self.name = function.__name__ | |
311 self.signature = signature | |
312 self.doc = inspect.getdoc(function) | |
313 self.file = code.co_filename | |
314 self.line = code.co_firstlineno | |
315 | |
316 | |
317 class ExplorerMethod(ExplorerFunction): | |
318 properties = ["self", "klass"] | |
319 """ | |
320 In addition to ExplorerFunction properties: | |
321 self -- the object I am bound to, or None if unbound | |
322 klass -- the class I am a method of | |
323 """ | |
324 def __init__(self, method, identifier): | |
325 | |
326 function = method.im_func | |
327 if type(function) is types.InstanceType: | |
328 function = function.__call__.im_func | |
329 | |
330 ExplorerFunction.__init__(self, function, identifier) | |
331 self.id = id(method) | |
332 self.klass = explorerPool.getExplorer(method.im_class, | |
333 identifier + '.im_class') | |
334 self.self = explorerPool.getExplorer(method.im_self, | |
335 identifier + '.im_self') | |
336 | |
337 if method.im_self: | |
338 # I'm a bound method -- eat the 'self' arg. | |
339 self.signature.discardSelf() | |
340 | |
341 | |
342 class ExplorerModule(Explorer): | |
343 """ | |
344 @ivar name: the name the module was defined as | |
345 @ivar doc: documentation string for the module | |
346 @ivar file: the file the module is defined in | |
347 | |
348 Attribute groups: | |
349 - B{classes} -- the public classes provided by the module | |
350 - B{functions} -- the public functions provided by the module | |
351 - B{data} -- the public data members provided by the module | |
352 | |
353 (\"Public\" is taken to be \"anything that doesn't start with _\") | |
354 """ | |
355 properties = ["name","doc","file"] | |
356 attributeGroups = ["classes", "functions", "data"] | |
357 | |
358 def __init__(self, module, identifier): | |
359 Explorer.__init__(self, module, identifier) | |
360 functions = {} | |
361 classes = {} | |
362 data = {} | |
363 for key, value in module.__dict__.items(): | |
364 if key[0] == '_': | |
365 continue | |
366 | |
367 mIdentifier = "%s.%s" % (identifier, key) | |
368 | |
369 if type(value) is types.ClassType: | |
370 classes[key] = explorerPool.getExplorer(value, | |
371 mIdentifier) | |
372 elif type(value) is types.FunctionType: | |
373 functions[key] = explorerPool.getExplorer(value, | |
374 mIdentifier) | |
375 elif type(value) is types.ModuleType: | |
376 pass # pass on imported modules | |
377 else: | |
378 data[key] = explorerPool.getExplorer(value, mIdentifier) | |
379 | |
380 self.name = module.__name__ | |
381 self.doc = inspect.getdoc(module) | |
382 self.file = getattr(module, '__file__', None) | |
383 self.classes = classes | |
384 self.functions = functions | |
385 self.data = data | |
386 | |
387 typeTable = {types.InstanceType: ExplorerInstance, | |
388 types.ClassType: ExplorerClass, | |
389 types.MethodType: ExplorerMethod, | |
390 types.FunctionType: ExplorerFunction, | |
391 types.ModuleType: ExplorerModule, | |
392 types.BuiltinFunctionType: ExplorerBuiltin, | |
393 types.ListType: ExplorerSequence, | |
394 types.TupleType: ExplorerSequence, | |
395 types.DictType: ExplorerMapping, | |
396 types.StringType: ExplorerImmutable, | |
397 types.NoneType: ExplorerImmutable, | |
398 types.IntType: ExplorerImmutable, | |
399 types.FloatType: ExplorerImmutable, | |
400 types.LongType: ExplorerImmutable, | |
401 types.ComplexType: ExplorerImmutable, | |
402 } | |
403 | |
404 class Signature(pb.Copyable): | |
405 """I represent the signature of a callable. | |
406 | |
407 Signatures are immutable, so don't expect my contents to change once | |
408 they've been set. | |
409 """ | |
410 _FLAVOURLESS = None | |
411 _HAS_DEFAULT = 2 | |
412 _VAR_LIST = 4 | |
413 _KEYWORD_DICT = 8 | |
414 | |
415 def __init__(self, argNames): | |
416 self.name = argNames | |
417 self.default = [None] * len(argNames) | |
418 self.flavour = [None] * len(argNames) | |
419 | |
420 def get_name(self, arg): | |
421 return self.name[arg] | |
422 | |
423 def get_default(self, arg): | |
424 if arg is types.StringType: | |
425 arg = self.name.index(arg) | |
426 | |
427 # Wouldn't it be nice if we just returned "None" when there | |
428 # wasn't a default? Well, yes, but often times "None" *is* | |
429 # the default, so return a tuple instead. | |
430 if self.flavour[arg] == self._HAS_DEFAULT: | |
431 return (True, self.default[arg]) | |
432 else: | |
433 return (False, None) | |
434 | |
435 def set_default(self, arg, value): | |
436 if arg is types.StringType: | |
437 arg = self.name.index(arg) | |
438 | |
439 self.flavour[arg] = self._HAS_DEFAULT | |
440 self.default[arg] = value | |
441 | |
442 def set_varlist(self, arg): | |
443 if arg is types.StringType: | |
444 arg = self.name.index(arg) | |
445 | |
446 self.flavour[arg] = self._VAR_LIST | |
447 | |
448 def set_keyword(self, arg): | |
449 if arg is types.StringType: | |
450 arg = self.name.index(arg) | |
451 | |
452 self.flavour[arg] = self._KEYWORD_DICT | |
453 | |
454 def is_varlist(self, arg): | |
455 if arg is types.StringType: | |
456 arg = self.name.index(arg) | |
457 | |
458 return (self.flavour[arg] == self._VAR_LIST) | |
459 | |
460 def is_keyword(self, arg): | |
461 if arg is types.StringType: | |
462 arg = self.name.index(arg) | |
463 | |
464 return (self.flavour[arg] == self._KEYWORD_DICT) | |
465 | |
466 def discardSelf(self): | |
467 """Invoke me to discard the first argument if this is a bound method. | |
468 """ | |
469 ## if self.name[0] != 'self': | |
470 ## log.msg("Warning: Told to discard self, but name is %s" % | |
471 ## self.name[0]) | |
472 self.name = self.name[1:] | |
473 self.default.pop(0) | |
474 self.flavour.pop(0) | |
475 | |
476 def getStateToCopy(self): | |
477 return {'name': tuple(self.name), | |
478 'flavour': tuple(self.flavour), | |
479 'default': tuple(self.default)} | |
480 | |
481 def __len__(self): | |
482 return len(self.name) | |
483 | |
484 def __str__(self): | |
485 arglist = [] | |
486 for arg in xrange(len(self)): | |
487 name = self.get_name(arg) | |
488 hasDefault, default = self.get_default(arg) | |
489 if hasDefault: | |
490 a = "%s=%s" % (name, default) | |
491 elif self.is_varlist(arg): | |
492 a = "*%s" % (name,) | |
493 elif self.is_keyword(arg): | |
494 a = "**%s" % (name,) | |
495 else: | |
496 a = name | |
497 arglist.append(a) | |
498 | |
499 return string.join(arglist,", ") | |
500 | |
501 | |
502 | |
503 | |
504 | |
505 class CRUFT_WatchyThingie: | |
506 # TODO: | |
507 # | |
508 # * an exclude mechanism for the watcher's browser, to avoid | |
509 # sending back large and uninteresting data structures. | |
510 # | |
511 # * an exclude mechanism for the watcher's trigger, to avoid | |
512 # triggering on some frequently-called-method-that-doesn't- | |
513 # actually-change-anything. | |
514 # | |
515 # * XXX! need removeWatch() | |
516 | |
517 def watchIdentifier(self, identifier, callback): | |
518 """Watch the object returned by evaluating the identifier. | |
519 | |
520 Whenever I think the object might have changed, I'll send an | |
521 ObjectLink of it to the callback. | |
522 | |
523 WARNING: This calls eval() on its argument! | |
524 """ | |
525 object = eval(identifier, | |
526 self.globalNamespace, | |
527 self.localNamespace) | |
528 return self.watchObject(object, identifier, callback) | |
529 | |
530 def watchObject(self, object, identifier, callback): | |
531 """Watch the given object. | |
532 | |
533 Whenever I think the object might have changed, I'll send an | |
534 ObjectLink of it to the callback. | |
535 | |
536 The identifier argument is used to generate identifiers for | |
537 objects which are members of this one. | |
538 """ | |
539 if type(object) is not types.InstanceType: | |
540 raise TypeError, "Sorry, can only place a watch on Instances." | |
541 | |
542 # uninstallers = [] | |
543 | |
544 dct = {} | |
545 reflect.addMethodNamesToDict(object.__class__, dct, '') | |
546 for k in object.__dict__.keys(): | |
547 dct[k] = 1 | |
548 | |
549 members = dct.keys() | |
550 | |
551 clazzNS = {} | |
552 clazz = new.classobj('Watching%s%X' % | |
553 (object.__class__.__name__, id(object)), | |
554 (_MonkeysSetattrMixin, object.__class__,), | |
555 clazzNS) | |
556 | |
557 clazzNS['_watchEmitChanged'] = new.instancemethod( | |
558 lambda slf, i=identifier, b=self, cb=callback: | |
559 cb(b.browseObject(slf, i)), | |
560 None, clazz) | |
561 | |
562 # orig_class = object.__class__ | |
563 object.__class__ = clazz | |
564 | |
565 for name in members: | |
566 m = getattr(object, name) | |
567 # Only hook bound methods. | |
568 if ((type(m) is types.MethodType) | |
569 and (m.im_self is not None)): | |
570 # What's the use of putting watch monkeys on methods | |
571 # in addition to __setattr__? Well, um, uh, if the | |
572 # methods modify their attributes (i.e. add a key to | |
573 # a dictionary) instead of [re]setting them, then | |
574 # we wouldn't know about it unless we did this. | |
575 # (Is that convincing?) | |
576 | |
577 monkey = _WatchMonkey(object) | |
578 monkey.install(name) | |
579 # uninstallers.append(monkey.uninstall) | |
580 | |
581 # XXX: This probably prevents these objects from ever having a | |
582 # zero refcount. Leak, Leak! | |
583 ## self.watchUninstallers[object] = uninstallers | |
584 | |
585 | |
586 class _WatchMonkey: | |
587 """I hang on a method and tell you what I see. | |
588 | |
589 TODO: Aya! Now I just do browseObject all the time, but I could | |
590 tell you what got called with what when and returning what. | |
591 """ | |
592 oldMethod = None | |
593 | |
594 def __init__(self, instance): | |
595 """Make a monkey to hang on this instance object. | |
596 """ | |
597 self.instance = instance | |
598 | |
599 def install(self, methodIdentifier): | |
600 """Install myself on my instance in place of this method. | |
601 """ | |
602 oldMethod = getattr(self.instance, methodIdentifier, None) | |
603 | |
604 # XXX: this conditional probably isn't effective. | |
605 if oldMethod is not self: | |
606 # avoid triggering __setattr__ | |
607 self.instance.__dict__[methodIdentifier] = ( | |
608 new.instancemethod(self, self.instance, | |
609 self.instance.__class__)) | |
610 self.oldMethod = (methodIdentifier, oldMethod) | |
611 | |
612 def uninstall(self): | |
613 """Remove myself from this instance and restore the original method. | |
614 | |
615 (I hope.) | |
616 """ | |
617 if self.oldMethod is None: | |
618 return | |
619 | |
620 # XXX: This probably doesn't work if multiple monkies are hanging | |
621 # on a method and they're not removed in order. | |
622 if self.oldMethod[1] is None: | |
623 delattr(self.instance, self.oldMethod[0]) | |
624 else: | |
625 setattr(self.instance, self.oldMethod[0], self.oldMethod[1]) | |
626 | |
627 def __call__(self, instance, *a, **kw): | |
628 """Pretend to be the method I replaced, and ring the bell. | |
629 """ | |
630 if self.oldMethod[1]: | |
631 rval = apply(self.oldMethod[1], a, kw) | |
632 else: | |
633 rval = None | |
634 | |
635 instance._watchEmitChanged() | |
636 return rval | |
637 | |
638 | |
639 class _MonkeysSetattrMixin: | |
640 """A mix-in class providing __setattr__ for objects being watched. | |
641 """ | |
642 def __setattr__(self, k, v): | |
643 """Set the attribute and ring the bell. | |
644 """ | |
645 if hasattr(self.__class__.__bases__[1], '__setattr__'): | |
646 # Hack! Using __bases__[1] is Bad, but since we created | |
647 # this class, we can be reasonably sure it'll work. | |
648 self.__class__.__bases__[1].__setattr__(self, k, v) | |
649 else: | |
650 self.__dict__[k] = v | |
651 | |
652 # XXX: Hey, waitasec, did someone just hang a new method on me? | |
653 # Do I need to put a monkey on it? | |
654 | |
655 self._watchEmitChanged() | |
OLD | NEW |