* accessible.py (_AccessibleMixin.__del__): Catch TypeError
[platform/core/uifw/at-spi2-atk.git] / pyatspi / accessible.py
1 '''
2 Creates functions at import time that are mixed into the 
3 Accessibility.Accessible base class to make it more Pythonic.
4
5 Based on public domain code originally posted at 
6 U{http://wwwx.cs.unc.edu/~parente/cgi-bin/RuntimeClassMixins}.
7
8 @var _ACCESSIBLE_CACHE: Pairs hash values for accessible objects to 
9   L{_PropertyCache} bags. We do not store actual accessibles in the dictionary
10   because that would +1 their ref counts and cause __del__ to never be called
11   which is the method we rely on to properly invalidate cache entries.
12 @type _ACCESSIBLE_CACHE: dictionary
13 @var _CACHE_LEVEL: Current level of caching enabled. Checked dynamically by
14   L{_AccessibleMixin}
15 @type _CACHE_LEVEL: integer
16
17 @author: Peter Parente
18 @organization: IBM Corporation
19 @copyright: Copyright (c) 2005, 2007 IBM Corporation
20 @license: LGPL
21
22 This library is free software; you can redistribute it and/or
23 modify it under the terms of the GNU Library General Public
24 License as published by the Free Software Foundation; either
25 version 2 of the License, or (at your option) any later version.
26
27 This library is distributed in the hope that it will be useful,
28 but WITHOUT ANY WARRANTY; without even the implied warranty of
29 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
30 Library General Public License for more details.
31
32 You should have received a copy of the GNU Library General Public
33 License along with this library; if not, write to the
34 Free Software Foundation, Inc., 59 Temple Place - Suite 330,
35 Boston, MA 02111-1307, USA.
36
37 Portions of this code originally licensed and copyright (c) 2005, 2007
38 IBM Corporation under the BSD license, available at
39 U{http://www.opensource.org/licenses/bsd-license.php}
40 '''
41 import new
42 import types
43 import ORBit
44 import Accessibility
45 import constants
46 import utils
47 import registry
48
49 _ACCESSIBLE_CACHE = {}
50 _CACHE_LEVEL = None
51
52 class _PropertyCache(object):
53   '''Fixed-size bag class for holding cached values.'''
54   __slots__ = ('name', 'description', 'rolename')
55
56 def getCacheLevel():
57   '''
58   Gets the current level of caching.
59   
60   @return: None indicating no caching is in effect. 
61     L{constants.CACHE_INTERFACES} indicating all interface query results are
62     cached. L{constants.CACHE_PROPERTIES} indicating all basic accessible
63     properties are cached.
64   @rtype: integer
65   '''
66   return _CACHE_LEVEL
67
68 def setCacheLevel(val):
69   '''
70   Sets the desired level of caching for all accessible objects created after
71   this function is invoked. Immediately clears the current accessible cache.
72   
73   @param val: None indicating no caching is in effect. 
74     L{constants.CACHE_INTERFACES} indicating all interface query results are
75     cached. L{constants.CACHE_PROPERTIES} indicating all basic accessible
76     properties are cached plus all interfaces.
77   @type val: integer
78   '''
79   global _CACHE_LEVEL
80   if _CACHE_LEVEL != val:
81     # empty our accessible cache  
82     _ACCESSIBLE_CACHE.clear()
83     # need to register/unregister for listeners depending on caching level
84     if val == constants.CACHE_PROPERTIES:
85       r = registry.Registry()
86       r.registerEventListener(_updateCache, *constants.CACHE_EVENTS)
87     else:
88       r = registry.Registry()
89       r.deregisterEventListener(_updateCache, *constants.CACHE_EVENTS)
90   _CACHE_LEVEL = val
91   
92 def clearCache():
93   '''Forces a clear of the entire cache.'''
94   _ACCESSIBLE_CACHE.clear()
95   
96 def printCache(template='%s'):
97   '''
98   Prints the contents of the cache.
99   
100   @param template: Format string to use when printing
101   @type template: string
102   '''
103   print template % _ACCESSIBLE_CACHE
104
105 def _updateCache(event):
106   '''
107   Invalidates an entry in the cache when the hash value of a source of an event
108   matches an entry in the cache.
109   
110   @param event: One of the L{constants.CACHE_EVENTS} event types
111   @type event: L{event.Event}
112   '''
113   try:
114     del _ACCESSIBLE_CACHE[hash(event.source)]
115   except KeyError:
116     return
117
118 def _makeQuery(interface):
119   '''
120   Builds a function querying to a specific interface and returns it.
121   
122   @param interface: Class representing an AT-SPI interface
123   @type interface: class
124   @return: Function querying to the given interface
125   @rtype: function
126   '''
127   def _inner(self):
128     '''
129     Queries an object for another interface.
130   
131     @return: An object with the desired interface
132     @rtype: object
133     @raise NotImplementedError: When the desired interface is not supported    
134     '''
135     iid = utils.getInterfaceIID(interface)
136     try:
137       i = self._icache[iid]
138     except KeyError:
139       # interface not cached
140       caching = True
141     except AttributeError:
142       # determine if we're caching
143       caching = _CACHE_LEVEL is not None
144       if caching:
145         # initialize the cache
146         self._icache = {}
147     else:
148       # check if our cached result was an interface, or an indicator that the
149       # interface is not supported
150       if i is None:
151         raise NotImplementedError
152       else:
153         return i
154
155     try:
156       # do the query remotely
157       i = self.queryInterface(iid)
158       if i is not None:
159         i = i._narrow(interface)
160     except Exception, e:
161       raise LookupError(e)      
162     if i is None:
163       # cache that the interface is not supported
164       if caching:
165         self._icache[iid] = None
166       raise NotImplementedError
167     
168     if caching:
169       # cache the narrow'ed result, but only if we're caching for this object
170       self._icache[iid] = i
171     return i
172   
173   return _inner
174
175 def _makeExceptionHandler(func):
176   '''
177   Builds a function calling the one it wraps in try/except statements catching
178   CORBA exceptions.
179   
180   @return: Function calling the method being wrapped
181   @rtype: function
182   '''
183   def _inner(self, *args, **kwargs):
184     try:
185       # try calling the original func
186       rv = func(self, *args, **kwargs)
187       if isinstance(rv, ORBit.CORBA.Object): rv.ref()
188       return rv
189     except ORBit.CORBA.NO_IMPLEMENT, e:
190       # raise Python exception
191       raise NotImplementedError(e)
192     except ORBit.CORBA.Exception, e:
193       # raise Python exception
194       raise LookupError(e)
195   return _inner
196
197 def _mixInterfaces(cls, interfaces):
198   '''
199   Add methods for querying to interfaces other than the base accessible to
200   the given class.
201   
202   @param cls: Class to mix interface methods into
203   @type cls: class
204   @param interfaces: Classes representing AT-SPI interfaces
205   @type interfaces: list of class
206   '''
207   # create functions in this module for all interfaces listed in constants
208   for interface in interfaces:
209     # build name of converter from the name of the interface
210     name = 'query%s' % utils.getInterfaceName(interface)
211     # build a function that queries to the given interface
212     func = _makeQuery(interface)
213     # build a new method that is a clone of the original function
214     method = new.function(func.func_code, func.func_globals, name, 
215                           func.func_defaults, func.func_closure)
216     # add the method to the given class
217     setattr(cls, name, method)
218
219 def _mixExceptions(cls):
220   '''
221   Wraps all methods and properties in a class with handlers for CORBA 
222   exceptions.
223   
224   @param cls: Class to mix interface methods into
225   @type cls: class
226   '''
227   # get a method type as a reference from a known method
228   method_type = Accessibility.Accessible.getRole.__class__
229   # loop over all names in the new class
230   for name in cls.__dict__.keys():
231     obj = cls.__dict__[name]
232     # check if we're on a protected or private method
233     if name.startswith('_'):
234       continue
235     # check if we're on a method
236     elif isinstance(obj, method_type):
237       # wrap the function in an exception handler
238       method = _makeExceptionHandler(obj)
239       # add the wrapped function to the class
240       setattr(cls, name, method)
241     # check if we're on a property
242     elif isinstance(obj, property):
243       # wrap the getters and setters
244       if obj.fget:
245         func = getattr(cls, obj.fget.__name__)
246         getter = _makeExceptionHandler(func)
247       else:
248         getter = None
249       if obj.fset:
250         func = getattr(cls, obj.fset.__name__)
251         setter = _makeExceptionHandler(func)
252       else:
253         setter = None
254       setattr(cls, name, property(getter, setter))
255
256 def _mixClass(cls, new_cls, ignore=[]):
257   '''
258   Adds the methods in new_cls to cls. After mixing, all instances of cls will
259   have the new methods. If there is a method name clash, the method already in
260   cls will be prefixed with '_mix_' before the new method of the same name is 
261   mixed in.
262   
263   @note: _ is not the prefix because if you wind up with __ in front of a 
264   variable, it becomes private and mangled when an instance is created. 
265   Difficult to invoke from the mixin class.
266
267   @param cls: Existing class to mix features into
268   @type cls: class
269   @param new_cls: Class containing features to add
270   @type new_cls: class
271   @param ignore: Ignore these methods from the mixin
272   @type ignore: iterable
273   '''
274   # loop over all names in the new class
275   for name, func in new_cls.__dict__.items():
276     if name in ignore:
277       continue
278     if isinstance(func, types.FunctionType):
279       # build a new function that is a clone of the one from new_cls
280       method = new.function(func.func_code, func.func_globals, name, 
281                             func.func_defaults, func.func_closure)
282       try:
283         # check if a method of the same name already exists in the target
284         old_method = getattr(cls, name)
285       except AttributeError:
286         pass
287       else:
288         # rename the old method so we can still call it if need be
289         setattr(cls, '_mix_'+name, old_method)
290       # add the clone to cls
291       setattr(cls, name, method)
292     elif isinstance(func, staticmethod):
293       try:
294         # check if a method of the same name already exists in the target
295         old_method = getattr(cls, name)
296       except AttributeError:
297         pass
298       else:
299         # rename the old method so we can still call it if need be
300         setattr(cls, '_mix_'+name, old_method)
301       setattr(cls, name, func)
302     elif isinstance(func, property):
303       try:
304         # check if a method of the same name already exists in the target
305         old_prop = getattr(cls, name)
306       except AttributeError:
307         pass
308       else:
309         # IMPORTANT: We save the old property before overwriting it, even 
310         # though we never end up calling the old prop from our mixin class.
311         # If we don't save the old one, we seem to introduce a Python ref count
312         # problem where the property get/set methods disappear before we can
313         # use them at a later time. This is a minor waste of memory because
314         # a property is a class object and we only overwrite a few of them.
315         setattr(cls, '_mix_'+name, old_prop)
316       setattr(cls, name, func)
317
318 class _AccessibleMixin(object):
319   '''
320   Defines methods to be added to the Accessibility.Accessible class. The
321   features defined here will be added to the Accessible class at run time so
322   that all instances of Accessible have them (i.e. there is no need to
323   explicitly wrap an Accessible in this class or derive a new class from it.)
324   
325   @cvar SLOTTED_CLASSES: Mapping from raw Accessibility class to a new class
326     having the slots defined by L{SLOTS}
327   @type SLOTTED_CLASSES: dictionary
328   @cvar SLOTS: All slots to create
329   @type SLOTS: tuple
330   '''
331   SLOTTED_CLASSES = {}
332   SLOTS = ('_icache', 'user_data')
333   
334   def __new__(cls):
335     '''
336     Creates a new class mimicking the one requested, but with extra named 
337     defined in __slots__. The _cache attribute is used internally for interface
338     caching. The user_data field may be populated with whatever data structure
339     a client wishes to use. Neither is set to a default value by default.
340     
341     Note that we can't simply mix __slots__ into this class because __slots__
342     has an effect only at class creation time. 
343     
344     We also do not completely obliterate __slots__ to allow __dict__ to be
345     instantiated as normal as reducing the initialization and memory overhead
346     of the millions of accessible objects that are created is a good thing for
347     many clients.
348     
349     @param cls: Accessibility object class
350     @type cls: class
351     @return: Instance of the new class
352     @rtype: object
353     '''
354     try:
355       # check if we've already created a new version of the class
356       new_cls = _AccessibleMixin.SLOTTED_CLASSES[cls]
357     except KeyError:
358       # create the new class if not
359       new_cls = type(cls.__name__, (cls,), 
360                      {'__module__' : cls.__module__, 
361                       '__slots__' : _AccessibleMixin.SLOTS})
362       _AccessibleMixin.SLOTTED_CLASSES[cls] = new_cls
363     obj = cls._mix___new__(new_cls)
364     return obj
365   
366   def __del__(self):
367     '''    
368     Decrements the reference count on the accessible object when there are no
369     Python references to this object. This provides automatic reference
370     counting for AT-SPI objects. Also removes this object from the cache if
371     we're caching properties. 
372     '''
373     try:
374       del _ACCESSIBLE_CACHE[hash(self)]
375     except Exception:
376       pass
377     try:
378       self.unref()
379     except Exception:
380       pass
381     
382   def __iter__(self):
383     '''
384     Iterator that yields one accessible child per iteration. If an exception is
385     encountered, None is yielded instead.
386     
387     @return: A child accessible
388     @rtype: Accessibility.Accessible
389     '''
390     for i in xrange(self.childCount):
391       try:
392         yield self.getChildAtIndex(i)
393       except LookupError:
394         yield None
395     
396   def __str__(self):
397     '''
398     Gets a human readable representation of the accessible.
399     
400     @return: Role and name information for the accessible
401     @rtype: string
402     '''
403     try:
404       return '[%s | %s]' % (self.getRoleName(), self.name)
405     except Exception:
406       return '[DEAD]'
407     
408   def __nonzero__(self):
409     '''
410     @return: True, always
411     @rtype: boolean
412     '''
413     return True
414     
415   def __getitem__(self, index):
416     '''
417     Thin wrapper around getChildAtIndex.
418     
419     @param index: Index of desired child
420     @type index: integer
421     @return: Accessible child
422     @rtype: Accessibility.Accessible
423     '''
424     n = self.childCount
425     if index >= n:
426       raise IndexError
427     elif index < -n:
428       raise IndexError
429     elif index < 0:
430       index += n
431     return self.getChildAtIndex(index)
432   
433   def __len__(self):
434     '''
435     Thin wrapper around childCount.
436     
437     @return: Number of child accessibles
438     @rtype: integer
439     '''
440     return self.childCount
441   
442   def _get_name(self):
443     '''
444     Gets the name of the accessible from the cache if it is available, 
445     otherwise, fetches it remotely.
446     
447     @return: Name of the accessible
448     @rtype: string
449     '''
450     if _CACHE_LEVEL != constants.CACHE_PROPERTIES:
451       return self._get_name()
452     
453     cache = _ACCESSIBLE_CACHE
454     h = hash(self)
455     try:
456       return cache[h].name
457     except KeyError:
458       # no cached info for this object yet
459       name = self._get_name()
460       pc = _PropertyCache()
461       pc.name = name
462       cache[h] = pc
463       return name
464     except AttributeError:
465       # no cached name for this object yet
466       name = self._get_name()
467       cache[h].name = name
468       return name
469     
470   name = property(_get_name, Accessibility.Accessible._set_name)
471   
472   def getRoleName(self):
473     '''
474     Gets the unlocalized role name of the accessible from the cache if it is 
475     available, otherwise, fetches it remotely.
476     
477     @return: Role name of the accessible
478     @rtype: string
479     '''
480     if _CACHE_LEVEL != constants.CACHE_PROPERTIES:
481       return self._mix_getRoleName()
482
483     cache = _ACCESSIBLE_CACHE
484     h = hash(self)
485     try:
486       return cache[h].rolename
487     except KeyError, e:
488       # no cached info for this object yet
489       rolename = self._mix_getRoleName()
490       pc = _PropertyCache()
491       pc.rolename = rolename
492       cache[h] = pc
493       return rolename
494     except AttributeError, e:
495       # no cached name for this object yet
496       rolename = self._mix_getRoleName()
497       cache[h].rolename = rolename
498       return rolename
499   
500   def _get_description(self):
501     '''    
502     Gets the description of the accessible from the cache if it is available,
503     otherwise, fetches it remotely.
504     
505     @return: Description of the accessible
506     @rtype: string
507     '''
508     if _CACHE_LEVEL != constants.CACHE_PROPERTIES:
509       return self._get_description()
510
511     cache = _ACCESSIBLE_CACHE
512     h = hash(self)
513     try:
514       return cache[h].description
515     except KeyError:
516       # no cached info for this object yet
517       description = self._get_description()
518       pc = _PropertyCache()
519       pc.description = description
520       cache[h] = pc
521       return description
522     except AttributeError:
523       # no cached name for this object yet
524       description = self._get_description()
525       cache[h].description = description
526       return description
527     
528   description = property(_get_description, 
529                          Accessibility.Accessible._set_description)
530   
531   def getIndexInParent(self):
532     '''
533     Gets the index of this accessible in its parent. Uses the implementation of
534     this method provided by the Accessibility.Accessible object, but checks the
535     bound of the value to ensure it is not outside the range of childCount 
536     reported by this accessible's parent.
537     
538     @return: Index of this accessible in its parent
539     @rtype: integer
540     '''
541     i = self._mix_getIndexInParent()
542     try:
543       # correct for out-of-bounds index reporting
544       return min(self.parent.childCount-1, i)
545     except AttributeError:
546       # return sentinel if there is no parent
547       return -1
548
549   def getApplication(self):
550     '''
551     Gets the most-parent accessible (the application) of this accessible. Tries 
552     using the getApplication method introduced in AT-SPI 1.7.0 first before 
553     resorting to traversing parent links.
554     
555     @warning: Cycles involving more than the previously traversed accessible 
556       are not detected by this code.
557     @return: Application object
558     @rtype: Accessibility.Application
559     '''
560     try:
561       return self._mix_getApplication()
562     except AttributeError:
563       pass
564     curr = self
565     try:
566       while curr.parent is not None and (not curr.parent == curr):
567         curr = curr.parent
568       return curr
569     except Exception:
570       pass
571     # return None if the application isn't reachable for any reason
572     return None
573
574 class _RelationMixin(object):
575   '''
576   Defines methods to be added to the Relation class. At this time it only
577   overrides L{_RelationMixin.getTarget} which by the IDL's standard is
578   supposed to return CORBA.Objects but we expect LAccessibility.Accessible
579   objects (see http://bugzilla.gnome.org/show_bug.cgi?id=435833). 
580   This seems to be a problem especially with the Java implementation of CORBA.
581   '''
582   def getTarget(self, index):
583     '''
584     Overrides the regular getTarget to return Accessibility.Accessible
585     objects.
586
587     @return: The 'nth' target of this Relation.
588     @rtype: Accessibility.Accessible
589     '''
590     target = self._mix_getTarget(index)
591     target.ref()
592     return target._narrow(Accessibility.Accessible)
593
594 # 1. mix the exception handlers into all queryable interfaces
595 map(_mixExceptions, constants.ALL_INTERFACES)
596 # 2. mix the exception handlers into other Accessibility objects
597 map(_mixExceptions, [Accessibility.StateSet])
598 # 3. mix the new functions
599 _mixClass(Accessibility.Accessible, _AccessibleMixin,
600           ['_get_name', '_get_description'])
601 # 4. mix queryInterface convenience methods
602 _mixInterfaces(Accessibility.Accessible, constants.ALL_INTERFACES)
603 # 5. mix Relation class
604 _mixClass(Accessibility.Relation, _RelationMixin)