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