[patch][python] Implement fallback objects (fd.o #9295)

Simon McVittie simon.mcvittie at collabora.co.uk
Tue Jun 19 06:55:52 PDT 2007


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

This reverts some API additions from a previous patch, which I wasn't
entirely happy about supporting long-term.

- From 543ebc088ffbef9a52de333d99361b47301571b3 Mon Sep 17 00:00:00 2001
From: Simon McVittie <simon.mcvittie at collabora.co.uk>
Date: Mon, 18 Jun 2007 16:31:20 +0100
Subject: [PATCH] Implement fallback objects.
In the process, simplify the signal decorator a bit - don't allow the signal
to be emitted from a subset of interfaces (removing connection_keyword),
deprecate path_keyword, disallow path_keyword on objects that support multiple
object paths, and add rel_path_keyword. This is an API removal since previous
patches, but is compatible with the last release.
- ---
 dbus/decorators.py   |  115 +++++++++++++++++++++++++++++++-------------------
 dbus/service.py      |   52 +++++++++++++++++++++--
 test/run-test.sh     |    3 +
 test/test-client.py  |   22 ++++++++-
 test/test-service.py |   39 +++++++++++++++++
 test/test-signals.py |   28 ++++++++++--
 6 files changed, 205 insertions(+), 54 deletions(-)

diff --git a/dbus/decorators.py b/dbus/decorators.py
index aab0b77..892a11f 100644
- --- a/dbus/decorators.py
+++ b/dbus/decorators.py
@@ -174,7 +174,7 @@ def method(dbus_interface, in_signature=None, out_signature=None,
 
 
 def signal(dbus_interface, signature=None, path_keyword=None,
- -           connection_keyword=None):
+           rel_path_keyword=None):
     """Factory for decorators used to mark methods of a `dbus.service.Object`
     to emit signals on the D-Bus.
 
@@ -198,66 +198,95 @@ def signal(dbus_interface, signature=None, path_keyword=None,
             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.
+            This keyword argument cannot be used on objects where
+            the class attribute ``SUPPORTS_MULTIPLE_OBJECT_PATHS`` is true.
+
+            :Deprecated: since 0.82.0. Use `rel_path_keyword` instead.
+
+        `rel_path_keyword` : str or None
+            A keyword argument to the decorated method. If not None,
+            that argument will not be emitted as an argument of
+            the signal.
+
+            When the signal is emitted, if the named keyword argument is given,
+            the signal will appear to come from the object path obtained by
+            appending the keyword argument to the object's object path.
+            This is useful to implement "fallback objects" (objects which
+            own an entire subtree of the object-path tree).
+
+            If the object is available at more than one object-path on the
+            same or different connections, the signal will be emitted at
+            an appropriate object-path on each connection - for instance,
+            if the object is exported at /abc on connection 1 and at
+            /def and /x/y/z on connection 2, and the keyword argument is
+            /foo, then signals will be emitted from /abc/foo and /def/foo
+            on connection 1, and /x/y/z/foo on connection 2.
     """
     _dbus_bindings.validate_interface_name(dbus_interface)
+
+    if path_keyword is not None:
+        from warnings import warn
+        warn(DeprecationWarning('dbus.service.signal::path_keyword has been '
+                                'deprecated since dbus-python 0.82.0, and '
+                                'will not work on objects that support '
+                                'multiple object paths'),
+             DeprecationWarning, stacklevel=2)
+        if rel_path_keyword is not None:
+            raise TypeError('dbus.service.signal::path_keyword and '
+                            'rel_path_keyword cannot both be used')
+
     def decorator(func):
         member_name = func.__name__
         _dbus_bindings.validate_member_name(member_name)
 
         def emit_signal(self, *args, **keywords):
+            abs_path = None
+            if path_keyword is not None:
+                if self.SUPPORTS_MULTIPLE_OBJECT_PATHS:
+                    raise TypeError('path_keyword cannot be used on the '
+                                    'signals of an object that supports '
+                                    'multiple object paths')
+                abs_path = keywords.pop(path_keyword, None)
+                if (abs_path != self.__dbus_object_path__ and
+                    not self.__dbus_object_path__.startswith(abs_path + '/')):
+                    raise ValueError('Path %r is not below %r', abs_path,
+                                     self.__dbus_object_path__)
+
+            rel_path = None
+            if rel_path_keyword is not None:
+                rel_path = keywords.pop(rel_path_keyword, None)
+
             func(self, *args, **keywords)
 
- -            object_path = None
- -            if path_keyword:
- -                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
+            for location in self.locations:
+                if abs_path is None:
+                    # non-deprecated case
+                    if rel_path is None or rel_path in ('/', ''):
+                        object_path = location[1]
+                    else:
+                        # will be validated by SignalMessage ctor in a moment
+                        object_path = location[1] + rel_path
                 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:
- -                # specified conn, specified path
- -                locations = ((connection, object_path),)
- -
- -            for location in locations:
- -                message = _dbus_bindings.SignalMessage(location[1],
+                    object_path = abs_path
+
+                message = _dbus_bindings.SignalMessage(object_path,
                                                        dbus_interface,
                                                        member_name)
- -                if signature is not None:
- -                    message.append(signature=signature, *args)
- -                else:
- -                    message.append(*args)
+                message.append(signature=signature, *args)
 
                 location[0].send_message(message)
+        # end emit_signal
 
         args = inspect.getargspec(func)[0]
         args.pop(0)
 
+        for keyword in rel_path_keyword, path_keyword:
+            if keyword is not None:
+                try:
+                    args.remove(keyword)
+                except ValueError:
+                    raise ValueError('function has no argument "%s"' % keyword)
+
         if signature:
             sig = tuple(_dbus_bindings.Signature(signature))
 
diff --git a/dbus/service.py b/dbus/service.py
index b9f1d6a..f871556 100644
- --- a/dbus/service.py
+++ b/dbus/service.py
@@ -393,7 +393,6 @@ class Object(Interface):
     #: 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.
@@ -444,6 +443,9 @@ class Object(Interface):
         #: Lock protecting `_locations`, `_connection` and `_object_path`
         self._locations_lock = thread.allocate_lock()
 
+        #: True if this is a fallback object handling a whole subtree.
+        self._fallback = False
+
         self._name = bus_name
 
         if conn is None and object_path is not None:
@@ -532,7 +534,8 @@ class Object(Interface):
                                  'path %s' % (self, self._object_path))
 
             connection._register_object_path(path, self._message_cb,
- -                                             self._unregister_cb)
+                                             self._unregister_cb,
+                                             self._fallback)
 
             if self._connection is None:
                 self._connection = connection
