pyatspi
WillieWalker and PeterParente met at CSUN07 and discussed the creation of an "official" Python wrapper for AT-SPI. This wrapper is now part of the GNOME at-spi module for any assistive technology or automated testing tool to use.
Goals
- To create a single Python wrapper for AT-SPI reusable across all Python assistive technolgies and test tools for GNOME
- To combine the best portions of LSR's pyLinAcc package and Orca's atspi module
- To provide a lightweight, pure-Python package
- To hide the CORBA-isms of accessing AT-SPI through pyORBit
- To minimize churn if/when the AT-SPI transport layer is changed
- To minimize future manual maintenance as AT-SPI changes
- To maximize performance with optional interface and property caching
- To limit event registration to one listener and multiplex
- To keep naming similar to AT-SPI
- To make portions of the interface more Pythonic
- To avoid assuming anything about the requirements of a client
See the original specification for more details.
Projects using pyatspi
Accerciser (0.1.2 and up)
Dogtail (port started in branch)
LDTP (Recording is completed ported / Execution engine port in progress - estimated for GNOME 2.22)
Orca (estimated for GNOME 2.20 or 2.22)
LSR (estimated for GNOME 2.20)
Current code
The code is feature complete according to the spec below. The code will ship with at-spi in GNOME 2.20. The code will work with previous versions of AT-SPI, however. You can get it and install it on any system by doing the following:
svn co svn://svn.gnome.org/svn/at-spi/trunk/pyatspi pyatspi cd pyatspi sudo python setup.py install
API documentation
See http://www.gnome.org/~parente/pyatspi/doc for epydoc HTML.
Problems
- How do we know when all keys have been unregistered from a device observer so that we can destroy the object?
- How do we support the option to initialize the ORB in threaded mode or not if the ORB must be initialized on import.
Bugs and enhancements
To report a bug or request an enhancement, please use at-spi and select python-bindings as the component.
Example usage
Basic calls
1 import pyatspi
2 reg = pyatspi.Registry
3 print reg.getDesktopCount()
4 desktop = reg.getDesktop(0)
5 # check if the desktop object is the same as its desktop interface
6 print desktop == desktop.queryDesktop()
7 try:
8 # check if the desktop has the Table interface (definitely not)
9 desktop.queryTable()
10 except NotImplementedError:
11 pass
12 # get the number of running apps
13 print len(desktop)
14 # show all the running apps
15 apps = [app.name for app in desktop]
16 print apps
Event listening
1 import pyatspi
2 def callback(event):
3 print event
4 reg = pyatspi.Registry
5 # register for focus and caret movement events
6 reg.registerEventListener(callback, 'focus', 'object:text-caret-moved')
7 # register for presses and releases of all keys with all possible modifiers
8 reg.registerKeystrokeListener(callback, mask=pyatspi.allModifiers())
9 reg.start()
Event generation
1 import pyatspi
2 reg = pyatspi.Registry
3 # generate a button 1 click
4 reg.generateMouseEvent(0, 0, pyatspi.MOUSE_B1C)
5 # generate a press and release of Enter
6 reg.generateKeyboardEvent(36, '', pyatspi.KEY_PRESSRELEASE)
Searching
1 import pyatspi
2 reg = pyatspi.Registry
3 desktop = reg.getDesktop(0)
4 # find the gedit application, breadth first
5 gedit = pyatspi.findDescendant(desktop, lambda x: x.name == 'gedit', breadth_first=True)
6 print gedit
7 # find the first text area
8 text = pyatspi.findDescendant(gedit, lambda x: x.getRoleName() == 'text')
9 print text
Cache profiling
1 import pyatspi
2 import timeit
3 import pprint
4 import gobject
5 def query(acc, n, *ints):
6 '''Query to the given interfaces n times. Return the times taken to query.'''
7 t_ints = []
8 for i in ints:
9 for j in xrange(n):
10 t1 = timeit.default_timer()
11 try:
12 a = i()
13 except NotImplementedError:
14 pass
15 t_ints.append(timeit.default_timer()-t1)
16 return t_ints
17 def props(acc, n):
18 '''Fetch the name, description, and role name properties n times. Return the
19 times taken to fetch.'''
20 t_ints = []
21 for j in xrange(n):
22 t1 = timeit.default_timer()
23 a = acc.name
24 b = acc.description
25 c = acc.getRoleName()
26 t_ints.append(timeit.default_timer()-t1)
27 return t_ints
28 reg = pyatspi.Registry
29 d = reg.getDesktop(0)
30 gedit = pyatspi.findDescendant(d, lambda x: x.name == 'gedit', True)
31 if gedit is None:
32 raise RuntimeError('run gedit before starting the test')
33 def testInterfaces():
34 print 'testing interface speedup'
35 print '========================='
36 avg_i = []
37 for caching in (None, pyatspi.CACHE_INTERFACES):
38 text = pyatspi.findDescendant(gedit, lambda x: x.getRoleName() == 'text')
39 pyatspi.setCacheLevel(caching)
40 print 'cache level: ', caching, text
41 results = query(text, 100, text.queryText, text.queryComponent,
42 text.queryStreamableContent, text.queryDesktop)
43 avg_i.append(sum(results) / float(len(results)))
44 print 'interface averages', avg_i
45 print 'interface speedup', avg_i[0] / avg_i[1]
46 print
47 def testProps():
48 print 'testing properties speedup'
49 print '=========================='
50 avg_p = []
51 for caching in (None, pyatspi.CACHE_PROPERTIES):
52 text = pyatspi.findDescendant(gedit, lambda x: x.getRoleName() == 'text')
53 pyatspi.setCacheLevel(caching)
54 print 'cache level: ', caching, text
55 results = props(text, 100)
56 avg_p.append(sum(results) / float(len(results)))
57 print 'cache contents: ', pyatspi.printCache()
58 print 'properties averages', avg_p
59 print 'properties speedup', avg_p[0] / avg_p[1]
60 def main():
61 testInterfaces()
62 testProps()
63 print 'exiting...'
64 print 'cache contents: ',
65 pyatspi.printCache()
66 reg.stop()
67 return False
68 gobject.timeout_add(0, main)
69 reg.start()
