[patch][python] Allow objects to start off unexported

Simon McVittie simon.mcvittie at collabora.co.uk
Mon Jun 11 06:58:15 PDT 2007


-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

- From 7f2e0bfe0c205af512af716f4fcb7b8508269eb7 Mon Sep 17 00:00:00 2001
From: Simon McVittie <simon.mcvittie at collabora.co.uk>
Date: Mon, 11 Jun 2007 14:51:59 +0100
Subject: [PATCH] dbus.service: Allow objects to start off unexported, and become exported later.
Also allow them to be exported on more than one object path or even connection.
dbus.decorators: Allow connection_keyword on signals and methods, so we can
tell which connection to use for any follow-up actions.
- ---
 dbus/decorators.py   |   81 ++++++++++++++-----
 dbus/service.py      |  209 ++++++++++++++++++++++++++++++++++++++++++--------
 test/test-service.py |    8 +-
 3 files changed, 239 insertions(+), 59 deletions(-)

diff --git a/dbus/decorators.py b/dbus/decorators.py
index 9cc0dbe..aab0b77 100644
- --- a/dbus/decorators.py
+++ b/dbus/decorators.py
@@ -34,7 +34,7 @@ from dbus.exceptions import DBusException
 def method(dbus_interface, in_signature=None, out_signature=None,
         async_callbacks=None,
         sender_keyword=None, path_keyword=None, destination_keyword=None,
- -        message_keyword=None,
+        message_keyword=None, connection_keyword=None,
         utf8_strings=False, byte_arrays=False):
     """Factory for decorators used to mark methods of a `dbus.service.Object`
     to be exported on the D-Bus.
@@ -96,6 +96,12 @@ def method(dbus_interface, in_signature=None, out_signature=None,
             the `dbus.lowlevel.MethodCallMessage` as a keyword argument
             with this name.
 
+        `connection_keyword` : str or None
+            If not None (the default), the decorated method will receive
+            the `dbus.connection.Connection` as a keyword argument
+            with this name. This is generally only useful for objects
+            that are available on more than one connection.
+
         `utf8_strings` : bool
             If False (default), D-Bus strings are passed to the decorated
             method as objects of class dbus.String, a unicode subclass.
@@ -138,6 +144,8 @@ def method(dbus_interface, in_signature=None, out_signature=None,
             args.remove(destination_keyword)
         if message_keyword:
             args.remove(message_keyword)
+        if connection_keyword:
+            args.remove(connection_keyword)
 
         if in_signature:
             in_sig = tuple(_dbus_bindings.Signature(in_signature))
@@ -156,6 +164,7 @@ def method(dbus_interface, in_signature=None, out_signature=None,
         func._dbus_path_keyword = path_keyword
         func._dbus_destination_keyword = destination_keyword
         func._dbus_message_keyword = message_keyword
+        func._dbus_connection_keyword = connection_keyword
         func._dbus_args = args
         func._dbus_get_args_options = {'byte_arrays': byte_arrays,
                                        'utf8_strings': utf8_strings}
@@ -164,7 +173,8 @@ def method(dbus_interface, in_signature=None, out_signature=None,
     return decorator
 
 
- -def signal(dbus_interface, signature=None, path_keyword=None):
+def signal(dbus_interface, signature=None, path_keyword=None,
+           connection_keyword=None):
     """Factory for decorators used to mark methods of a `dbus.service.Object`
     to emit signals on the D-Bus.
 
@@ -187,6 +197,19 @@ def signal(dbus_interface, signature=None, path_keyword=None):
             Note that when calling the decorated method, you must always
             pass in the object path as a keyword argument, not as a
             positional argument.
