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)]
118 def _makeQuery(interface):
120 Builds a function querying to a specific interface and returns it.
122 @param interface: Class representing an AT-SPI interface
123 @type interface: class
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
135 iid = utils.getInterfaceIID(interface)
137 i = self._icache[iid]
139 # interface not cached
141 except AttributeError:
142 # determine if we're caching
143 caching = _CACHE_LEVEL is not None
145 # initialize the cache
148 # check if our cached result was an interface, or an indicator that the
149 # interface is not supported
151 raise NotImplementedError
156 # do the query remotely
157 i = self.queryInterface(iid)
159 i = i._narrow(interface)
163 # cache that the interface is not supported
165 self._icache[iid] = None
166 raise NotImplementedError
169 # cache the narrow'ed result, but only if we're caching for this object
170 self._icache[iid] = i
175 def _makeExceptionHandler(func):
177 Builds a function calling the one it wraps in try/except statements catching
180 @return: Function calling the method being wrapped
183 def _inner(self, *args, **kwargs):
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
195 def _mixInterfaces(cls, interfaces):
197 Add methods for querying to interfaces other than the base accessible to
200 @param cls: Class to mix interface methods into
202 @param interfaces: Classes representing AT-SPI interfaces
203 @type interfaces: list of class
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)
217 def _mixExceptions(cls):
219 Wraps all methods and properties in a class with handlers for CORBA
222 @param cls: Class to mix interface methods into
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('_'):
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
243 func = getattr(cls, obj.fget.__name__)
244 getter = _makeExceptionHandler(func)
248 func = getattr(cls, obj.fset.__name__)
249 setter = _makeExceptionHandler(func)
252 setattr(cls, name, property(getter, setter))
254 def _mixClass(cls, new_cls, ignore=[]):
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
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.
265 @param cls: Existing class to mix features into
267 @param new_cls: Class containing features to add
269 @param ignore: Ignore these methods from the mixin
270 @type ignore: iterable
272 # loop over all names in the new class
273 for name, func in new_cls.__dict__.items():
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)
281 # check if a method of the same name already exists in the target
282 old_method = getattr(cls, name)
283 except AttributeError:
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):
292 # check if a method of the same name already exists in the target
293 old_method = getattr(cls, name)
294 except AttributeError:
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):
302 # check if a method of the same name already exists in the target
303 old_prop = getattr(cls, name)
304 except AttributeError:
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)
316 class _AccessibleMixin(object):
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.)
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
330 SLOTS = ('_icache', 'user_data')
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.
339 Note that we can't simply mix __slots__ into this class because __slots__
340 has an effect only at class creation time.
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
347 @param cls: Accessibility object class
349 @return: Instance of the new class
353 # check if we've already created a new version of the class
354 new_cls = _AccessibleMixin.SLOTTED_CLASSES[cls]
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)
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.
372 del _ACCESSIBLE_CACHE[hash(self)]
382 Iterator that yields one accessible child per iteration. If an exception is
383 encountered, None is yielded instead.
385 @return: A child accessible
386 @rtype: Accessibility.Accessible
388 for i in xrange(self.childCount):
390 yield self.getChildAtIndex(i)
396 Gets a human readable representation of the accessible.
398 @return: Role and name information for the accessible
402 return '[%s | %s]' % (self.getRoleName(), self.name)
406 def __nonzero__(self):
408 @return: True, always
413 def __getitem__(self, index):
415 Thin wrapper around getChildAtIndex.
417 @param index: Index of desired child
419 @return: Accessible child
420 @rtype: Accessibility.Accessible
429 return self.getChildAtIndex(index)
433 Thin wrapper around childCount.
435 @return: Number of child accessibles
438 return self.childCount
442 Gets the name of the accessible from the cache if it is available,
443 otherwise, fetches it remotely.
445 @return: Name of the accessible
448 if _CACHE_LEVEL != constants.CACHE_PROPERTIES:
449 return self._get_name()
451 cache = _ACCESSIBLE_CACHE
456 # no cached info for this object yet
457 name = self._get_name()
458 pc = _PropertyCache()
462 except AttributeError:
463 # no cached name for this object yet
464 name = self._get_name()
468 name = property(_get_name, Accessibility.Accessible._set_name)
470 def getRoleName(self):
472 Gets the unlocalized role name of the accessible from the cache if it is
473 available, otherwise, fetches it remotely.
475 @return: Role name of the accessible
478 if _CACHE_LEVEL != constants.CACHE_PROPERTIES:
479 return self._mix_getRoleName()
481 cache = _ACCESSIBLE_CACHE
484 return cache[h].rolename
486 # no cached info for this object yet
487 rolename = self._mix_getRoleName()
488 pc = _PropertyCache()
489 pc.rolename = rolename
492 except AttributeError, e:
493 # no cached name for this object yet
494 rolename = self._mix_getRoleName()
495 cache[h].rolename = rolename
498 def _get_description(self):
500 Gets the description of the accessible from the cache if it is available,
501 otherwise, fetches it remotely.
503 @return: Description of the accessible
506 if _CACHE_LEVEL != constants.CACHE_PROPERTIES:
507 return self._get_description()
509 cache = _ACCESSIBLE_CACHE
512 return cache[h].description
514 # no cached info for this object yet
515 description = self._get_description()
516 pc = _PropertyCache()
517 pc.description = description
520 except AttributeError:
521 # no cached name for this object yet
522 description = self._get_description()
523 cache[h].description = description
526 description = property(_get_description,
527 Accessibility.Accessible._set_description)
529 def getIndexInParent(self):
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.
536 @return: Index of this accessible in its parent
539 i = self._mix_getIndexInParent()
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
547 def getApplication(self):
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.
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
559 return self._mix_getApplication()
560 except AttributeError:
564 while curr.parent is not None and (not curr.parent == curr):
569 # return None if the application isn't reachable for any reason
572 class _RelationMixin(object):
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.
580 def getTarget(self, index):
582 Overrides the regular getTarget to return Accessibility.Accessible
585 @return: The 'nth' target of this Relation.
586 @rtype: Accessibility.Accessible
588 target = self._mix_getTarget(index)
589 return target._narrow(Accessibility.Accessible)
591 # 1. mix the exception handlers into all queryable interfaces
592 map(_mixExceptions, constants.ALL_INTERFACES)
593 # 2. mix the exception handlers into other Accessibility objects
594 map(_mixExceptions, [Accessibility.StateSet])
595 # 3. mix the new functions
596 _mixClass(Accessibility.Accessible, _AccessibleMixin,
597 ['_get_name', '_get_description'])
598 # 4. mix queryInterface convenience methods
599 _mixInterfaces(Accessibility.Accessible, constants.ALL_INTERFACES)
600 # 5. mix Relation class
601 _mixClass(Accessibility.Relation, _RelationMixin)