diff options
author | vivian, zhang <vivian.zhang@intel.com> | 2012-06-03 11:21:25 +0800 |
---|---|---|
committer | vivian, zhang <vivian.zhang@intel.com> | 2012-06-03 11:21:25 +0800 |
commit | 018fcb4a1852ab3368903d57bc0b5e54fa03e6c8 (patch) | |
tree | 108a0f4727db6ea287725f85bbca3eee9801983a /doc/tutorial.txt | |
parent | c0384c46d79b70ce46d887fdaa2f4c59fb89fd33 (diff) | |
download | dbus-python-018fcb4a1852ab3368903d57bc0b5e54fa03e6c8.tar.gz dbus-python-018fcb4a1852ab3368903d57bc0b5e54fa03e6c8.tar.bz2 dbus-python-018fcb4a1852ab3368903d57bc0b5e54fa03e6c8.zip |
Diffstat (limited to 'doc/tutorial.txt')
-rw-r--r-- | doc/tutorial.txt | 681 |
1 files changed, 681 insertions, 0 deletions
diff --git a/doc/tutorial.txt b/doc/tutorial.txt new file mode 100644 index 0000000..73251cf --- /dev/null +++ b/doc/tutorial.txt @@ -0,0 +1,681 @@ +==================== +dbus-python tutorial +==================== + +:Author: Simon McVittie, `Collabora Ltd.`_ +:Date: 2006-06-14 + +.. _`Collabora Ltd.`: http://www.collabora.co.uk/ + +This tutorial requires Python 2.4 or up, and ``dbus-python`` 0.80rc4 or up. + +.. contents:: + +.. -------------------------------------------------------------------- + +.. _Bus object: +.. _Bus objects: + +Connecting to the Bus +===================== + +Applications that use D-Bus typically connect to a *bus daemon*, which +forwards messages between the applications. To use D-Bus, you need to create a +``Bus`` object representing the connection to the bus daemon. + +There are generally two bus daemons you may be interested in. Each user +login session should have a *session bus*, which is local to that +session. It's used to communicate between desktop applications. Connect +to the session bus by creating a ``SessionBus`` object:: + + import dbus + + session_bus = dbus.SessionBus() + +The *system bus* is global and usually started during boot; it's used to +communicate with system services like udev_, NetworkManager_, and the +`Hardware Abstraction Layer daemon (hald)`_. To connect to the system +bus, create a ``SystemBus`` object:: + + import dbus + + system_bus = dbus.SystemBus() + +Of course, you can connect to both in the same application. + +For special purposes, you might use a non-default Bus, or a connection +which isn't a Bus at all, using some new API added in dbus-python 0.81.0. +This is not described here, and will at some stage be the subject of a separate +tutorial. + +.. _udev: + http://www.kernel.org/pub/linux/utils/kernel/hotplug/udev.html +.. _NetworkManager: + http://www.gnome.org/projects/NetworkManager/ +.. _Hardware Abstraction Layer daemon (hald): + http://www.freedesktop.org/wiki/Software/hal + +.. -------------------------------------------------------------------- + +Making method calls +=================== + +D-Bus applications can export objects for other applications' use. To +start working with an object in another application, you need to know: + +* The *bus name*. This identifies which application you want to + communicate with. You'll usually identify applications by a + *well-known name*, which is a dot-separated string starting with a + reversed domain name, such as ``org.freedesktop.NetworkManager`` + or ``com.example.WordProcessor``. + +* The *object path*. Applications can export many objects - for + instance, example.com's word processor might provide an object + representing the word processor application itself and an object for + each document window opened, or it might also provide an object for + each paragraph within a document. + + To identify which one you want to interact with, you use an object path, + a slash-separated string resembling a filename. For instance, example.com's + word processor might provide an object at ``/`` representing the word + processor itself, and objects at ``/documents/123`` and + ``/documents/345`` representing opened document windows. + +As you'd expect, one of the main things you can do with remote objects +is to call their methods. As in Python, methods may have parameters, +and they may return one or more values. + +.. _proxy object: + +Proxy objects +------------- + +To interact with a remote object, you use a *proxy object*. This is a +Python object which acts as a proxy or "stand-in" for the remote object - +when you call a method on a proxy object, this causes dbus-python to make +a method call on the remote object, passing back any return values from +the remote object's method as the return values of the proxy method call. + +To obtain a proxy object, call the ``get_object`` method on the ``Bus``. +For example, NetworkManager_ has the well-known name +``org.freedesktop.NetworkManager`` and exports an object whose object +path is ``/org/freedesktop/NetworkManager``, plus an object per network +interface at object paths like +``/org/freedesktop/NetworkManager/Devices/eth0``. You can get a proxy +for the object representing eth0 like this:: + + import dbus + bus = dbus.SystemBus() + proxy = bus.get_object('org.freedesktop.NetworkManager', + '/org/freedesktop/NetworkManager/Devices/eth0') + # proxy is a dbus.proxies.ProxyObject + +Interfaces and methods +---------------------- + +D-Bus uses *interfaces* to provide a namespacing mechanism for methods. +An interface is a group of related methods and signals (more on signals +later), identified by a name which is a series of dot-separated components +starting with a reversed domain name. For instance, each NetworkManager_ +object representing a network interface implements the interface +``org.freedesktop.NetworkManager.Devices``, which has methods like +``getProperties``. + +To call a method, call the method of the same name on the proxy object, +passing in the interface name via the ``dbus_interface`` keyword argument:: + + import dbus + bus = dbus.SystemBus() + eth0 = bus.get_object('org.freedesktop.NetworkManager', + '/org/freedesktop/NetworkManager/Devices/eth0') + props = eth0.getProperties(dbus_interface='org.freedesktop.NetworkManager.Devices') + # props is a tuple of properties, the first of which is the object path + +.. _dbus.Interface: + +As a short cut, if you're going to be calling many methods with the same +interface, you can construct a ``dbus.Interface`` object and call +methods on that, without needing to specify the interface again:: + + import dbus + bus = dbus.SystemBus() + eth0 = bus.get_object('org.freedesktop.NetworkManager', + '/org/freedesktop/NetworkManager/Devices/eth0') + eth0_dev_iface = dbus.Interface(eth0, + dbus_interface='org.freedesktop.NetworkManager.Devices') + props = eth0_dev_iface.getProperties() + # props is the same as before + +See also +~~~~~~~~ + +See the example in ``examples/example-client.py``. Before running it, +you'll need to run ``examples/example-service.py`` in the background or +in another shell. + +Data types +---------- + +Unlike Python, D-Bus is statically typed - each method has a certain +*signature* representing the types of its arguments, and will not accept +arguments of other types. + +D-Bus has an introspection mechanism, which ``dbus-python`` tries to use +to discover the correct argument types. If this succeeds, Python types +are converted into the right D-Bus data types automatically, if possible; +``TypeError`` is raised if the type is inappropriate. + +If the introspection mechanism fails (or the argument's type is +variant - see below), you have to provide arguments of +the correct type. ``dbus-python`` provides Python types corresponding to +the D-Bus data types, and a few native Python types are also converted to +D-Bus data types automatically. If you use a type which isn't among these, +a ``TypeError`` will be raised telling you that ``dbus-python`` was +unable to guess the D-Bus signature. + +Basic types +~~~~~~~~~~~ + +The following basic data types are supported. + +========================== ============================= ===== +Python type converted to D-Bus type notes +========================== ============================= ===== +D-Bus `proxy object`_ ObjectPath (signature 'o') `(+)`_ +`dbus.Interface`_ ObjectPath (signature 'o') `(+)`_ +`dbus.service.Object`_ ObjectPath (signature 'o') `(+)`_ +``dbus.Boolean`` Boolean (signature 'b') a subclass of ``int`` +``dbus.Byte`` byte (signature 'y') a subclass of ``int`` +``dbus.Int16`` 16-bit signed integer ('n') a subclass of ``int`` +``dbus.Int32`` 32-bit signed integer ('i') a subclass of ``int`` +``dbus.Int64`` 64-bit signed integer ('x') `(*)`_ +``dbus.UInt16`` 16-bit unsigned integer ('q') a subclass of ``int`` +``dbus.UInt32`` 32-bit unsigned integer ('u') `(*)_` +``dbus.UInt64`` 64-bit unsigned integer ('t') `(*)_` +``dbus.Double`` double-precision float ('d') a subclass of ``float`` +``dbus.ObjectPath`` object path ('o') a subclass of ``str`` +``dbus.Signature`` signature ('g') a subclass of ``str`` +``dbus.String`` string ('s') a subclass of + ``unicode`` +``dbus.UTF8String`` string ('s') a subclass of ``str`` +``bool`` Boolean ('b') +``int`` or subclass 32-bit signed integer ('i') +``long`` or subclass 64-bit signed integer ('x') +``float`` or subclass double-precision float ('d') +``str`` or subclass string ('s') must be valid UTF-8 +``unicode`` or subclass string ('s') +========================== ============================= ===== + +.. _(*): + +Types marked (*) may be a subclass of either ``int`` or ``long``, depending +on platform. + +.. _(+): + +(+): D-Bus proxy objects, exported D-Bus service objects and anything +else with the special attribute ``__dbus_object_path__``, which +must be a string, are converted to their object-path. This might be +useful if you're writing an object-oriented API using dbus-python. + +Basic type conversions +~~~~~~~~~~~~~~~~~~~~~~ + +If introspection succeeded, ``dbus-python`` will also accept: + +* for Boolean parameters, any object (converted as if via ``int(bool(...))``) +* for byte parameters, a single-character string (converted as if via ``ord()``) +* for byte and integer parameters, any integer (must be in the correct range) +* for object-path and signature parameters, any ``str`` or ``unicode`` + subclass (the value must follow the appropriate syntax) + +Container types +~~~~~~~~~~~~~~~ + +D-Bus supports four container types: array (a variable-length sequence of the +same type), struct (a fixed-length sequence whose members may have +different types), dictionary (a mapping from values of the same basic type to +values of the same type), and variant (a container which may hold any +D-Bus type, including another variant). + +Arrays are represented by Python lists, or by ``dbus.Array``, a subclass +of ``list``. When sending an array, if an introspected signature is +available, that will be used; otherwise, if the ``signature`` keyword +parameter was passed to the ``Array`` constructor, that will be used to +determine the contents' signature; otherwise, ``dbus-python`` will guess +from the array's first item. + +The signature of an array is 'ax' where 'x' represents the signature of +one item. For instance, you could also have 'as' (array of strings) or +'a(ii)' (array of structs each containing two 32-bit integers). + +There's also a type ``dbus.ByteArray`` which is a subclass of ``str``, +used as a more efficient representation of a D-Bus array of bytes +(signature 'ay'). + +Structs are represented by Python tuples, or by ``dbus.Struct``, a +subclass of ``tuple``. When sending a struct, if an introspected signature is +available, that will be used; otherwise, if the ``signature`` keyword +parameter was passed to the ``Array`` constructor, that will be used to +determine the contents' signature; otherwise, ``dbus-python`` will guess +from the array's first item. + +The signature of a struct consists of the signatures of the contents, +in parentheses - for instance '(is)' is the signature of a struct +containing a 32-bit integer and a string. + +Dictionaries are represented by Python dictionaries, or by +``dbus.Dictionary``, a subclass of ``dict``. When sending a dictionary, +if an introspected signature is available, that will be used; otherwise, +if the ``signature`` keyword parameter was passed to the ``Dictionary`` +constructor, that will be used to determine the contents' key and value +signatures; otherwise, ``dbus-python`` will guess from an arbitrary item +of the ``dict``. + +The signature of a dictionary is 'a{xy}' where 'x' represents the +signature of the keys (which may not be a container type) and 'y' +represents the signature of the values. For instance, +'a{s(ii)}' is a dictionary where the keys are strings and the values are +structs containing two 32-bit integers. + +Variants are represented by setting the ``variant_level`` keyword +argument in the constructor of any D-Bus data type to a value greater +than 0 (``variant_level`` 1 means a variant containing some other data type, +``variant_level`` 2 means a variant containing a variant containing some +other data type, and so on). If a non-variant is passed as an argument +but introspection indicates that a variant is expected, it'll +automatically be wrapped in a variant. + +The signature of a variant is 'v'. + +.. _byte_arrays and utf8_strings: + +Return values, and the ``byte_arrays`` and ``utf8_strings`` options +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a D-Bus method returns no value, the Python proxy method will return +``None``. + +If a D-Bus method returns one value, the Python proxy method will return +that value as one of the ``dbus.`` types - by default, strings are +returned as ``dbus.String`` (a subclass of Unicode) and byte arrays are +returned as a ``dbus.Array`` of ``dbus.Byte``. + +If a D-Bus method returns multiple values, the Python proxy method +will return a tuple containing those values. + +If you want strings returned as ``dbus.UTF8String`` (a subclass of +``str``) pass the keyword parameter ``utf8_strings=True`` to the proxy +method. + +If you want byte arrays returned as ``dbus.ByteArray`` (also a +subclass of ``str`` - in practice, this is often what you want) pass +the keyword parameter ``byte_arrays=True`` to the proxy method. + +.. -------------------------------------------------------------------- + +Making asynchronous method calls +================================ + +Asynchronous (non-blocking) method calls allow multiple method calls to +be in progress simultaneously, and allow your application to do other +work while it's waiting for the results. To make asynchronous calls, +you first need an event loop or "main loop". + +Setting up an event loop +------------------------ + +Currently, the only main loop supported by ``dbus-python`` is GLib. + +``dbus-python`` has a global default main loop, which is the easiest way +to use this functionality. To arrange for the GLib main loop to be the +default, use:: + + from dbus.mainloop.glib import DBusGMainLoop + + DBusGMainLoop(set_as_default=True) + +You must do this before `connecting to the bus`_. + +Actually starting the main loop is as usual for ``pygobject``:: + + import gobject + + loop = gobject.MainLoop() + loop.run() + +While ``loop.run()`` is executing, GLib will run your callbacks when +appropriate. To stop, call ``loop.quit()``. + +You can also set a main loop on a per-connection basis, by passing a +main loop to the Bus constructor:: + + import dbus + from dbus.mainloop.glib import DBusGMainLoop + + dbus_loop = DBusGMainLoop() + + bus = dbus.SessionBus(mainloop=dbus_loop) + +This isn't very useful until we support more than one main loop, though. + +Backwards compatibility: ``dbus.glib`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In versions of ``dbus-python`` prior to 0.80, the way to set GLib as the +default main loop was:: + + import dbus.glib + +Executing that import statement would automatically load the GLib main +loop and make this the default. This is now deprecated, since it's +highly non-obvious, but may be useful if you want to write or understand +backwards-compatible code. + +The Qt main loop +~~~~~~~~~~~~~~~~ + +PyQt v4.2 and later includes support for integrating dbus-python with +the Qt event loop. To connect D-Bus to this main loop, call +``dbus.mainloop.qt.DBusQtMainLoop`` instead of +``dbus.mainloop.glib.DBusGMainLoop``. Otherwise the Qt loop is used in +exactly the same way as the GLib loop. + +Making asynchronous calls +------------------------- + +To make a call asynchronous, pass two callables as keyword arguments +``reply_handler`` and ``error_handler`` to the proxy method. The proxy +method will immediately return `None`. At some later time, when the event +loop is running, one of these will happen: either + +* the ``reply_handler`` will be called with the method's return values + as arguments; or + +* the ``error_handler`` will be called with one argument, an instance of + ``DBusException`` representing a remote exception. + +See also +~~~~~~~~ + +``examples/example-async-client.py`` makes asynchronous method calls to +the service provided by ``examples/example-service.py`` which return +either a value or an exception. As for ``examples/example-client.py``, +you need to run ``examples/example-service.py`` in the background or +in another shell first. + +.. -------------------------------------------------------------------- + +Receiving signals +================= + +To receive signals, the Bus needs to be connected to an event loop - see +section `Setting up an event loop`_. Signals will only be received while +the event loop is running. + +Signal matching +--------------- + +To respond to signals, you can use the ``add_signal_receiver`` method on +`Bus objects`_. This arranges for a callback to be called when a +matching signal is received, and has the following arguments: + +* a callable (the ``handler_function``) which will be called by the event loop + when the signal is received - its parameters will be the arguments of + the signal + +* the signal name, ``signal_name``: here None (the default) matches all names + +* the D-Bus interface, ``dbus_interface``: again None is the default, + and matches all interfaces + +* a sender bus name (well-known or unique), ``bus_name``: None is again + the default, and matches all senders. Well-known names match signals + from whatever application is currently the primary owner of that + well-known name. + +* a sender object path, ``path``: once again None is the default and + matches all object paths + +``add_signal_receiver`` also has keyword arguments ``utf8_strings`` and +``byte_arrays`` which influence the types used when calling the +handler function, in the same way as the `byte_arrays and utf8_strings`_ +options on proxy methods. + +``add_signal_receiver`` returns a ``SignalMatch`` object. Its only +useful public API at the moment is a ``remove`` method with no +arguments, which removes the signal match from the connection. + +Getting more information from a signal +-------------------------------------- + +You can also arrange for more information to be passed to the handler +function. If you pass the keyword arguments ``sender_keyword``, +``destination_keyword``, ``interface_keyword``, ``member_keyword`` or +``path_keyword`` to the ``connect_to_signal`` method, the appropriate +part of the signal message will be passed to the handler function as a +keyword argument: for instance if you use :: + + def handler(sender=None): + print "got signal from %r" % sender + + iface.connect_to_signal("Hello", handler, sender_keyword='sender') + +and a signal ``Hello`` with no arguments is received from +``com.example.Foo``, the ``handler`` function will be called with +``sender='com.example.Foo'``. + +String argument matching +------------------------ + +If there are keyword parameters for the form ``arg``\ *n* where n is a +small non-negative number, their values must be ``unicode`` objects +or UTF-8 strings. The handler will only be called if that argument +of the signal (numbered from zero) is a D-Bus string (in particular, +not an object-path or a signature) with that value. + +.. *this comment is to stop the above breaking vim syntax highlighting* + +Receiving signals from a proxy object +------------------------------------- + +`Proxy objects`_ have a special method ``connect_to_signal`` which +arranges for a callback to be called when a signal is received +from the corresponding remote object. The parameters are: + +* the name of the signal + +* a callable (the handler function) which will be called by the event loop + when the signal is received - its parameters will be the arguments of + the signal + +* the handler function, a callable: the same as for ``add_signal_receiver`` + +* the keyword argument ``dbus_interface`` qualifies the name with its + interface + +`dbus.Interface` objects have a similar ``connect_to_signal`` method, +but in this case you don't need the ``dbus_interface`` keyword argument +since the interface to use is already known. + +The same extra keyword arguments as for ``add_signal_receiver`` are also +available, and just like ``add_signal_receiver``, it returns a +SignalMatch. + +You shouldn't use proxy objects just to listen to signals, since they +might activate the relevant service when created, but if you already have a +proxy object in order to call methods, it's often convenient to use it to add +signal matches too. + +See also +-------- + +``examples/signal-recipient.py`` receives signals - it demonstrates +general signal matching as well as ``connect_to_signal``. Before running it, +you'll need to run ``examples/signal-emitter.py`` in the background or +in another shell. + +.. _BusName: + +.. -------------------------------------------------------------------- + +Claiming a bus name +=================== + +FIXME describe `BusName`_ - perhaps fix its API first? + +The unique-instance idiom +------------------------- + +FIXME provide exemplary code, put it in examples + +.. _exported object: +.. _exported objects: + +.. -------------------------------------------------------------------- + +Exporting objects +================= + +Objects made available to other applications over D-Bus are said to be +*exported*. All subclasses of ``dbus.service.Object`` are automatically +exported. + +To export objects, the Bus needs to be connected to an event loop - see +section `Setting up an event loop`_. Exported methods will only be called, +and queued signals will only be sent, while the event loop is running. + +.. _dbus.service.Object: + +Inheriting from ``dbus.service.Object`` +--------------------------------------- + +To export an object onto the Bus, just subclass +``dbus.service.Object``. Object expects either a `BusName`_ or a `Bus +object`_, and an object-path, to be passed to its constructor: arrange +for this information to be available. For example:: + + class Example(dbus.service.Object): + def __init__(self, object_path): + dbus.service.Object.__init__(self, dbus.SessionBus(), path) + +This object will automatically support introspection, but won't do +anything particularly interesting. To fix that, you'll need to export some +methods and signals too. + +FIXME also mention dbus.gobject.ExportedGObject once I've written it + +Exporting methods with ``dbus.service.method`` +---------------------------------------------- + +To export a method, use the decorator ``dbus.service.method``. For +example:: + + class Example(dbus.service.Object): + def __init__(self, object_path): + dbus.service.Object.__init__(self, dbus.SessionBus(), path) + + @dbus.service.method(dbus_interface='com.example.Sample', + in_signature='v', out_signature='s') + def StringifyVariant(self, variant): + return str(variant) + +The ``in_signature`` and ``out_signature`` are D-Bus signature strings +as described in `Data Types`_. + +As well as the keywords shown, you can pass ``utf8_strings`` and +``byte_arrays`` keyword arguments, which influence the types which will +be passed to the decorated method when it's called via D-Bus, in the +same way that the `byte_arrays and utf8_strings`_ options affect the +return value of a proxy method. + +You can find a simple example in ``examples/example-service.py``, which +we used earlier to demonstrate ``examples/example-client.py``. + +Finding out the caller's bus name +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``method`` decorator accepts a ``sender_keyword`` keyword argument. +If you set that to a string, the unique bus name of the sender will be +passed to the decorated method as a keyword argument of that name:: + + class Example(dbus.service.Object): + def __init__(self, object_path): + dbus.service.Object.__init__(self, dbus.SessionBus(), path) + + @dbus.service.method(dbus_interface='com.example.Sample', + in_signature='', out_signature='s', + sender_keyword='sender') + def SayHello(self, sender=None): + return 'Hello, %s!' % sender + # -> something like 'Hello, :1.1!' + +Asynchronous method implementations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +FIXME and also add an example, perhaps examples/example-async-service.py + +Emitting signals with ``dbus.service.signal`` +--------------------------------------------- + +To export a signal, use the decorator ``dbus.service.signal``; to emit +that signal, call the decorated method. The decorated method can also +contain code which will be run when called, as usual. For example:: + + class Example(dbus.service.Object): + def __init__(self, object_path): + dbus.service.Object.__init__(self, dbus.SessionBus(), path) + + @dbus.service.signal(dbus_interface='com.example.Sample', + signature='us') + def NumberOfBottlesChanged(self, number, contents): + print "%d bottles of %s on the wall" % (number, contents) + + e = Example('/bottle-counter') + e.NumberOfBottlesChanged(100, 'beer') + # -> emits com.example.Sample.NumberOfBottlesChanged(100, 'beer') + # and prints "100 bottles of beer on the wall" + +The signal will be queued for sending when the decorated method returns - +you can prevent the signal from being sent by raising an exception +from the decorated method (for instance, if the parameters are +inappropriate). The signal will only actually be sent when the event loop +next runs. + +Example +~~~~~~~ + +``examples/example-signal-emitter.py`` emits some signals on demand when +one of its methods is called. (In reality, you'd emit a signal when some +sort of internal state changed, which may or may not be triggered by a +D-Bus method call.) + +.. -------------------------------------------------------------------- + +License for this document +========================= + +Copyright 2006-2007 `Collabora Ltd.`_ + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +.. + vim:set ft=rst sw=4 sts=4 et tw=72: |