+
+        `connection_keyword` : str or None
+            Similar to `path_keyword`, but this gives the Connection on
+            which the signal should be emitted. If given, and the path_keyword
+            is also given, the signal will be emitted at that path on that
+            connection; if given, but the path_keyword is not, the signal
+            will be emitted from every path at which this object is available
+            on that connection.
+
+            If not given, the signal is emitted on every Connection on which
+            the object is available: if the `path_keyword` is given, it will
+            be emitted at that path on each Connection, otherwise it will be
+            emitted once per (Connection, path) pair.
     """
     _dbus_bindings.validate_interface_name(dbus_interface)
     def decorator(func):
@@ -195,28 +218,42 @@ def signal(dbus_interface, signature=None, path_keyword=None):
 
         def emit_signal(self, *args, **keywords):
             func(self, *args, **keywords)
- -            object_path = self.__dbus_object_path__
+
+            object_path = None
             if path_keyword:
- -                kw = keywords.pop(path_keyword, None)
- -                if kw is not None:
- -                    if not (kw == object_path
- -                            or object_path == '/'
- -                            or kw.startswith(object_path + '/')):
- -                        raise DBusException('Object path %s is not in the '
- -                                            'subtree starting at %s'
- -                                            % (kw, object_path))
- -                    object_path = kw
- -
- -            message = _dbus_bindings.SignalMessage(object_path,
- -                                                   dbus_interface,
- -                                                   member_name)
- -
- -            if signature is not None:
- -                message.append(signature=signature, *args)
+                object_path = keywords.pop(path_keyword, None)
+            connection = None
+            if connection_keyword:
+                connection = keywords.pop(connection_keyword, None)
+
+            if connection is None:
+                if object_path is None:
+                    # any conn, any path
+                    locations = self.locations
+                else:
+                    # any conn, specified path
+                    connections = set()
+                    for location in self.locations:
+                        connections.add(connection)
+                    locations = [(connection, object_path)
+                                 for connection in connections]
+            elif object_path is None:
+                # specified conn, any path
+                locations = [L for L in self.locations if L[0] is connection]
             else:
- -                message.append(*args)
- -
- -            self._connection.send_message(message)
+                # specified conn, specified path
+                locations = ((connection, object_path),)
+
+            for location in locations:
+                message = _dbus_bindings.SignalMessage(location[1],
+                                                       dbus_interface,
+                                                       member_name)
+                if signature is not None:
+                    message.append(signature=signature, *args)
+                else:
+                    message.append(*args)
+
+                location[0].send_message(message)
 
         args = inspect.getargspec(func)[0]
         args.pop(0)
diff --git a/dbus/service.py b/dbus/service.py
index ae50751..b9f1d6a 100644
- --- a/dbus/service.py
+++ b/dbus/service.py
@@ -26,6 +26,10 @@ import sys
 import logging
 import operator
 import traceback
+try:
+    import thread
+except ImportError:
+    import dummy_thread as thread
 
 import _dbus_bindings
 from dbus import SessionBus
@@ -342,6 +346,10 @@ class InterfaceType(type):
 class Interface(object):
     __metaclass__ = InterfaceType
 
+#: A unique object used as the value of Object._object_path and
+#: Object._connection if it's actually in more than one place
+_MANY = object()
+
 class Object(Interface):
     r"""A base class for exporting your own Objects across the Bus.
 
@@ -374,57 +382,171 @@ class Object(Interface):
                 return self._last_input
     """
 
+    #: If True, this object can be made available at more than one object path.
+    #: If True but `SUPPORTS_MULTIPLE_CONNECTIONS` is False, the object may
+    #: handle more than one object path, but they must all be on the same
+    #: connection.
+    SUPPORTS_MULTIPLE_OBJECT_PATHS = False
+
+    #: If True, this object can be made available on more than one connection.
+    #: If True but `SUPPORTS_MULTIPLE_OBJECT_PATHS` is False, the object must
+    #: have the same object path on all its connections.
+    SUPPORTS_MULTIPLE_CONNECTIONS = False
+
     # the signature of __init__ is a bit mad, for backwards compatibility
     def __init__(self, conn=None, object_path=None, bus_name=None):
         """Constructor. Either conn or bus_name is required; object_path
         is also required.
 
         :Parameters:
- -            `conn` : dbus.connection.Connection
+            `conn` : dbus.connection.Connection or None
                 The connection on which to export this object.
 
- -                If None, use the Bus associated with the given ``bus_name``,
- -                or raise TypeError if there is no ``bus_name`` either.
+                If None, use the Bus associated with the given ``bus_name``.
+                If there is no ``bus_name`` either, the object is not
+                initially available on any Connection.
 
                 For backwards compatibility, if an instance of
                 dbus.service.BusName is passed as the first parameter,
                 this is equivalent to passing its associated Bus as
                 ``conn``, and passing the BusName itself as ``bus_name``.
 
- -            `object_path` : str
- -                The D-Bus object path at which to export this Object.
+            `object_path` : str or None
+                A D-Bus object path at which to make this Object available
+                immediately. If this is not None, a `conn` or `bus_name` must
+                also be provided.
 