@@ -544,7 +547,7 @@ class Object(Interface):
             elif self._object_path != path:
                 self._object_path = _MANY
 
- -            self._locations.append((connection, path, False))
+            self._locations.append((connection, path, self._fallback))
         finally:
             self._locations_lock.release()
 
@@ -717,6 +720,47 @@ class Object(Interface):
         return reflection_data
 
     def __repr__(self):
- -        return '<dbus.service.Object %s on %r at %#x>' % (self._object_path, self._name, id(self))
+        where = ''
+        if (self._object_path is not _MANY
+            and self._object_path is not None):
+            where = ' at %s' % self._object_path
+        return '<%s.%s%s at %#x>' % (self.__class__.__module__,
+                                   self.__class__.__name__, where,
+                                   id(self))
     __str__ = __repr__
 
+class FallbackObject(Object):
+    """An object that implements an entire subtree of the object-path
+    tree."""
+
+    SUPPORTS_MULTIPLE_OBJECT_PATHS = True
+
+    def __init__(self, conn=None, object_path=None):
+        """Constructor.
+
+        Note that the superclass' ``bus_name`` __init__ argument is not
+        supported here.
+
+        :Parameters:
+            `conn` : dbus.connection.Connection or None
+                The connection on which to export this object.
+
+                If None, the object is not initially available on any
+                Connection.
+
+            `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.
+
+                This object will implements all object-paths in the subtree
+                starting at this object-path, except where a more specific
+                object has been added.
+        """
+        super(FallbackObject, self).__init__()
+        self._fallback = True
+
+        if conn is None and object_path is not None:
+            raise TypeError('If object_path is given, conn is required')
+        if conn is not None and object_path is not None:
+            self.add_to_connection(conn, object_path)
diff --git a/test/run-test.sh b/test/run-test.sh
index 13992c9..94da991 100755
- --- a/test/run-test.sh
+++ b/test/run-test.sh
@@ -37,6 +37,8 @@ if test -z "$DBUS_TEST_PYTHON_IN_RUN_TEST"; then
   exec "$DBUS_TOP_SRCDIR"/test/run-with-tmp-session-bus.sh $SCRIPTNAME
 fi  
 
+dbus-monitor > "$DBUS_TOP_BUILDDIR"/test/monitor.log &
+
 echo "running test-standalone.py"
 $PYTHON "$DBUS_TOP_SRCDIR"/test/test-standalone.py || die "test-standalone.py failed"
 
