a1c953f003c7622ff133569a54495b4600325cc4
[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(iid):
119   '''
120   Builds a function querying to a specific interface and returns it.
121   
122   @param iid: Interface identifier to use when querying
123   @type iid: string
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     try:
136       i = self._icache[iid]
137     except KeyError:
138       # interface not cached
139       caching = True
140     except AttributeError:
141       # determine if we're caching
142       caching = _CACHE_LEVEL is not None
143       if caching:
144         # initialize the cache
145         self._icache = {}
146     else:
147       # check if our cached result was an interface, or an indicator that the
148       # interface is not supported
149       if i is None:
150         raise NotImplementedError
151       else:
152         return i
153
154     try:
155       # do the query remotely
156       i = self.queryInterface(iid)
157     except Exception, e:
158       raise LookupError(e)
159     if i is None:
160       # cache that the interface is not supported
161       if caching:
162         self._icache[iid] = None
163       raise NotImplementedError
164     
165     if caching:
166       # cache the narrow'ed result, but only if we're caching for this object
167       self._icache[iid] = i
168     return i
169   
170   return _inner
171
172 def _makeExceptionHandler(func):
173   '''
174   Builds a function calling the one it wraps in try/except statements catching
175   CORBA exceptions.
176   
177   @return: Function calling the method being wrapped
178   @rtype: function
179   '''
180   def _inner(self, *args, **kwargs):
181     try:
182       # try calling the original func
183       return func(self, *args, **kwargs)
184     except ORBit.CORBA.NO_IMPLEMENT, e:
185       # raise Python exception
186       raise NotImplementedError(e)
187     except ORBit.CORBA.Exception, e:
188       # raise Python exception
189       raise LookupError(e)
190   return _inner
191
192 def _mixInterfaces(cls, interfaces):
193   '''
194   Add methods for querying to interfaces other than the base accessible to
195   the given class.
196   
197   @param cls: Class to mix interface methods into
198   @type cls: class
199   @param interfaces: Classes representing AT-SPI interfaces
200   @type interfaces: list of class
201   '''
202   # create functions in this module for all interfaces listed in constants
203   for interface in interfaces:
204     # build name of converter from the name of the interface
205     name = 'query%s' % utils.getInterfaceName(interface)
206     # build a function that queries to the given interface
207     func = _makeQuery(utils.getInterfaceIID(interface))
208     # build a new method that is a clone of the original function
209     method = new.function(func.func_code, func.func_globals, name, 
210                           func.func_defaults, func.func_closure)
211     # add the method to the given class
212     setattr(cls, name, method)
213
214 def _mixExceptions(cls):
215   '''
216   Wraps all methods and properties in a class with handlers for CORBA 
217   exceptions.
218   
219   @param cls: Class to mix interface methods into
220   @type cls: class
221   '''
222   # get a method type as a reference from a known method
223   method_type = Accessibility.Accessible.getRole.__class__
224   # loop over all names in the new class
225   for name in cls.__dict__.keys():
226     obj = cls.__dict__[name]
227     # check if we're on a protected or private method
228     if name.startswith('_'):
229       continue
230     # check if we're on a method
231     elif isinstance(obj, method_type):
232       # wrap the function in an exception handler
233       method = _makeExceptionHandler(obj)
234       # add the wrapped function to the class
235       setattr(cls, name, method)
236     # check if we're on a property
237     elif isinstance(obj, property):
238       # wrap the getters and setters
239       if obj.fget:
240         func = getattr(cls, obj.fget.__name__)
241         getter = _makeExceptionHandler(func)
242       else:
243         getter = None
244       if obj.fset:
245         func = getattr(cls, obj.fset.__name__)
246         setter = _makeExceptionHandler(func)
247       else:
248         setter = None
249       setattr(cls, name, property(getter, setter))
250
251 def _mixClass(cls, new_cls, ignore=[]):
252   '''
253   Adds the methods in new_cls to cls. After mixing, all instances of cls will
254   have the new methods. If there is a method name clash, the method already in
255   cls will be prefixed with '_mix_' before the new method of the same name is 
256   mixed in.
257   
258   @note: _ is not the prefix because if you wind up with __ in front of a 
259   variable, it becomes private and mangled when an instance is created. 
260   Difficult to invoke from the mixin class.
261
262   @param cls: Existing class to mix features into
263   @type cls: class
264   @param new_cls: Class containing features to add
265   @type new_cls: class
266   @param ignore: Ignore these methods from the mixin
267   @type ignore: iterable
268   '''
269   # loop over all names in the new class
270   for name, func in new_cls.__dict__.items():
271     if name in ignore:
272       continue
273     if isinstance(func, types.FunctionType):
274       # build a new function that is a clone of the one from new_cls
275       method = new.function(func.func_code, func.func_globals, name, 
276                             func.func_defaults, func.func_closure)
277       try:
278         # check if a method of the same name already exists in the target
279         old_method = getattr(cls, name)
280       except AttributeError:
281         pass
282       else:
283         # rename the old method so we can still call it if need be
284         setattr(cls, '_mix_'+name, old_method)
285       # add the clone to cls
286       setattr(cls, name, method)
287     elif isinstance(func, staticmethod):
288       try:
289         # check if a method of the same name already exists in the target
290         old_method = getattr(cls, name)
291       except AttributeError:
292         pass
293       else:
294         # rename the old method so we can still call it if need be
295         setattr(cls, '_mix_'+name, old_method)
296       setattr(cls, name, func)
297     elif isinstance(func, property):
298       try:
299         # check if a method of the same name already exists in the target
300         old_prop = getattr(cls, name)
301       except AttributeError:
302         pass
303       else:
304         # IMPORTANT: We save the old property before overwriting it, even 
305         # though we never end up calling the old prop from our mixin class.
306         # If we don't save the old one, we seem to introduce a Python ref count
307         # problem where the property get/set methods disappear before we can
308         # use them at a later time. This is a minor waste of memory because
309         # a property is a class object and we only overwrite a few of them.
310         setattr(cls, '_mix_'+name, old_prop)
311       setattr(cls, name, func)
312
313 class _AccessibleMixin(object):
314   '''
315   Defines methods to be added to the Accessibility.Accessible class. The
316   features defined here will be added to the Accessible class at run time so
317   that all instances of Accessible have them (i.e. there is no need to
318   explicitly wrap an Accessible in this class or derive a new class from it.)
319   
320   @cvar SLOTTED_CLASSES: Mapping from raw Accessibility class to a new class
321     having the slots defined by L{SLOTS}
322   @type SLOTTED_CLASSES: dictionary
323   @cvar SLOTS: All slots to create
324   @type SLOTS: tuple
325   '''
326   SLOTTED_CLASSES = {}
327   SLOTS = ('_icache', 'user_data')
328   
329   def __new__(cls):
330     '''
331     Creates a new class mimicking the one requested, but with extra named 
332     defined in __slots__. The _cache attribute is used internally for interface
333     caching. The user_data field may be populated with whatever data structure
334     a client wishes to use. Neither is set to a default value by default.
335     
336     Note that we can't simply mix __slots__ into this class because __slots__
337     has an effect only at class creation time. 
338     
339     We also do not completely obliterate __slots__ to allow __dict__ to be
340     instantiated as normal as reducing the initialization and memory overhead
341     of the millions of accessible objects that are created is a good thing for
342     many clients.
343     
344     @param cls: Accessibility object class
345     @type cls: class
346     @return: Instance of the new class
347     @rtype: object
348     '''
349     try:
350       # check if we've already created a new version of the class
351       new_cls = _AccessibleMixin.SLOTTED_CLASSES[cls]
352     except KeyError:
353       # create the new class if not
354       new_cls = type(cls.__name__, (cls,), 
355                      {'__module__' : cls.__module__, 
356                       '__slots__' : _AccessibleMixin.SLOTS})
357       _AccessibleMixin.SLOTTED_CLASSES[cls] = new_cls
358     obj = cls._mix___new__(new_cls)
359     return obj
360   
361   def __del__(self):
362     '''    
363     Decrements the reference count on the accessible object when there are no
364     Python references to this object. This provides automatic reference
365     counting for AT-SPI objects. Also removes this object from the cache if
366     we're caching properties. 
367     '''
368     try:
369       del _ACCESSIBLE_CACHE[hash(self)]
370     except KeyError:
371       pass
372     try:
373       self.unref()
374     except Exception:
375       pass
376     
377   def __iter__(self):
378     '''
379     Iterator that yields one accessible child per iteration. If an exception is
380     encountered, None is yielded instead.
381     
382     @return: A child accessible
383     @rtype: Accessibility.Accessible
384     '''
385     for i in xrange(self.childCount):
386       try:
387         yield self.getChildAtIndex(i)
388       except LookupError:
389         yield None
390     
391   def __str__(self):
392     '''
393     Gets a human readable representation of the accessible.
394     
395     @return: Role and name information for the accessible
396     @rtype: string
397     '''
398     try:
399       return '[%s | %s]' % (self.getRoleName(), self.name)
400     except Exception:
401       return '[DEAD]'
402     
403   def __nonzero__(self):
404     '''
405     @return: True, always
406     @rtype: boolean
407     '''
408     return True
409     
410   def __getitem__(self, index):
411     '''
412     Thin wrapper around getChildAtIndex.
413     
414     @param index: Index of desired child
415     @type index: integer
416     @return: Accessible child
417     @rtype: Accessibility.Accessible
418     '''
419     n = self.childCount
420     if index >= n:
421       raise IndexError
422     elif index < -n:
423       raise IndexError
424     elif index < 0:
425       index += n
426     return self.getChildAtIndex(index)
427   
428   def __len__(self):
429     '''
430     Thin wrapper around childCount.
431     
432     @return: Number of child accessibles
433     @rtype: integer
434     '''
435     return self.childCount
436   
437   def _get_name(self):
438     '''
439     Gets the name of the accessible from the cache if it is available, 
440     otherwise, fetches it remotely.
441     
442     @return: Name of the accessible
443     @rtype: string
444     '''
445     if _CACHE_LEVEL != constants.CACHE_PROPERTIES:
446       return self._get_name()
447     
448     cache = _ACCESSIBLE_CACHE
449     h = hash(self)
450     try:
451       return cache[h].name
452     except KeyError:
453       # no cached info for this object yet
454       name = self._get_name()
455       pc = _PropertyCache()
456       pc.name = name
457       cache[h] = pc
458       return name
459     except AttributeError:
460       # no cached name for this object yet
461       name = self._get_name()
462       cache[h].name = name
463       return name
464     
465   name = property(_get_name, Accessibility.Accessible._set_name)
466   
467   def getRoleName(self):
468     '''
469     Gets the unlocalized role name of the accessible from the cache if it is 
470     available, otherwise, fetches it remotely.
471     
472     @return: Role name of the accessible
473     @rtype: string
474     '''
475     if _CACHE_LEVEL != constants.CACHE_PROPERTIES:
476       return self._mix_getRoleName()
477
478     cache = _ACCESSIBLE_CACHE
479     h = hash(self)
480     try:
481       return cache[h].rolename
482     except KeyError, e:
483       # no cached info for this object yet
484       rolename = self._mix_getRoleName()
485       pc = _PropertyCache()
486       pc.rolename = rolename
487       cache[h] = pc
488       return rolename
489     except AttributeError, e:
490       # no cached name for this object yet
491       rolename = self._mix_getRoleName()
492       cache[h].rolename = rolename
493       return rolename
494   
495   def _get_description(self):
496     '''    
497     Gets the description of the accessible from the cache if it is available,
498     otherwise, fetches it remotely.
499     
500     @return: Description of the accessible
501     @rtype: string
502     '''
503     if _CACHE_LEVEL != constants.CACHE_PROPERTIES:
504       return self._get_description()
505
506     cache = _ACCESSIBLE_CACHE
507     h = hash(self)
508     try:
509       return cache[h].description
510     except KeyError:
511       # no cached info for this object yet
512       description = self._get_description()
513       pc = _PropertyCache()
514       pc.description = description
515       cache[h] = pc
516       return description
517     except AttributeError:
518       # no cached name for this object yet
519       description = self._get_description()
520       cache[h].description = description
521       return description
522     
523   description = property(_get_description, 
524                          Accessibility.Accessible._set_description)
525   
526   def getIndexInParent(self):
527     '''
528     Gets the index of this accessible in its parent. Uses the implementation of
529     this method provided by the Accessibility.Accessible object, but checks the
530     bound of the value to ensure it is not outside the range of childCount 
531     reported by this accessible's parent.
532     
533     @return: Index of this accessible in its parent
534     @rtype: integer
535     '''
536     i = self._mix_getIndexInParent()
537     try:
538       # correct for out-of-bounds index reporting
539       return min(self.parent.childCount-1, i)
540     except AttributeError:
541       # return sentinel if there is no parent
542       return -1
543
544   def getApplication(self):
545     '''
546     Gets the most-parent accessible (the application) of this accessible. Tries 
547     using the getApplication method introduced in AT-SPI 1.7.0 first before 
548     resorting to traversing parent links.
549     
550     @warning: Cycles involving more than the previously traversed accessible 
551       are not detected by this code.
552     @return: Application object
553     @rtype: Accessibility.Application
554     '''
555     try:
556       return self._mix_getApplication()
557     except AttributeError:
558       pass
559     curr = self
560     try:
561       while curr.parent is not None and (not curr.parent == curr):
562         curr = curr.parent
563       return curr
564     except Exception:
565       pass
566     # return None if the application isn't reachable for any reason
567     return None
568
569 # 1. mix the exception handlers into all queryable interfaces
570 map(_mixExceptions, constants.ALL_INTERFACES)
571 # 2. mix the exception handlers into other Accessibility objects
572 map(_mixExceptions, [Accessibility.StateSet])
573 # 3. mix the new functions
574 _mixClass(Accessibility.Accessible, _AccessibleMixin,
575           ['_get_name', '_get_description'])
576 # 4. mix queryInterface convenience methods
577 _mixInterfaces(Accessibility.Accessible, constants.ALL_INTERFACES)