- -            `bus_name` : dbus.service.BusName
+            `bus_name` : dbus.service.BusName or None
                 Represents a well-known name claimed by this process. A
                 reference to the BusName object will be held by this
                 Object, preventing the name from being released during this
                 Object's lifetime (unless it's released manually).
         """
- -        if object_path is None:
- -            raise TypeError('The object_path argument is required')
- -        _dbus_bindings.validate_object_path(object_path)
- -        if object_path == LOCAL_PATH:
- -            raise DBusException('Objects may not be exported on the reserved '
- -                                'path %s' % LOCAL_PATH)
+        if object_path is not None:
+            _dbus_bindings.validate_object_path(object_path)
 
         if isinstance(conn, BusName):
             # someone's using the old API; don't gratuitously break them
             bus_name = conn
             conn = bus_name.get_bus()
         elif conn is None:
- -            # someone's using the old API but naming arguments, probably
- -            if bus_name is None:
- -                raise TypeError('Either conn or bus_name is required')
- -            conn = bus_name.get_bus()
+            if bus_name is not None:
+                # someone's using the old API but naming arguments, probably
+                conn = bus_name.get_bus()
+
+        #: Either an object path, None or _MANY
+        self._object_path = None
+        #: Either a dbus.connection.Connection, None or _MANY
+        self._connection = None
+        #: A list of tuples (Connection, object path, False) where the False
+        #: is for future expansion (to support fallback paths)
+        self._locations = []
+        #: Lock protecting `_locations`, `_connection` and `_object_path`
+        self._locations_lock = thread.allocate_lock()
 
- -        self._object_path = object_path
         self._name = bus_name
- -        self._connection = conn
 
- -        self._connection._register_object_path(object_path, self._message_cb, self._unregister_cb)
+        if conn is None and object_path is not None:
+            raise TypeError('If object_path is given, either conn or bus_name '
+                            'is required')
+        if conn is not None and object_path is not None:
+            self.add_to_connection(conn, object_path)
+
+    @property
+    def __dbus_object_path__(self):
+        """The object-path at which this object is available.
+        Access raises AttributeError if there is no object path, or more than
+        one object path.
+        """
+        if self._object_path is _MANY:
+            raise AttributeError('Object %r has more than one object path: '
+                                 'use Object.locations instead' % self)
+        elif self._object_path is None:
+            raise AttributeError('Object %r has no object path yet' % self)
+        else:
+            return self._object_path
+
+    @property
+    def connection(self):
+        """The Connection on which this object is available.
+        Access raises AttributeError if there is no Connection, or more than
+        one Connection.
+        """
+        if self._connection is _MANY:
+            raise AttributeError('Object %r is on more than one Connection: '
+                                 'use Object.locations instead' % self)
+        elif self._connection is None:
+            raise AttributeError('Object %r has no Connection yet' % self)
+        else:
+            return self._connection
+
+    @property
+    def locations(self):
+        """An iterable over tuples representing locations at which this
+        object is available.
+
+        Each tuple has at least two items, but may have more in future
+        versions of dbus-python, so do not rely on their exact length.
+        The first two items are the dbus.connection.Connection and the object
+        path.
+        """
+        return iter(self._locations)
+
+    def add_to_connection(self, connection, path):
+        """Make this object accessible via the given D-Bus connection and
+        object path.
+
+        :Parameters:
+            `connection` : dbus.connection.Connection
+                Export the object on this connection. If the class attribute
+                SUPPORTS_MULTIPLE_CONNECTIONS is False (default), this object
+                can only be made available on one connection; if the class
+                attribute is set True by a subclass, the object can be made
+                available on more than one connection.
+
+            `path` : dbus.ObjectPath or other str
+                Place the object at this object path. If the class attribute
+                SUPPORTS_MULTIPLE_OBJECT_PATHS is False (default), this object
+                can only be made available at one object path; if the class
+                attribute is set True by a subclass, the object can be made
+                available with more than one object path.
+        :Raises ValueError: if the object's class attributes do not allow the
+            object to be exported in the desired way.
+        """
+        if path == LOCAL_PATH:
+            raise ValueError('Objects may not be exported on the reserved '
+                             'path %s' % LOCAL_PATH)
 
- -    __dbus_object_path__ = property(lambda self: self._object_path, None, None,
- -                                    "The D-Bus object path of this object")
+        self._locations_lock.acquire()
+        try:
+            if (self._connection is not None and
+                self._connection is not connection and
+                not self.SUPPORTS_MULTIPLE_CONNECTIONS):
+                raise ValueError('%r is already exported on '
+                                 'connection %r' % (self, self._connection))
+
+            if (self._object_path is not None and
+                not self.SUPPORTS_MULTIPLE_OBJECT_PATHS and
+                self._object_path != path):
+                raise ValueError('%r is already exported at object '
+                                 'path %s' % (self, self._object_path))
+
+            connection._register_object_path(path, self._message_cb,
+                                             self._unregister_cb)
+
+            if self._connection is None:
+                self._connection = connection
+            elif self._connection is not connection:
+                self._connection = _MANY
+
+            if self._object_path is None:
+                self._object_path = path
+            elif self._object_path != path:
+                self._object_path = _MANY
+
+            self._locations.append((connection, path, False))
+        finally:
+            self._locations_lock.release()
 
     def remove_from_connection(self, connection=None, path=None):
         """Make this object inaccessible via the given D-Bus connection