@@ -95,4 +97,5 @@ $PYTHON "$DBUS_TOP_SRCDIR"/test/test-p2p.py || die "... failed"
 rm -f "$DBUS_TOP_BUILDDIR"/test/test-service.log
 rm -f "$DBUS_TOP_BUILDDIR"/test/cross-client.log
 rm -f "$DBUS_TOP_BUILDDIR"/test/cross-server.log
+rm -f "$DBUS_TOP_BUILDDIR"/test/monitor.log
 exit 0
diff --git a/test/test-client.py b/test/test-client.py
index 7616d3c..f90dcce 100755
- --- a/test/test-client.py
+++ b/test/test-client.py
@@ -93,13 +93,13 @@ class TestDBusBindings(unittest.TestCase):
 
     def testInterfaceKeyword(self):
         #test dbus_interface parameter
- -        print self.remote_object.Echo("dbus_interface on Proxy test Passed", dbus_interface = "org.freedesktop.DBus.TestSuiteInterface")
- -        print self.iface.Echo("dbus_interface on Interface test Passed", dbus_interface = "org.freedesktop.DBus.TestSuiteInterface")
+        print self.remote_object.Echo("dbus_interface on Proxy test Passed", dbus_interface = IFACE)
+        print self.iface.Echo("dbus_interface on Interface test Passed", dbus_interface = IFACE)
         self.assert_(True)
 
     def testGetDBusMethod(self):
         self.assertEquals(self.iface.get_dbus_method('AcceptListOfByte')('\1\2\3'), [1,2,3])
- -        self.assertEquals(self.remote_object.get_dbus_method('AcceptListOfByte', dbus_interface='org.freedesktop.DBus.TestSuiteInterface')('\1\2\3'), [1,2,3])
+        self.assertEquals(self.remote_object.get_dbus_method('AcceptListOfByte', dbus_interface=IFACE)('\1\2\3'), [1,2,3])
 
     def testCallingConventionOptions(self):
         self.assertEquals(self.iface.AcceptListOfByte('\1\2\3'), [1,2,3])
@@ -408,6 +408,22 @@ class TestDBusBindings(unittest.TestCase):
         self.assert_(iface.RemoveSelf())
         self.assert_(not self.iface.HasRemovableObject())
 
