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