@@ -446,21 +568,42 @@ class Object(Interface):
             if the object was not exported on the requested connection
             or path, or (if both are None) was not exported at all.
         """
- -        if self._object_path is None or self._connection is None:
- -            raise LookupError('%r is not exported' % self)
- -        if path is not None and self._object_path != path:
- -            raise LookupError('%r is not exported at path %r' % (self, path))
- -        if connection is not None and self._connection != connection:
- -            raise LookupError('%r is not exported on %r' % (self, connection))
- -
+        self._locations_lock.acquire()
         try:
- -            self._connection._unregister_object_path(self._object_path)
+            if self._object_path is None or self._connection is None:
+                raise LookupError('%r is not exported' % self)
+
+            if connection is not None or path is not None:
+                dropped = []
+                for location in self._locations:
+                    if ((connection is None or location[0] is connection) and
+                        (path is None or location[1] == path)):
+                        dropped.append(location)
+            else:
+                dropped = self._locations
+                self._locations = []
+
+            if not dropped:
+                raise LookupError('%r is not exported at a location matching '
+                                  '(%r,%r)' % (self, connection, path))
+
+            for location in dropped:
+                try:
+                    location[0]._unregister_object_path(location[1])
+                except LookupError:
+                    pass
+                if self._locations:
+                    try:
+                        self._locations.remove(location)
+                    except ValueError:
+                        pass
         finally:
- -            self._connection = None
- -            self._object_path = None
+            self._locations_lock.release()
 
     def _unregister_cb(self, connection):
- -        _logger.info('Unregistering exported object %r', self)
+        # there's not really enough information to do anything useful here
+        _logger.info('Unregistering exported object %r from some path '
+                     'on %r', self, connection)
 
     def _message_cb(self, connection, message):
         try:
@@ -493,6 +636,8 @@ class Object(Interface):
                 keywords[parent_method._dbus_destination_keyword] = message.get_destination()
             if parent_method._dbus_message_keyword:
                 keywords[parent_method._dbus_message_keyword] = message
+            if parent_method._dbus_connection_keyword:
+                keywords[parent_method._dbus_connection_keyword] = connection
 
             # call method
             retval = candidate_method(self, *args, **keywords)
diff --git a/test/test-service.py b/test/test-service.py
index fe24010..4372392 100755
- --- a/test/test-service.py
+++ b/test/test-service.py
@@ -50,9 +50,6 @@ OBJECT = "/org/freedesktop/DBus/TestSuitePythonObject"
 
 class RemovableObject(dbus.service.Object):
     # Part of test for https://bugs.freedesktop.org/show_bug.cgi?id=10457
- -    def __init__(self, bus_name, object_path=OBJECT + '/RemovableObject'):
- -        super(RemovableObject, self).__init__(bus_name, object_path)
- -
     @dbus.service.method(IFACE, in_signature='', out_signature='b')
     def IsThere(self):
         return True
@@ -78,7 +75,7 @@ class TestInterface(dbus.service.Interface):
 class TestObject(dbus.service.Object, TestInterface):
     def __init__(self, bus_name, object_path=OBJECT):
         dbus.service.Object.__init__(self, bus_name, object_path)
- -        self._removables = []
+        self._removable = RemovableObject()
 
     """ Echo whatever is sent
     """
@@ -229,7 +226,8 @@ class TestObject(dbus.service.Object, TestInterface):
     def AddRemovableObject(self):
         # Part of test for https://bugs.freedesktop.org/show_bug.cgi?id=10457
         # Keep the removable object reffed, since that's the use case for this
- -        self._removables.append(RemovableObject(global_name))
+        self._removable.add_to_connection(self._connection,
+                                          OBJECT + '/RemovableObject')
         return True
 
     @dbus.service.method(IFACE, in_signature='', out_signature='b')
- -- 
1.5.2.1

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.6 (GNU/Linux)
Comment: OpenPGP key: http://www.pseudorandom.co.uk/2003/contact/ or pgp.net

iD8DBQFGbVT3WSc8zVUw7HYRAuS/AJ0bvaqZCBfHiMMh9BzgKt42VvHBvACeNGgU
K7Do3icefhd8/ElWHY+++sY=
=q0Zh
-----END PGP SIGNATURE-----


More information about the dbus mailing list