2 Creates functions at import time that are mixed into the
3 Accessibility.Accessible base class to make it more Pythonic.
5 Based on public domain code originally posted at
6 U{http://wwwx.cs.unc.edu/~parente/cgi-bin/RuntimeClassMixins}.
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
15 @type _CACHE_LEVEL: integer
17 @author: Peter Parente
18 @organization: IBM Corporation
19 @copyright: Copyright (c) 2005, 2007 IBM Corporation
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.
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.
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.
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}
49 _ACCESSIBLE_CACHE = {}
52 class _PropertyCache(object):
53 '''Fixed-size bag class for holding cached values.'''
54 __slots__ = ('name', 'description', 'rolename')
58 Gets the current level of caching.
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.
68 def setCacheLevel(val):
70 Sets the desired level of caching for all accessible objects created after
71 this function is invoked. Immediately clears the current accessible cache.
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.
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)
88 r = registry.Registry()
89 r.deregisterEventListener(_updateCache, *constants.CACHE_EVENTS)
93 '''Forces a clear of the entire cache.'''
94 _ACCESSIBLE_CACHE.clear()
96 def printCache(template='%s'):
98 Prints the contents of the cache.
100 @param template: Format string to use when printing
101 @type template: string
103 print template % _ACCESSIBLE_CACHE
105 def _updateCache(event):
107 Invalidates an entry in the cache when the hash value of a source of an event
108 matches an entry in the cache.
110 @param event: One of the L{constants.CACHE_EVENTS} event types
111 @type event: L{event.Event}
114 del _ACCESSIBLE_CACHE[hash(event.source)]
120 Builds a function querying to a specific interface and returns it.
122 @param iid: Interface identifier to use when querying
124 @return: Function querying to the given interface
129 Queries an object for another interface.
131 @return: An object with the desired interface
133 @raise NotImplementedError: When the desired interface is not supported
136 i = self._icache[iid]
138 # interface not cached
141 # determine if we're caching
142 caching = _CACHE_LEVEL is not None
144 # initialize the cache
147 # check if our cached result was an interface, or an indicator that the
148 # interface is not supported
150 raise NotImplementedError
155 # do the query remotely
156 i = self.queryInterface(iid)
160 # cache that the interface is not supported
162 self._icache[iid] = None
163 raise NotImplementedError
166 # cache the narrow'ed result, but only if we're caching for this object
167 self._icache[iid] = i
172 def _makeExceptionHandler(func):
174 Builds a function calling the one it wraps in try/except statements catching
177 @return: Function calling the method being wrapped
180 def _inner(self, *args, **kwargs):
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
192 def _mixInterfaces(cls, interfaces):
194 Add methods for querying to interfaces other than the base accessible to
197 @param cls: Class to mix interface methods into
199 @param interfaces: Classes representing AT-SPI interfaces
200 @type interfaces: list of class
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)
214 def _mixExceptions(cls):
216 Wraps all methods and properties in a class with handlers for CORBA
219 @param cls: Class to mix interface methods into
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('_'):
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
240 func = getattr(cls, obj.fget.__name__)
241 getter = _makeExceptionHandler(func)
245 func = getattr(cls, obj.fset.__name__)
246 setter = _makeExceptionHandler(func)
249 setattr(cls, name, property(getter, setter))
251 def _mixClass(cls, new_cls, ignore=[]):
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
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.
262 @param cls: Existing class to mix features into
264 @param new_cls: Class containing features to add
266 @param ignore: Ignore these methods from the mixin
267 @type ignore: iterable
269 # loop over all names in the new class
270 for name, func in new_cls.__dict__.items():
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)
278 # check if a method of the same name already exists in the target
279 old_method = getattr(cls, name)
280 except AttributeError:
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):
289 # check if a method of the same name already exists in the target
290 old_method = getattr(cls, name)
291 except AttributeError:
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):
299 # check if a method of the same name already exists in the target
300 old_prop = getattr(cls, name)
301 except AttributeError:
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)
313 class _AccessibleMixin(object):
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.)
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
331 Creates a new class mimicking the one requested, but with an extra _cache
332 attribute set in the __slots__ tuple. This field can be set to a dictionary
333 or other object to allow caching to occur.
335 Note that we can't simply mix __slots__ into this class because __slots__
336 has an effect only at class creation time.
338 @param cls: Accessibility object class
340 @return: Instance of the new class
344 # check if we've already created a new version of the class
345 new_cls = _AccessibleMixin.SLOTTED_CLASSES[cls]
347 # create the new class if not
348 new_cls = type(cls.__name__, (cls,),
349 {'__module__' : cls.__module__,
350 '__slots__' : _AccessibleMixin.SLOTS})
351 _AccessibleMixin.SLOTTED_CLASSES[cls] = new_cls
352 obj = cls._mix___new__(new_cls)
353 # don't create the interface cache until we need it
359 Decrements the reference count on the accessible object when there are no
360 Python references to this object. This provides automatic reference
361 counting for AT-SPI objects. Also removes this object from the cache if
362 we're caching properties.
365 del _ACCESSIBLE_CACHE[hash(self)]
375 Iterator that yields one accessible child per iteration. If an exception is
376 encountered, None is yielded instead.
378 @return: A child accessible
379 @rtype: Accessibility.Accessible
381 for i in xrange(self.childCount):
383 yield self.getChildAtIndex(i)
389 Gets a human readable representation of the accessible.
391 @return: Role and name information for the accessible
395 return '[%s | %s]' % (self.getRoleName(), self.name)
399 def __nonzero__(self):
401 @return: True, always
406 def __getitem__(self, index):
408 Thin wrapper around getChildAtIndex.
410 @param index: Index of desired child
412 @return: Accessible child
413 @rtype: Accessibility.Accessible
422 return self.getChildAtIndex(index)
426 Thin wrapper around childCount.
428 @return: Number of child accessibles
431 return self.childCount
435 Gets the name of the accessible from the cache if it is available,
436 otherwise, fetches it remotely.
438 @return: Name of the accessible
441 if _CACHE_LEVEL != constants.CACHE_PROPERTIES:
442 return self._get_name()
444 cache = _ACCESSIBLE_CACHE
449 # no cached info for this object yet
450 name = self._get_name()
451 pc = _PropertyCache()
455 except AttributeError:
456 # no cached name for this object yet
457 name = self._get_name()
461 name = property(_get_name, Accessibility.Accessible._set_name)
463 def getRoleName(self):
465 Gets the unlocalized role name of the accessible from the cache if it is
466 available, otherwise, fetches it remotely.
468 @return: Role name of the accessible
471 if _CACHE_LEVEL != constants.CACHE_PROPERTIES:
472 return self._mix_getRoleName()
474 cache = _ACCESSIBLE_CACHE
477 return cache[h].rolename
479 # no cached info for this object yet
480 rolename = self._mix_getRoleName()
481 pc = _PropertyCache()
482 pc.rolename = rolename
485 except AttributeError, e:
486 # no cached name for this object yet
487 rolename = self._mix_getRoleName()
488 cache[h].rolename = rolename
491 def _get_description(self):
493 Gets the description of the accessible from the cache if it is available,
494 otherwise, fetches it remotely.
496 @return: Description of the accessible
499 if _CACHE_LEVEL != constants.CACHE_PROPERTIES:
500 return self._get_description()
502 cache = _ACCESSIBLE_CACHE
505 return cache[h].description
507 # no cached info for this object yet
508 description = self._get_description()
509 pc = _PropertyCache()
510 pc.description = description
513 except AttributeError:
514 # no cached name for this object yet
515 description = self._get_description()
516 cache[h].description = description
519 description = property(_get_description,
520 Accessibility.Accessible._set_description)
522 def getIndexInParent(self):
524 Gets the index of this accessible in its parent. Uses the implementation of
525 this method provided by the Accessibility.Accessible object, but checks the
526 bound of the value to ensure it is not outside the range of childCount
527 reported by this accessible's parent.
529 @return: Index of this accessible in its parent
532 i = self._mix_getIndexInParent()
534 # correct for out-of-bounds index reporting
535 return min(self.parent.childCount-1, i)
536 except AttributeError:
537 # return sentinel if there is no parent
540 def getApplication(self):
542 Gets the most-parent accessible (the application) of this accessible. Tries
543 using the getApplication method introduced in AT-SPI 1.7.0 first before
544 resorting to traversing parent links.
546 @warning: Cycles involving more than the previously traversed accessible
547 are not detected by this code.
548 @return: Application object
549 @rtype: Accessibility.Application
552 return self._mix_getApplication()
553 except AttributeError:
557 while curr.parent is not None and (not curr.parent == curr):
562 # return None if the application isn't reachable for any reason
565 # 1. mix the exception handlers into all queryable interfaces
566 map(_mixExceptions, constants.ALL_INTERFACES)
567 # 2. mix the exception handlers into other Accessibility objects
568 map(_mixExceptions, [Accessibility.StateSet])
569 # 3. mix the new functions
570 _mixClass(Accessibility.Accessible, _AccessibleMixin,
571 ['_get_name', '_get_description'])
572 # 4. mix queryInterface convenience methods
573 _mixInterfaces(Accessibility.Accessible, constants.ALL_INTERFACES)