[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