From 486bf991e87a3a09defab01e550e6453b30482bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Gajda?= Date: Thu, 16 Jan 2020 15:52:43 +0100 Subject: [PATCH 1/5] Add representation to Variant --- dbus_next/signature.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dbus_next/signature.py b/dbus_next/signature.py index c3b96f9..f838af4 100644 --- a/dbus_next/signature.py +++ b/dbus_next/signature.py @@ -386,3 +386,6 @@ def __eq__(self, other): return self.signature == other.signature and self.value == other.value else: return super().__eq__(other) + + def __repr__(self): + return "" % (self.type.signature, self.value) From d0587631007d27a90c8616bb58a3942016a68c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Gajda?= Date: Thu, 16 Jan 2020 15:55:19 +0100 Subject: [PATCH 2/5] Rework test for signal --- test/service/test_signals.py | 127 +++++++++++++++++++++++++---------- 1 file changed, 90 insertions(+), 37 deletions(-) diff --git a/test/service/test_signals.py b/test/service/test_signals.py index e066453..52d5dd7 100644 --- a/test/service/test_signals.py +++ b/test/service/test_signals.py @@ -1,6 +1,9 @@ -from dbus_next.service import ServiceInterface, signal, SignalDisabledError +from dbus_next.service import ServiceInterface, signal, SignalDisabledError, dbus_property from dbus_next.aio import MessageBus from dbus_next import Message, MessageType +from dbus_next.constants import PropertyAccess +from dbus_next.signature import Variant + import pytest import asyncio @@ -33,6 +36,45 @@ def signal_disabled(self): assert type(self) is ExampleInterface +class ExpectMessage: + def __init__(self, bus1, bus2, timeout=1): + self.future = asyncio.get_event_loop().create_future() + self.bus1 = bus1 + self.bus2 = bus2 + self.timeout = timeout + self.timeout_task = None + + def message_handler(self, msg): + self.timeout_task.cancel() + self.bus2.remove_message_handler(self.message_handler) + self.future.set_result(msg) + return True + + def timeout_cb(self): + self.bus2.remove_message_handler(self.message_handler) + self.future.set_result(TimeoutError()) + + async def __aenter__(self): + self.bus2.add_message_handler(self.message_handler) + self.timeout_task = asyncio.get_event_loop().call_later(self.timeout, self.timeout_cb) + + return self.future + + async def __aexit__(self, exc_type, exc_val, exc_tb): + pass + + +def assert_signal_ok(signal, interface_name, export_path, unique_name, member, signature, body): + assert not isinstance(signal, TimeoutError) + assert signal.message_type == MessageType.SIGNAL + assert signal.interface == interface_name + assert signal.path == export_path + assert signal.sender == unique_name + assert signal.member == member + assert signal.signature == signature + assert signal.body == body + + @pytest.mark.asyncio async def test_signals(): bus1 = await MessageBus().connect() @@ -50,42 +92,53 @@ async def test_signals(): signature='s', body=[f'sender={bus1.unique_name}'])) - async def wait_for_message(): - # TODO timeout - future = asyncio.get_event_loop().create_future() - - def message_handler(signal): - if signal.sender == bus1.unique_name and signal.interface == interface.name: - bus1.remove_message_handler(message_handler) - future.set_result(signal) - - bus2.add_message_handler(message_handler) - return await future - - def assert_signal_ok(signal, member, signature, body): - assert signal.message_type == MessageType.SIGNAL, signal.body[0] - assert signal.interface == interface.name - assert signal.path == export_path - assert signal.sender == bus1.unique_name - assert signal.member == member - assert signal.signature == signature - assert signal.body == body - - interface.signal_empty() - signal = await wait_for_message() - assert_signal_ok(signal, 'signal_empty', '', []) - - interface.original_name() - signal = await wait_for_message() - assert_signal_ok(signal, 'renamed', '', []) - - interface.signal_simple() - signal = await wait_for_message() - assert_signal_ok(signal, 'signal_simple', 's', ['hello']) - - interface.signal_multiple() - signal = await wait_for_message() - assert_signal_ok(signal, 'signal_multiple', 'ss', ['hello', 'world']) + async with ExpectMessage(bus1, bus2) as expected_signal: + interface.signal_empty() + assert_signal_ok( + signal=await expected_signal, + interface_name=interface.name, + export_path=export_path, + unique_name=bus1.unique_name, + member='signal_empty', + signature='', + body=[] + ) + + async with ExpectMessage(bus1, bus2) as expected_signal: + interface.original_name() + assert_signal_ok( + signal=await expected_signal, + interface_name=interface.name, + export_path=export_path, + unique_name=bus1.unique_name, + member='renamed', + signature='', + body=[] + ) + + async with ExpectMessage(bus1, bus2) as expected_signal: + interface.signal_simple() + assert_signal_ok( + signal=await expected_signal, + interface_name=interface.name, + export_path=export_path, + unique_name=bus1.unique_name, + member='signal_simple', + signature='s', + body=['hello'] + ) + + async with ExpectMessage(bus1, bus2) as expected_signal: + interface.signal_multiple() + assert_signal_ok( + signal=await expected_signal, + interface_name=interface.name, + export_path=export_path, + unique_name=bus1.unique_name, + member='signal_multiple', + signature='ss', + body=['hello', 'world'] + ) with pytest.raises(SignalDisabledError): interface.signal_disabled() From 4c7e31cf42704f2bc510d45fd81e5fe2af45ef5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Gajda?= Date: Fri, 17 Jan 2020 16:06:48 +0100 Subject: [PATCH 3/5] InterfacesAdded and InterfacesRemoved signals with test --- dbus_next/message_bus.py | 48 +++++++++++++++++++ test/service/test_signals.py | 92 ++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/dbus_next/message_bus.py b/dbus_next/message_bus.py index fbba4d0..12467b3 100644 --- a/dbus_next/message_bus.py +++ b/dbus_next/message_bus.py @@ -99,6 +99,7 @@ def export(self, path: str, interface: ServiceInterface): self._path_exports[path].append(interface) ServiceInterface._add_bus(interface, self) + self.emit_interface_added(path, interface) def unexport(self, path: str, interface: Optional[Union[ServiceInterface, str]] = None): """Unexport the path or service interface to make it no longer @@ -129,19 +130,23 @@ def unexport(self, path: str, interface: Optional[Union[ServiceInterface, str]] except StopIteration: return + removed_interfaces = [] if interface is None: del self._path_exports[path] for iface in filter(lambda e: not self._has_interface(e), exports): + removed_interfaces.append(iface.name) ServiceInterface._remove_bus(iface, self) else: for i, iface in enumerate(exports): if iface is interface: + removed_interfaces.append(iface.name) del self._path_exports[path][i] if not self._path_exports[path]: del self._path_exports[path] if not self._has_interface(iface): ServiceInterface._remove_bus(iface, self) break + self.emit_interface_removed(path, removed_interfaces) def introspect(self, bus_name: str, path: str, callback: Callable[[Optional[intr.Node], Optional[Exception]], None]): @@ -181,6 +186,49 @@ def reply_notify(reply, err): interface='org.freedesktop.DBus.Introspectable', member='Introspect'), reply_notify) + def emit_interface_added(self, path, interface): + """Emit the ``org.freedesktop.DBus.ObjectManager.InterfacesAdded`` signal. + + This signal is intended to be used to alert clients when + a new interface has been added. + + :param path: Path of exported object. + :type path: str + :param interface: Exported service interface. + :type interface: :class:`ServiceInterface + ` + """ + body = { + interface.name: { + prop.name: Variant(prop.signature, prop.prop_getter(interface)) + for prop in interface._get_properties(interface)} + } + + self.send( + Message.new_signal(path=path, + interface='org.freedesktop.DBus.ObjectManager', + member='InterfacesAdded', + signature='oa{sa{sv}}', + body=[path, body])) + + def emit_interface_removed(self, path, removed_interfaces): + """Emit the ``org.freedesktop.DBus.ObjectManager.InterfacesRemoved` signal. + + This signal is intended to be used to alert clients when + a interface has been removed. + + :param path: Path of removed (unexported) object. + :type path: str + :param removed_interfaces: List of unexported service interfaces. + :type removed_interfaces: list[str] + """ + self.send( + Message.new_signal(path=path, + interface='org.freedesktop.DBus.ObjectManager', + member='InterfacesRemoved', + signature='oas', + body=[path, removed_interfaces])) + def request_name(self, name: str, flags: NameFlag = NameFlag.NONE, diff --git a/test/service/test_signals.py b/test/service/test_signals.py index 52d5dd7..c414ee6 100644 --- a/test/service/test_signals.py +++ b/test/service/test_signals.py @@ -35,6 +35,23 @@ def original_name(self): def signal_disabled(self): assert type(self) is ExampleInterface + @dbus_property(access=PropertyAccess.READ) + def test_prop(self) -> 'i': + return 42 + + +class SecondExampleInterface(ServiceInterface): + def __init__(self, name): + super().__init__(name) + + @dbus_property(access=PropertyAccess.READ) + def str_prop(self) -> 's': + return "abc" + + @dbus_property(access=PropertyAccess.READ) + def list_prop(self) -> 'ai': + return [1, 2, 3] + class ExpectMessage: def __init__(self, bus1, bus2, timeout=1): @@ -142,3 +159,78 @@ async def test_signals(): with pytest.raises(SignalDisabledError): interface.signal_disabled() + + +@pytest.mark.asyncio +async def test_interface_add_remove_signal(): + bus1 = await MessageBus().connect() + bus2 = await MessageBus().connect() + + await bus2.call( + Message(destination='org.freedesktop.DBus', + path='/org/freedesktop/DBus', + interface='org.freedesktop.DBus', + member='AddMatch', + signature='s', + body=[f'sender={bus1.unique_name}'])) + + first_interface = ExampleInterface('test.interface.first') + second_interface = SecondExampleInterface('test.interface.second') + export_path = '/test/path' + + # add first interface + async with ExpectMessage(bus1, bus2) as expected_signal: + bus1.export(export_path, first_interface) + assert_signal_ok( + signal=await expected_signal, + interface_name='org.freedesktop.DBus.ObjectManager', + export_path=export_path, + unique_name=bus1.unique_name, + member='InterfacesAdded', + signature='oa{sa{sv}}', + body=[export_path, {'test.interface.first': {'test_prop': Variant('i', 42)}}] + ) + + # add second interface + async with ExpectMessage(bus1, bus2) as expected_signal: + bus1.export(export_path, second_interface) + assert_signal_ok( + signal=await expected_signal, + interface_name='org.freedesktop.DBus.ObjectManager', + export_path=export_path, + unique_name=bus1.unique_name, + member='InterfacesAdded', + signature='oa{sa{sv}}', + body=[export_path, + {'test.interface.second': {'str_prop': Variant('s', "abc"), 'list_prop': Variant('ai', [1, 2, 3])}}] + ) + + # remove single interface + async with ExpectMessage(bus1, bus2) as expected_signal: + bus1.unexport(export_path, second_interface) + assert_signal_ok( + signal=await expected_signal, + interface_name='org.freedesktop.DBus.ObjectManager', + export_path=export_path, + unique_name=bus1.unique_name, + member='InterfacesRemoved', + signature='oas', + body=[export_path, ['test.interface.second']] + ) + + # add second interface again + async with ExpectMessage(bus1, bus2): + bus1.export(export_path, second_interface) + + # remove multiple interfaces + async with ExpectMessage(bus1, bus2) as expected_signal: + bus1.unexport(export_path) + assert_signal_ok( + signal=await expected_signal, + interface_name='org.freedesktop.DBus.ObjectManager', + export_path=export_path, + unique_name=bus1.unique_name, + member='InterfacesRemoved', + signature='oas', + body=[export_path, ['test.interface.first', 'test.interface.second']] + ) From 95a911473fa56aaa1e5e6ca68d6008f54b378513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Gajda?= Date: Mon, 20 Jan 2020 13:23:20 +0100 Subject: [PATCH 4/5] Rework after review --- dbus_next/message_bus.py | 8 +++--- test/service/test_signals.py | 56 ++++++++++++------------------------ 2 files changed, 23 insertions(+), 41 deletions(-) diff --git a/dbus_next/message_bus.py b/dbus_next/message_bus.py index 12467b3..a2446bf 100644 --- a/dbus_next/message_bus.py +++ b/dbus_next/message_bus.py @@ -99,7 +99,7 @@ def export(self, path: str, interface: ServiceInterface): self._path_exports[path].append(interface) ServiceInterface._add_bus(interface, self) - self.emit_interface_added(path, interface) + self._emit_interface_added(path, interface) def unexport(self, path: str, interface: Optional[Union[ServiceInterface, str]] = None): """Unexport the path or service interface to make it no longer @@ -146,7 +146,7 @@ def unexport(self, path: str, interface: Optional[Union[ServiceInterface, str]] if not self._has_interface(iface): ServiceInterface._remove_bus(iface, self) break - self.emit_interface_removed(path, removed_interfaces) + self._emit_interface_removed(path, removed_interfaces) def introspect(self, bus_name: str, path: str, callback: Callable[[Optional[intr.Node], Optional[Exception]], None]): @@ -186,7 +186,7 @@ def reply_notify(reply, err): interface='org.freedesktop.DBus.Introspectable', member='Introspect'), reply_notify) - def emit_interface_added(self, path, interface): + def _emit_interface_added(self, path, interface): """Emit the ``org.freedesktop.DBus.ObjectManager.InterfacesAdded`` signal. This signal is intended to be used to alert clients when @@ -211,7 +211,7 @@ def emit_interface_added(self, path, interface): signature='oa{sa{sv}}', body=[path, body])) - def emit_interface_removed(self, path, removed_interfaces): + def _emit_interface_removed(self, path, removed_interfaces): """Emit the ``org.freedesktop.DBus.ObjectManager.InterfacesRemoved` signal. This signal is intended to be used to alert clients when diff --git a/test/service/test_signals.py b/test/service/test_signals.py index c414ee6..d468a56 100644 --- a/test/service/test_signals.py +++ b/test/service/test_signals.py @@ -54,22 +54,22 @@ def list_prop(self) -> 'ai': class ExpectMessage: - def __init__(self, bus1, bus2, timeout=1): + def __init__(self, bus1, bus2, interface_name, timeout=1): self.future = asyncio.get_event_loop().create_future() self.bus1 = bus1 self.bus2 = bus2 + self.interface_name = interface_name self.timeout = timeout self.timeout_task = None def message_handler(self, msg): - self.timeout_task.cancel() - self.bus2.remove_message_handler(self.message_handler) - self.future.set_result(msg) - return True + if msg.sender == self.bus1.unique_name and msg.interface == self.interface_name: + self.timeout_task.cancel() + self.future.set_result(msg) + return True def timeout_cb(self): - self.bus2.remove_message_handler(self.message_handler) - self.future.set_result(TimeoutError()) + self.future.set_exception(TimeoutError) async def __aenter__(self): self.bus2.add_message_handler(self.message_handler) @@ -78,15 +78,12 @@ async def __aenter__(self): return self.future async def __aexit__(self, exc_type, exc_val, exc_tb): - pass + self.bus2.remove_message_handler(self.message_handler) -def assert_signal_ok(signal, interface_name, export_path, unique_name, member, signature, body): - assert not isinstance(signal, TimeoutError) +def assert_signal_ok(signal, export_path, member, signature, body): assert signal.message_type == MessageType.SIGNAL - assert signal.interface == interface_name assert signal.path == export_path - assert signal.sender == unique_name assert signal.member == member assert signal.signature == signature assert signal.body == body @@ -109,49 +106,41 @@ async def test_signals(): signature='s', body=[f'sender={bus1.unique_name}'])) - async with ExpectMessage(bus1, bus2) as expected_signal: + async with ExpectMessage(bus1, bus2, interface.name) as expected_signal: interface.signal_empty() assert_signal_ok( signal=await expected_signal, - interface_name=interface.name, export_path=export_path, - unique_name=bus1.unique_name, member='signal_empty', signature='', body=[] ) - async with ExpectMessage(bus1, bus2) as expected_signal: + async with ExpectMessage(bus1, bus2, interface.name) as expected_signal: interface.original_name() assert_signal_ok( signal=await expected_signal, - interface_name=interface.name, export_path=export_path, - unique_name=bus1.unique_name, member='renamed', signature='', body=[] ) - async with ExpectMessage(bus1, bus2) as expected_signal: + async with ExpectMessage(bus1, bus2, interface.name) as expected_signal: interface.signal_simple() assert_signal_ok( signal=await expected_signal, - interface_name=interface.name, export_path=export_path, - unique_name=bus1.unique_name, member='signal_simple', signature='s', body=['hello'] ) - async with ExpectMessage(bus1, bus2) as expected_signal: + async with ExpectMessage(bus1, bus2, interface.name) as expected_signal: interface.signal_multiple() assert_signal_ok( signal=await expected_signal, - interface_name=interface.name, export_path=export_path, - unique_name=bus1.unique_name, member='signal_multiple', signature='ss', body=['hello', 'world'] @@ -179,26 +168,22 @@ async def test_interface_add_remove_signal(): export_path = '/test/path' # add first interface - async with ExpectMessage(bus1, bus2) as expected_signal: + async with ExpectMessage(bus1, bus2, 'org.freedesktop.DBus.ObjectManager') as expected_signal: bus1.export(export_path, first_interface) assert_signal_ok( signal=await expected_signal, - interface_name='org.freedesktop.DBus.ObjectManager', export_path=export_path, - unique_name=bus1.unique_name, member='InterfacesAdded', signature='oa{sa{sv}}', body=[export_path, {'test.interface.first': {'test_prop': Variant('i', 42)}}] ) # add second interface - async with ExpectMessage(bus1, bus2) as expected_signal: + async with ExpectMessage(bus1, bus2, 'org.freedesktop.DBus.ObjectManager') as expected_signal: bus1.export(export_path, second_interface) assert_signal_ok( signal=await expected_signal, - interface_name='org.freedesktop.DBus.ObjectManager', export_path=export_path, - unique_name=bus1.unique_name, member='InterfacesAdded', signature='oa{sa{sv}}', body=[export_path, @@ -206,30 +191,27 @@ async def test_interface_add_remove_signal(): ) # remove single interface - async with ExpectMessage(bus1, bus2) as expected_signal: + async with ExpectMessage(bus1, bus2, 'org.freedesktop.DBus.ObjectManager') as expected_signal: bus1.unexport(export_path, second_interface) assert_signal_ok( signal=await expected_signal, - interface_name='org.freedesktop.DBus.ObjectManager', export_path=export_path, - unique_name=bus1.unique_name, member='InterfacesRemoved', signature='oas', body=[export_path, ['test.interface.second']] ) # add second interface again - async with ExpectMessage(bus1, bus2): + async with ExpectMessage(bus1, bus2, 'org.freedesktop.DBus.ObjectManager') as expected_signal: bus1.export(export_path, second_interface) + await expected_signal # remove multiple interfaces - async with ExpectMessage(bus1, bus2) as expected_signal: + async with ExpectMessage(bus1, bus2, 'org.freedesktop.DBus.ObjectManager') as expected_signal: bus1.unexport(export_path) assert_signal_ok( signal=await expected_signal, - interface_name='org.freedesktop.DBus.ObjectManager', export_path=export_path, - unique_name=bus1.unique_name, member='InterfacesRemoved', signature='oas', body=[export_path, ['test.interface.first', 'test.interface.second']] From 91ec51ff31fa800fac34d21c4459c8f87e9502c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Gajda?= Date: Wed, 22 Jan 2020 15:43:38 +0100 Subject: [PATCH 5/5] Fix error handling in InterfacesAdded signal --- dbus_next/message_bus.py | 12 +++++++----- test/client/test_signals.py | 2 ++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/dbus_next/message_bus.py b/dbus_next/message_bus.py index a2446bf..0cfe5dd 100644 --- a/dbus_next/message_bus.py +++ b/dbus_next/message_bus.py @@ -7,6 +7,7 @@ from .signature import Variant from .proxy_object import BaseProxyObject from . import introspection as intr +from contextlib import suppress import inspect import traceback @@ -198,11 +199,12 @@ def _emit_interface_added(self, path, interface): :type interface: :class:`ServiceInterface ` """ - body = { - interface.name: { - prop.name: Variant(prop.signature, prop.prop_getter(interface)) - for prop in interface._get_properties(interface)} - } + body = {interface.name: {}} + properties = interface._get_properties(interface) + + for prop in properties: + with suppress(Exception): + body[interface.name][prop.name] = Variant(prop.signature, prop.prop_getter(interface)) self.send( Message.new_signal(path=path, diff --git a/test/client/test_signals.py b/test/client/test_signals.py index 2d1d24d..b47c226 100644 --- a/test/client/test_signals.py +++ b/test/client/test_signals.py @@ -63,6 +63,8 @@ def multiple_handler(value1, value2): except Exception as e: err = e + await ping() + interface.on_some_signal(single_handler) interface.on_signal_multiple(multiple_handler)