+    def testFallbackObjectTrivial(self):
+        obj = self.bus.get_object(NAME, OBJECT + '/Fallback')
+        iface = dbus.Interface(obj, IFACE)
+        path, unique_name = iface.TestPathAndConnKeywords()
+        self.assertEquals(path, OBJECT + '/Fallback')
+        #self.assertEquals(rel, '/Badger/Mushroom')
+        self.assertEquals(unique_name, obj.bus_name)
+
+    def testFallbackObject(self):
+        obj = self.bus.get_object(NAME, OBJECT + '/Fallback/Badger/Mushroom')
+        iface = dbus.Interface(obj, IFACE)
+        path, unique_name = iface.TestPathAndConnKeywords()
+        self.assertEquals(path, OBJECT + '/Fallback/Badger/Mushroom')
+        #self.assertEquals(rel, '/Badger/Mushroom')
+        self.assertEquals(unique_name, obj.bus_name)
+
 """ Remove this for now
 class TestDBusPythonToGLibBindings(unittest.TestCase):
     def setUp(self):
diff --git a/test/test-service.py b/test/test-service.py
index 4372392..dcd511e 100755
- --- a/test/test-service.py
+++ b/test/test-service.py
@@ -72,6 +72,44 @@ class TestInterface(dbus.service.Interface):
     def CheckInheritance(self):
         return False
 
+class Fallback(dbus.service.FallbackObject):
+    def __init__(self, bus_name, object_path=OBJECT + '/Fallback'):
+        super(Fallback, self).__init__(bus_name, object_path)
+
+    @dbus.service.method(IFACE, in_signature='', out_signature='os',
+                         path_keyword='path', # rel_path_keyword='rel',
+                         connection_keyword='conn')
+    def TestPathAndConnKeywords(self, path=None, conn=None):
+        return path, conn.get_unique_name()
+
+    @dbus.service.signal(IFACE, signature='s', rel_path_keyword='rel_path')
+    def SignalOneString(self, test, rel_path=None):
+        logger.info('SignalOneString(%r) @ %r', test, rel_path)
+
+    # Deprecated
+    @dbus.service.signal(IFACE, signature='ss', path_keyword='path')
+    def SignalTwoStrings(self, test, test2, path=None):
+        logger.info('SignalTwoStrings(%r, %r) @ %r', test, test2, path)
+
+    @dbus.service.method(IFACE, in_signature='su', out_signature='',
+                         path_keyword='path')
+    def EmitSignal(self, signal, value, path=None):
+        sig = getattr(self, str(signal), None)
+        assert sig is not None
+
+        assert path.startswith(OBJECT + '/Fallback')
+        rel_path = path[len(OBJECT + '/Fallback'):]
+        if rel_path == '':
+            rel_path = '/'
+
+        if signal == 'SignalOneString':
+            logger.info('Emitting %s from rel %r', signal, rel_path)
+            sig('I am a fallback', rel_path=rel_path)
+        else:
+            val = ('I am', 'a fallback')
+            logger.info('Emitting %s from abs %r', signal, path)
+            sig('I am', 'a deprecated fallback', path=path)
+
 class TestObject(dbus.service.Object, TestInterface):
     def __init__(self, bus_name, object_path=OBJECT):
         dbus.service.Object.__init__(self, bus_name, object_path)
@@ -245,5 +283,6 @@ session_bus = dbus.SessionBus()
 global_name = dbus.service.BusName(NAME, bus=session_bus)
 object = TestObject(global_name)
 g_object = TestGObject(global_name)
+fallback_object = Fallback(session_bus)
 loop = gobject.MainLoop()
 loop.run()
diff --git a/test/test-signals.py b/test/test-signals.py
index 797f70c..22b4b4b 100644
- --- a/test/test-signals.py
+++ b/test/test-signals.py
@@ -48,14 +48,28 @@ if not pkg.startswith(pydir):
 if not _dbus_bindings.__file__.startswith(builddir):
     raise Exception("DBus modules (%s) are not being picked up from the package"%_dbus_bindings.__file__)
 
+
+NAME = "org.freedesktop.DBus.TestSuitePythonService"
+IFACE = "org.freedesktop.DBus.TestSuiteInterface"
+OBJECT = "/org/freedesktop/DBus/TestSuitePythonObject"
+
+
 class TestSignals(unittest.TestCase):
     def setUp(self):
         logger.info('setUp()')
         self.bus = dbus.SessionBus()
- -        self.remote_object = self.bus.get_object("org.freedesktop.DBus.TestSuitePythonService", "/org/freedesktop/DBus/TestSuitePythonObject")
- -        self.remote_object_follow = self.bus.get_object("org.freedesktop.DBus.TestSuitePythonService", "/org/freedesktop/DBus/TestSuitePythonObject", follow_name_owner_changes=True)
- -        self.iface = dbus.Interface(self.remote_object, "org.freedesktop.DBus.TestSuiteInterface")
- -        self.iface_follow = dbus.Interface(self.remote_object_follow, "org.freedesktop.DBus.TestSuiteInterface")
+        self.remote_object = self.bus.get_object(NAME, OBJECT)
+        self.remote_object_fallback_trivial = self.bus.get_object(NAME,
+                OBJECT + '/Fallback')
+        self.remote_object_fallback = self.bus.get_object(NAME,
+                OBJECT + '/Fallback/Badger')
+        self.remote_object_follow = self.bus.get_object(NAME, OBJECT,
+                follow_name_owner_changes=True)
+        self.iface = dbus.Interface(self.remote_object, IFACE)
+        self.iface_follow = dbus.Interface(self.remote_object_follow, IFACE)
+        self.fallback_iface = dbus.Interface(self.remote_object_fallback, IFACE)
+        self.fallback_trivial_iface = dbus.Interface(
+                self.remote_object_fallback_trivial, IFACE)
         self.in_test = None
 
     def signal_test_impl(self, iface, name, test_removal=False):
@@ -104,6 +118,12 @@ class TestSignals(unittest.TestCase):
                 raise AssertionError('Signal should not have arrived, but did')
             gobject.source_remove(source_id)
 
+    def testFallback(self):
+        self.signal_test_impl(self.fallback_iface, 'Fallback')
+
+    def testFallbackTrivial(self):
+        self.signal_test_impl(self.fallback_trivial_iface, 'FallbackTrivial')
+
     def testSignal(self):
         self.signal_test_impl(self.iface, 'Signal')
 
- -- 
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

iD8DBQFGd+BoWSc8zVUw7HYRAtwNAKC3Y51mQzyLfrAhIcuW4HPwUz89SACgwB++
qJjA2qahVc3BJONIU5RFNxU=
=1KvF
-----END PGP SIGNATURE-----


More information about the dbus mailing list