From 887fe663e0fa969967cc8125d8589cdcbdc8357c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Lowas-Rzechonek?= Date: Sat, 28 Dec 2019 21:09:07 +0100 Subject: [PATCH 1/3] Run formatter --- dbus_next/aio/message_bus.py | 1 - dbus_next/aio/proxy_object.py | 2 -- dbus_next/glib/message_bus.py | 1 - dbus_next/glib/proxy_object.py | 2 -- dbus_next/introspection.py | 10 +++------- dbus_next/message.py | 1 - dbus_next/message_bus.py | 9 ++++----- dbus_next/proxy_object.py | 2 -- dbus_next/service.py | 1 - dbus_next/signature.py | 2 -- 10 files changed, 7 insertions(+), 24 deletions(-) diff --git a/dbus_next/aio/message_bus.py b/dbus_next/aio/message_bus.py index 8193a68..b57b63e 100644 --- a/dbus_next/aio/message_bus.py +++ b/dbus_next/aio/message_bus.py @@ -33,7 +33,6 @@ class MessageBus(BaseMessageBus): be :class:`None` until the message bus connects. :vartype unique_name: str """ - def __init__(self, bus_address: str = None, bus_type: BusType = BusType.SESSION): super().__init__(bus_address, bus_type, ProxyObject) self._loop = asyncio.get_event_loop() diff --git a/dbus_next/aio/proxy_object.py b/dbus_next/aio/proxy_object.py index 0a22447..21a8f3a 100644 --- a/dbus_next/aio/proxy_object.py +++ b/dbus_next/aio/proxy_object.py @@ -65,7 +65,6 @@ class ProxyInterface(BaseProxyInterface): If the service returns an error for a DBus call, a :class:`DBusError ` will be raised with information about the error. """ - def _add_method(self, intr_method): async def method_fn(*args): msg = await self.bus.call( @@ -128,7 +127,6 @@ class ProxyObject(BaseProxyObject): For more information, see the :class:`BaseProxyObject `. """ - def __init__(self, bus_name: str, path: str, introspection: Union[intr.Node, str, ET.Element], bus: BaseMessageBus): super().__init__(bus_name, path, introspection, bus, ProxyInterface) diff --git a/dbus_next/glib/message_bus.py b/dbus_next/glib/message_bus.py index 1c94cb0..c1a95f2 100644 --- a/dbus_next/glib/message_bus.py +++ b/dbus_next/glib/message_bus.py @@ -140,7 +140,6 @@ class MessageBus(BaseMessageBus): be :class:`None` until the message bus connects. :vartype unique_name: str """ - def __init__(self, bus_address: str = None, bus_type: BusType = BusType.SESSION): if _import_error: raise _import_error diff --git a/dbus_next/glib/proxy_object.py b/dbus_next/glib/proxy_object.py index 5cecd60..1ce47d2 100644 --- a/dbus_next/glib/proxy_object.py +++ b/dbus_next/glib/proxy_object.py @@ -102,7 +102,6 @@ def set_callback(error: Exception) :class:`DBusError ` will be raised with information about the error. """ - def _add_method(self, intr_method): in_len = len(intr_method.in_args) out_len = len(intr_method.out_args) @@ -270,7 +269,6 @@ class ProxyObject(BaseProxyObject): For more information, see the :class:`BaseProxyObject `. """ - def __init__(self, bus_name: str, path: str, introspection: Union[intr.Node, str, ET.Element], bus: BaseMessageBus): super().__init__(bus_name, path, introspection, bus, ProxyInterface) diff --git a/dbus_next/introspection.py b/dbus_next/introspection.py index 3cbacbd..adcd542 100644 --- a/dbus_next/introspection.py +++ b/dbus_next/introspection.py @@ -28,7 +28,6 @@ class Arg: - :class:`InvalidSignatureError ` - If the signature is not valid. - :class:`InvalidIntrospectionError ` - If the signature is not a single complete type. """ - def __init__(self, signature: Union[SignatureType, str], direction: List[ArgDirection] = None, @@ -101,7 +100,6 @@ class Signal: :raises: - :class:`InvalidMemberNameError ` - If the name of the signal is not a valid member name. """ - def __init__(self, name: str, args: List[Arg] = None): if name is not None: assert_member_name_valid(name) @@ -165,7 +163,6 @@ class Method: :raises: - :class:`InvalidMemberNameError ` - If the name of this method is not valid. """ - def __init__(self, name: str, in_args: List[Arg] = [], out_args: List[Arg] = []): assert_member_name_valid(name) @@ -238,8 +235,9 @@ class Property: - :class `InvalidSignatureError ` - If the given signature is not valid. - :class: `InvalidMemberNameError ` - If the member name is not valid. """ - - def __init__(self, name: str, signature: str, + def __init__(self, + name: str, + signature: str, access: PropertyAccess = PropertyAccess.READWRITE): assert_member_name_valid(name) @@ -303,7 +301,6 @@ class Interface: :raises: - :class:`InvalidInterfaceNameError ` - If the name is not a valid interface name. """ - def __init__(self, name: str, methods: List[Method] = None, @@ -387,7 +384,6 @@ class Node: :raises: - :class:`InvalidIntrospectionError ` - If the name is not a valid node name. """ - def __init__(self, name: str = None, interfaces: List[Interface] = None, is_root: bool = True): if not is_root and not name: raise InvalidIntrospectionError('child nodes must have a "name" attribute') diff --git a/dbus_next/message.py b/dbus_next/message.py index 1f6d643..4dd3844 100644 --- a/dbus_next/message.py +++ b/dbus_next/message.py @@ -54,7 +54,6 @@ class Message: - :class:`InvalidMemberNameError` - If ``member`` is not a valid member name. - :class:`InvalidInterfaceNameError` - If ``error_name`` or ``interface`` is not a valid interface name. """ - def __init__(self, destination: str = None, path: str = None, diff --git a/dbus_next/message_bus.py b/dbus_next/message_bus.py index 51008bd..ada5f8a 100644 --- a/dbus_next/message_bus.py +++ b/dbus_next/message_bus.py @@ -43,7 +43,6 @@ class BaseMessageBus: be :class:`None` until the message bus connects. :vartype unique_name: str """ - def __init__(self, bus_address: Optional[str] = None, bus_type: BusType = BusType.SESSION, @@ -184,8 +183,8 @@ def reply_notify(reply, err): def request_name(self, name: str, flags: NameFlag = NameFlag.NONE, - callback: Optional[ - Callable[[Optional[RequestNameReply], Optional[Exception]], None]] = None): + callback: Optional[Callable[[Optional[RequestNameReply], Optional[Exception]], + None]] = None): """Request that this message bus owns the given name. :param name: The name to request. @@ -227,8 +226,8 @@ def reply_notify(reply, err): def release_name(self, name: str, - callback: Optional[ - Callable[[Optional[ReleaseNameReply], Optional[Exception]], None]] = None): + callback: Optional[Callable[[Optional[ReleaseNameReply], Optional[Exception]], + None]] = None): """Request that this message bus release the given name. :param name: The name to release. diff --git a/dbus_next/proxy_object.py b/dbus_next/proxy_object.py index cf211d9..ff17a2f 100644 --- a/dbus_next/proxy_object.py +++ b/dbus_next/proxy_object.py @@ -37,7 +37,6 @@ class BaseProxyInterface: :ivar bus: The message bus this proxy interface is connected to. :vartype bus: :class:`BaseMessageBus ` """ - def __init__(self, bus_name, path, introspection, bus): self.bus_name = bus_name @@ -104,7 +103,6 @@ class BaseProxyObject: - :class:`InvalidObjectPathError ` - If the given object path is not valid. - :class:`InvalidIntrospectionError ` - If the introspection data for the node is not valid. """ - def __init__(self, bus_name: str, path: str, introspection: Union[intr.Node, str, ET.Element], bus: 'message_bus.BaseMessageBus', ProxyInterface: Type[BaseProxyInterface]): assert_object_path_valid(path) diff --git a/dbus_next/service.py b/dbus_next/service.py index 6152d89..d459fad 100644 --- a/dbus_next/service.py +++ b/dbus_next/service.py @@ -317,7 +317,6 @@ class ServiceInterface: valid interface name. :vartype name: str """ - def __init__(self, name: str): # TODO cannot be overridden by a dbus member self.name = name diff --git a/dbus_next/signature.py b/dbus_next/signature.py index c3b96f9..adedc36 100644 --- a/dbus_next/signature.py +++ b/dbus_next/signature.py @@ -291,7 +291,6 @@ class SignatureTree: :raises: :class:`InvalidSignatureError` if the given signature is not valid. """ - def __init__(self, signature: str = ''): self.signature = signature @@ -353,7 +352,6 @@ class Variant: :class:`InvalidSignatureError` if the signature is not valid. :class:`SignatureBodyMismatchError` if the signature does not match the body. """ - def __init__(self, signature: Union[str, SignatureTree, SignatureType], value: Any): signature_str = '' signature_tree = None From 3c9dfbce0c65c7241aa2d4e035646f9733122c26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Lowas-Rzechonek?= Date: Sat, 28 Dec 2019 14:03:16 +0100 Subject: [PATCH 2/3] Allow coroutines in service methods fixes #24 --- dbus_next/aio/message_bus.py | 26 ++++++++++++++++ dbus_next/message_bus.py | 49 ++++++++++++++++------------- dbus_next/service.py | 10 ------ docs/high-level-service/index.rst | 4 +-- test/service/test_methods.py | 51 +++++++++++++++++++++++++++++-- 5 files changed, 105 insertions(+), 35 deletions(-) diff --git a/dbus_next/aio/message_bus.py b/dbus_next/aio/message_bus.py index b57b63e..158ca08 100644 --- a/dbus_next/aio/message_bus.py +++ b/dbus_next/aio/message_bus.py @@ -2,6 +2,7 @@ from .._private.unmarshaller import Unmarshaller from ..message import Message from ..constants import BusType, NameFlag, RequestNameReply, ReleaseNameReply, MessageType, MessageFlag +from ..service import ServiceInterface from .._private.auth import auth_external, auth_parse_line, auth_begin, AuthResponse from ..errors import AuthError, DBusError from .proxy_object import ProxyObject @@ -256,6 +257,31 @@ def send(self, msg: Message): def get_proxy_object(self, bus_name: str, path: str, introspection: intr.Node) -> ProxyObject: return super().get_proxy_object(bus_name, path, introspection) + @classmethod + def _make_method_handler(cls, interface, method): + if not asyncio.iscoroutinefunction(method.fn): + return super()._make_method_handler(interface, method) + + def handler(msg, send_reply): + def done(fut): + try: + body = ServiceInterface._fn_result_to_body(fut.result(), + method.out_signature_tree) + send_reply(Message.new_method_return(msg, method.out_signature, body)) + except DBusError as e: + send_reply(e._as_message(msg)) + except Exception as e: + send_reply( + Message.new_error( + msg, ErrorType.SERVICE_ERROR, + f'The service interface raised an error: {e}.\n{traceback.format_exc()}' + )) + + fut = asyncio.ensure_future(method.fn(interface, *msg.body)) + fut.add_done_callback(done) + + return handler + def _message_reader(self): try: while True: diff --git a/dbus_next/message_bus.py b/dbus_next/message_bus.py index ada5f8a..5094ed0 100644 --- a/dbus_next/message_bus.py +++ b/dbus_next/message_bus.py @@ -548,9 +548,7 @@ def _process_message(self, msg): if handler: try: - result = handler(msg) - if type(result) is Message: - send_reply(result) + handler(msg, send_reply) except DBusError as e: send_reply(e._as_message(msg)) except Exception as e: @@ -559,7 +557,6 @@ def _process_message(self, msg): msg, ErrorType.SERVICE_ERROR, f'The service interface raised an error: {e}.\n{traceback.format_exc()}' )) - else: send_reply( Message.new_error( @@ -575,6 +572,15 @@ def _process_message(self, msg): handler(msg, None) del self._method_return_handlers[msg.reply_serial] + @classmethod + def _make_method_handler(cls, interface, method): + def handler(msg, send_reply): + result = method.fn(interface, *msg.body) + body = ServiceInterface._fn_result_to_body(result, method.out_signature_tree) + send_reply(Message.new_method_return(msg, method.out_signature, body)) + + return handler + def _find_message_handler(self, msg): handler = None @@ -593,7 +599,7 @@ def _find_message_handler(self, msg): handler = self._default_get_machine_id_handler elif msg._matches(interface='org.freedesktop.DBus.ObjectManager', member='GetManagedObjects'): - handler = self._default_object_manager + handler = self._default_get_managed_objects_handler else: for interface in self._path_exports.get(msg.path, []): @@ -603,23 +609,23 @@ def _find_message_handler(self, msg): if msg._matches(interface=interface.name, member=method.name, signature=method.in_signature): - handler = ServiceInterface._make_method_handler(interface, method) + handler = self._make_method_handler(interface, method) break if handler: break return handler - def _default_introspect_handler(self, msg): + def _default_introspect_handler(self, msg, send_reply): introspection = self._introspect_export_path(msg.path).tostring() - return Message.new_method_return(msg, 's', [introspection]) + send_reply(Message.new_method_return(msg, 's', [introspection])) - def _default_ping_handler(self, msg): - return Message.new_method_return(msg) + def _default_ping_handler(self, msg, send_reply): + send_reply(Message.new_method_return(msg)) - def _default_get_machine_id_handler(self, msg): + def _default_get_machine_id_handler(self, msg, send_reply): if self._machine_id: - self.send(Message.new_method_return(msg, 's', self._machine_id)) + send_reply(Message.new_method_return(msg, 's', self._machine_id)) return def reply_handler(reply, err): @@ -629,11 +635,11 @@ def reply_handler(reply, err): if reply.message_type == MessageType.METHOD_RETURN: self._machine_id = reply.body[0] - self.send(Message.new_method_return(msg, 's', [self._machine_id])) + send_reply(Message.new_method_return(msg, 's', [self._machine_id])) elif reply.message_type == MessageType.ERROR: - self.send(Message.new_error(msg, reply.error_name, reply.body)) + send_reply(Message.new_error(msg, reply.error_name, reply.body)) else: - self.send(Message.new_error(msg, ErrorType.FAILED, 'could not get machine_id')) + send_reply(Message.new_error(msg, ErrorType.FAILED, 'could not get machine_id')) self._call( Message(destination='org.freedesktop.DBus', @@ -641,7 +647,7 @@ def reply_handler(reply, err): interface='org.freedesktop.DBus.Peer', member='GetMachineId'), reply_handler) - def _default_object_manager(self, msg): + def _default_get_managed_objects_handler(self, msg, send_reply): result = {} for node in self._path_exports: @@ -652,9 +658,9 @@ def _default_object_manager(self, msg): for interface in self._path_exports[node]: result[node][interface.name] = self._get_all_properties(interface) - return Message.new_method_return(msg, 'a{oa{sa{sv}}}', [result]) + send_reply(Message.new_method_return(msg, 'a{oa{sa{sv}}}', [result])) - def _default_properties_handler(self, msg): + def _default_properties_handler(self, msg, send_reply): methods = {'Get': 'ss', 'Set': 'ssv', 'GetAll': 's'} if msg.member not in methods or methods[msg.member] != msg.signature: raise DBusError( @@ -694,7 +700,8 @@ def _default_properties_handler(self, msg): raise DBusError(ErrorType.UNKNOWN_PROPERTY, 'the property does not have read access') prop_value = getattr(interface, prop.prop_getter.__name__) - return Message.new_method_return(msg, 'v', [Variant(prop.signature, prop_value)]) + send_reply( + Message.new_method_return(msg, 'v', [Variant(prop.signature, prop_value)])) elif msg.member == 'Set': if not prop.access.writable(): raise DBusError(ErrorType.PROPERTY_READ_ONLY, 'the property is readonly') @@ -704,11 +711,11 @@ def _default_properties_handler(self, msg): f'wrong signature for property. expected "{prop.signature}"') assert prop.prop_setter setattr(interface, prop.prop_setter.__name__, value.value) - return Message.new_method_return(msg) + send_reply(Message.new_method_return(msg)) elif msg.member == 'GetAll': result = self._get_all_properties(interface) - return Message.new_method_return(msg, 'a{sv}', [result]) + send_reply(Message.new_method_return(msg, 'a{sv}', [result])) else: assert False diff --git a/dbus_next/service.py b/dbus_next/service.py index d459fad..bd390cd 100644 --- a/dbus_next/service.py +++ b/dbus_next/service.py @@ -1,7 +1,6 @@ from .constants import PropertyAccess from .signature import SignatureTree, SignatureBodyMismatchError, Variant from . import introspection as intr -from .message import Message from .errors import SignalDisabledError from functools import wraps @@ -452,12 +451,3 @@ def _handle_signal(interface, signal, result): for bus in ServiceInterface._get_buses(interface): bus._interface_signal_notify(interface, interface.name, signal.name, signal.signature, body) - - @staticmethod - def _make_method_handler(interface, method): - def handler(msg): - result = method.fn(interface, *msg.body) - body = ServiceInterface._fn_result_to_body(result, method.out_signature_tree) - return Message.new_method_return(msg, method.out_signature, body) - - return handler diff --git a/docs/high-level-service/index.rst b/docs/high-level-service/index.rst index 0c4852a..cd10855 100644 --- a/docs/high-level-service/index.rst +++ b/docs/high-level-service/index.rst @@ -12,7 +12,7 @@ If you're exposing a service for general use, you can request a well-known name Services are defined by subclassing :class:`ServiceInterface ` and definining members as methods on the class with the decorator methods :func:`@method() `, :func:`@dbus_property() `, and :func:`@signal() `. The parameters of the decorated class methods must be annotated with DBus type strings to indicate the types of values they expect. See the documentation on `the type system `_ for more information on how DBus types are mapped to Python values with signature strings. The decorator methods themselves take arguments that affect how the member is exported on the bus, such as the name of the member or the access permissions of a property. -A class method decorated with ``@method()`` will be called when a client calls the method over DBus. The parameters given to the class method will be provided by the calling client and will conform to the parameter type annotations. The value returned by the class method will be returned to the client and must conform to the return type annotation specified by the user. If the return annotation specifies more than one type, the values must be returned in a ``list``. +A class method decorated with ``@method()`` will be called when a client calls the method over DBus. The parameters given to the class method will be provided by the calling client and will conform to the parameter type annotations. The value returned by the class method will be returned to the client and must conform to the return type annotation specified by the user. If the return annotation specifies more than one type, the values must be returned in a ``list``. When :class:`aio.MessageBus` is used, methods can be coroutines. A class method decorated with ``@dbus_property()`` will be exposed as a DBus property getter. This decoration works the same as a standard Python ``@property``. The getter will be called when a client gets the property through the standard properties interface with ``org.freedesktop.DBus.Properties.Get``. Define a property setter with ``@method_name.setter`` taking the new value as a parameter. The setter will be called when the client sets the property through ``org.freedesktop.DBus.Properties.Set``. @@ -48,7 +48,7 @@ After the service interface is defined, call :func:`MessageBus.export() 'vv': + async def Bazify(self, bar: '(iiu)') -> 'vv': print(f'called Bazify with bar={bar}') return [Variant('s', 'example'), Variant('s', 'bazify')] diff --git a/test/service/test_methods.py b/test/service/test_methods.py index f85f91b..4f528f1 100644 --- a/test/service/test_methods.py +++ b/test/service/test_methods.py @@ -51,12 +51,59 @@ def throws_dbus_error(self): raise DBusError('test.error', 'an error ocurred') +class AsyncInterface(ServiceInterface): + def __init__(self, name): + super().__init__(name) + + @method() + async def echo(self, what: 's') -> 's': + assert type(self) is AsyncInterface + return what + + @method() + async def echo_multiple(self, what1: 's', what2: 's') -> 'ss': + assert type(self) is AsyncInterface + return [what1, what2] + + @method() + async def echo_containers(self, array: 'as', variant: 'v', dict_entries: 'a{sv}', + struct: '(s(s(v)))') -> 'asva{sv}(s(s(v)))': + assert type(self) is AsyncInterface + return [array, variant, dict_entries, struct] + + @method() + async def ping(self): + assert type(self) is AsyncInterface + pass + + @method(name='renamed') + async def original_name(self): + assert type(self) is AsyncInterface + pass + + @method(disabled=True) + async def not_here(self): + assert type(self) is AsyncInterface + pass + + @method() + async def throws_unexpected_error(self): + assert type(self) is AsyncInterface + raise Exception('oops') + + @method() + def throws_dbus_error(self): + assert type(self) is AsyncInterface + raise DBusError('test.error', 'an error ocurred') + + +@pytest.mark.parametrize('interface_class', [ExampleInterface, AsyncInterface]) @pytest.mark.asyncio -async def test_methods(): +async def test_methods(interface_class): bus1 = await MessageBus().connect() bus2 = await MessageBus().connect() - interface = ExampleInterface('test.interface') + interface = interface_class('test.interface') export_path = '/test/path' async def call(member, signature='', body=[], flags=MessageFlag.NONE): From 0b228d16ed00dfec29ab759d41bc80f9177e7888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Lowas-Rzechonek?= Date: Sat, 28 Dec 2019 16:48:22 +0100 Subject: [PATCH 3/3] Extract reply handling to a context manager --- dbus_next/aio/message_bus.py | 14 +++-------- dbus_next/message_bus.py | 49 +++++++++++++++++++++++------------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/dbus_next/aio/message_bus.py b/dbus_next/aio/message_bus.py index 158ca08..fe2c723 100644 --- a/dbus_next/aio/message_bus.py +++ b/dbus_next/aio/message_bus.py @@ -264,18 +264,10 @@ def _make_method_handler(cls, interface, method): def handler(msg, send_reply): def done(fut): - try: - body = ServiceInterface._fn_result_to_body(fut.result(), - method.out_signature_tree) + with send_reply: + result = fut.result() + body = ServiceInterface._fn_result_to_body(result, method.out_signature_tree) send_reply(Message.new_method_return(msg, method.out_signature, body)) - except DBusError as e: - send_reply(e._as_message(msg)) - except Exception as e: - send_reply( - Message.new_error( - msg, ErrorType.SERVICE_ERROR, - f'The service interface raised an error: {e}.\n{traceback.format_exc()}' - )) fut = asyncio.ensure_future(method.fn(interface, *msg.body)) fut.add_done_callback(done) diff --git a/dbus_next/message_bus.py b/dbus_next/message_bus.py index 5094ed0..d0286eb 100644 --- a/dbus_next/message_bus.py +++ b/dbus_next/message_bus.py @@ -496,6 +496,32 @@ def _on_message(self, msg): logging.error( f'got unexpected error processing a message: {e}.\n{traceback.format_exc()}') + def _send_reply(self, msg): + bus = self + + class SendReply: + def __enter__(self): + return self + + def __call__(self, reply): + if msg.flags & MessageFlag.NO_REPLY_EXPECTED: + return + + bus.send(reply) + + def __exit__(self, type, value, tb): + if type == DBusError: + self(value._as_message(msg)) + + if type == Exception: + self( + Message.new_error( + msg, ErrorType.SERVICE_ERROR, + f'The service interface raised an error: {value}.\n{traceback.format_tb(tb)}' + )) + + return SendReply() + def _process_message(self, msg): handled = False @@ -541,28 +567,17 @@ def _process_message(self, msg): if not handled: handler = self._find_message_handler(msg) - send_reply = self.send - - if msg.flags & MessageFlag.NO_REPLY_EXPECTED: - send_reply = lambda msg: None + send_reply = self._send_reply(msg) - if handler: - try: + with send_reply: + if handler: handler(msg, send_reply) - except DBusError as e: - send_reply(e._as_message(msg)) - except Exception as e: + else: send_reply( Message.new_error( - msg, ErrorType.SERVICE_ERROR, - f'The service interface raised an error: {e}.\n{traceback.format_exc()}' + msg, ErrorType.UNKNOWN_METHOD, + f'{msg.interface}.{msg.member} with signature "{msg.signature}" could not be found' )) - else: - send_reply( - Message.new_error( - msg, ErrorType.UNKNOWN_METHOD, - f'{msg.interface}.{msg.member} with signature "{msg.signature}" could not be found' - )) else: # An ERROR or a METHOD_RETURN