From ffb374cabda8b51b6d8c15178f2a347bbb1d8b81 Mon Sep 17 00:00:00 2001 From: Anders Hellerup Madsen Date: Wed, 5 Jan 2022 22:34:48 +0100 Subject: [PATCH 1/4] support signatures in method decorator --- dbus_next/service.py | 61 +++++++++++++++++++++++---------- test/service/test_decorators.py | 23 ++++++++++--- 2 files changed, 61 insertions(+), 23 deletions(-) diff --git a/dbus_next/service.py b/dbus_next/service.py index b2a96cf..386fdf1 100644 --- a/dbus_next/service.py +++ b/dbus_next/service.py @@ -6,32 +6,47 @@ from functools import wraps import inspect -from typing import no_type_check_decorator, Dict, List, Any +from typing import no_type_check_decorator, Dict, List, Any, Optional import copy import asyncio class _Method: - def __init__(self, fn, name, disabled=False): - in_signature = '' - out_signature = '' + def __init__(self, + fn, + name, + disabled=False, + in_signature: Optional[str] = None, + out_signature: Optional[str] = None): inspection = inspect.signature(fn) - in_args = [] - for i, param in enumerate(inspection.parameters.values()): - if i == 0: - # first is self - continue - annotation = parse_annotation(param.annotation) - if not annotation: - raise ValueError( - 'method parameters must specify the dbus type string as an annotation') - in_args.append(intr.Arg(annotation, intr.ArgDirection.IN, param.name)) - in_signature += annotation + if in_signature is None: + in_signature = '' + in_args = [] + for i, param in enumerate(inspection.parameters.values()): + if i == 0: + # first is self + continue + annotation = parse_annotation(param.annotation) + if not annotation: + raise ValueError( + 'method parameters must specify the dbus type string as an annotation') + in_args.append(intr.Arg(annotation, intr.ArgDirection.IN, param.name)) + in_signature += annotation + else: + name_iter = iter(inspection.parameters.keys()) + next(name_iter) # skip self parameter + in_args = [ + intr.Arg(type_, intr.ArgDirection.IN, name) + for name, type_ in zip(name_iter, + SignatureTree._get(in_signature).types) + ] + + if out_signature is None: + out_signature = parse_annotation(inspection.return_annotation) out_args = [] - out_signature = parse_annotation(inspection.return_annotation) if out_signature: for type_ in SignatureTree._get(out_signature).types: out_args.append(intr.Arg(type_, intr.ArgDirection.OUT)) @@ -41,12 +56,15 @@ def __init__(self, fn, name, disabled=False): self.disabled = disabled self.introspection = intr.Method(name, in_args, out_args) self.in_signature = in_signature - self.out_signature = out_signature self.in_signature_tree = SignatureTree._get(in_signature) + self.out_signature = out_signature self.out_signature_tree = SignatureTree._get(out_signature) -def method(name: str = None, disabled: bool = False): +def method(name: str = None, + disabled: bool = False, + in_signature: Optional[str] = None, + out_signature: Optional[str] = None): """A decorator to mark a class method of a :class:`ServiceInterface` to be a DBus service method. The parameters and return value must each be annotated with a signature @@ -91,7 +109,12 @@ def wrapped(*args, **kwargs): fn(*args, **kwargs) fn_name = name if name else fn.__name__ - wrapped.__dict__['__DBUS_METHOD'] = _Method(fn, fn_name, disabled=disabled) + _method = _Method(fn, + fn_name, + disabled=disabled, + in_signature=in_signature, + out_signature=out_signature) + wrapped.__dict__['__DBUS_METHOD'] = _method return wrapped diff --git a/test/service/test_decorators.py b/test/service/test_decorators.py index 1cd236f..992c404 100644 --- a/test/service/test_decorators.py +++ b/test/service/test_decorators.py @@ -1,6 +1,8 @@ from dbus_next import PropertyAccess, introspection as intr from dbus_next.service import method, signal, dbus_property, ServiceInterface +from typing import List + class ExampleInterface(ServiceInterface): def __init__(self): @@ -17,6 +19,10 @@ def some_method(self, one: 's', two: 's') -> 's': def another_method(self, eight: 'o', six: 't'): pass + @method(in_signature="sasu", out_signature="i") + def a_third_method(self, one: str, two: List[str], three) -> int: + return 42 + @signal() def some_signal(self) -> 'as': return ['result'] @@ -56,16 +62,25 @@ def test_method_decorator(): methods = ServiceInterface._get_methods(interface) signals = ServiceInterface._get_signals(interface) - assert len(methods) == 2 + assert len(methods) == 3 method = methods[0] + assert method.name == 'a_third_method' + assert method.in_signature == 'sasu' + assert method.out_signature == 'i' + assert not method.disabled + assert type(method.introspection) is intr.Method + assert len(method.introspection.in_args) == 3 + assert len(method.introspection.out_args) == 1 + + method = methods[1] assert method.name == 'renamed_method' assert method.in_signature == 'ot' assert method.out_signature == '' assert method.disabled assert type(method.introspection) is intr.Method - method = methods[1] + method = methods[2] assert method.name == 'some_method' assert method.in_signature == 'ss' assert method.out_signature == 's' @@ -142,7 +157,7 @@ def test_interface_introspection(): signals = xml.findall('signal') properties = xml.findall('property') - assert len(xml) == 4 - assert len(methods) == 1 + assert len(xml) == 5 + assert len(methods) == 2 assert len(signals) == 1 assert len(properties) == 2 From f158687d25053b19677c9514336e6f8d9fbf2606 Mon Sep 17 00:00:00 2001 From: Anders Hellerup Madsen Date: Thu, 6 Jan 2022 22:39:27 +0100 Subject: [PATCH 2/4] support signatures in dbus_property decorator The builtin @decorator property recreates itself when the .setter, .getter or .deleter child-decorators are used. This meant that the introspection meant for the getter was also being applied to the setter. I added a guard for this situation to exit early, and instead copy the already introspected properties over from the getter. --- dbus_next/service.py | 36 +++++++++++++++++++++++++------ test/service/test_decorators.py | 38 +++++++++++++++++++++++++-------- 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/dbus_next/service.py b/dbus_next/service.py index 386fdf1..c37618e 100644 --- a/dbus_next/service.py +++ b/dbus_next/service.py @@ -228,18 +228,26 @@ def set_options(self, options): self.__dict__['__DBUS_PROPERTY'] = True def __init__(self, fn, *args, **kwargs): + if args: + # this is a call to prop.setter - all we need to do call super + return super().__init__(fn, *args, **kwargs) + self.prop_getter = fn self.prop_setter = None inspection = inspect.signature(fn) + if len(inspection.parameters) != 1: raise ValueError('the property must only have the "self" input parameter') - return_annotation = parse_annotation(inspection.return_annotation) + return_annotation = kwargs.pop('signature', None) + if return_annotation is None: + return_annotation = parse_annotation(inspection.return_annotation) if not return_annotation: raise ValueError( - 'the property must specify the dbus type string as a return annotation string') + 'the property must specify the dbus type string as a return annotation string or with the signature option' + ) self.signature = return_annotation tree = SignatureTree._get(return_annotation) @@ -249,10 +257,9 @@ def __init__(self, fn, *args, **kwargs): self.type = tree.types[0] - if 'options' in kwargs: - options = kwargs['options'] + options = kwargs.pop('options', None) + if options is not None: self.set_options(options) - del kwargs['options'] super().__init__(fn, *args, **kwargs) @@ -260,15 +267,30 @@ def setter(self, fn, **kwargs): # XXX The setter decorator seems to be recreating the class in the list # of class members and clobbering the options so we need to reset them. # Why does it do that? + # + # The default implementation of setter basically looks like this: + # + # def setter(self, fset): + # return type(self)(self.fget, fset, self.fdel) + # + # That is it creates a new instance, with the new setter, carrying + # the getter and deleter over from the the existing instance. + # + # In this case, we need to carry all the private properties from the + # old instance and reset the options on the new instance. result = super().setter(fn, **kwargs) + result.prop_getter = self.prop_getter result.prop_setter = fn + result.signature = self.signature + result.type = self.type result.set_options(self.options) return result def dbus_property(access: PropertyAccess = PropertyAccess.READWRITE, name: str = None, - disabled: bool = False): + disabled: bool = False, + signature: Optional[str] = None): """A decorator to mark a class method of a :class:`ServiceInterface` to be a DBus property. The class method must be a Python getter method with a return annotation @@ -316,7 +338,7 @@ def string_prop(self, val: 's'): @no_type_check_decorator def decorator(fn): options = {'name': name, 'access': access, 'disabled': disabled} - return _Property(fn, options=options) + return _Property(fn, options=options, signature=signature) return decorator diff --git a/test/service/test_decorators.py b/test/service/test_decorators.py index 992c404..3c83c72 100644 --- a/test/service/test_decorators.py +++ b/test/service/test_decorators.py @@ -10,6 +10,7 @@ def __init__(self): self._some_prop = 55 self._another_prop = 101 self._weird_prop = 500 + self._foo_prop = 17 @method() def some_method(self, one: 's', two: 's') -> 's': @@ -19,10 +20,6 @@ def some_method(self, one: 's', two: 's') -> 's': def another_method(self, eight: 'o', six: 't'): pass - @method(in_signature="sasu", out_signature="i") - def a_third_method(self, one: str, two: List[str], three) -> int: - return 42 - @signal() def some_signal(self) -> 'as': return ['result'] @@ -53,6 +50,18 @@ def weird_prop(self) -> 't': def setter_for_weird_prop(self, val: 't'): self._weird_prop = val + @method(in_signature="sasu", out_signature="i") + def a_third_method(self, one: str, two: List[str], three) -> int: + return 42 + + @dbus_property(signature='u') + def foo_prop(self) -> int: + return self._foo_prop + + @foo_prop.setter + def foo_prop(self, val: int): + self._foo_prop = val + def test_method_decorator(): interface = ExampleInterface() @@ -101,7 +110,7 @@ def test_method_decorator(): assert not signal.disabled assert type(signal.introspection) is intr.Signal - assert len(properties) == 3 + assert len(properties) == 4 renamed_readonly_prop = properties[0] assert renamed_readonly_prop.name == 'renamed_readonly_property' @@ -110,7 +119,18 @@ def test_method_decorator(): assert renamed_readonly_prop.disabled assert type(renamed_readonly_prop.introspection) is intr.Property - weird_prop = properties[1] + foo_prop = properties[1] + assert foo_prop.name == 'foo_prop' + assert foo_prop.access == PropertyAccess.READWRITE + assert foo_prop.signature == 'u' + assert not foo_prop.disabled + assert foo_prop.prop_getter is not None + assert foo_prop.prop_getter.__name__ == 'foo_prop' + assert foo_prop.prop_setter is not None + assert foo_prop.prop_setter.__name__ == 'foo_prop' + assert type(foo_prop.introspection) is intr.Property + + weird_prop = properties[2] assert weird_prop.name == 'weird_prop' assert weird_prop.access == PropertyAccess.READWRITE assert weird_prop.signature == 't' @@ -121,7 +141,7 @@ def test_method_decorator(): assert weird_prop.prop_setter.__name__ == 'setter_for_weird_prop' assert type(weird_prop.introspection) is intr.Property - prop = properties[2] + prop = properties[3] assert prop.name == 'some_prop' assert prop.access == PropertyAccess.READWRITE assert prop.signature == 'u' @@ -157,7 +177,7 @@ def test_interface_introspection(): signals = xml.findall('signal') properties = xml.findall('property') - assert len(xml) == 5 + assert len(xml) == 6 assert len(methods) == 2 assert len(signals) == 1 - assert len(properties) == 2 + assert len(properties) == 3 From dd66119f1418db0ef08de798f74405b741962546 Mon Sep 17 00:00:00 2001 From: Anders Hellerup Madsen Date: Thu, 6 Jan 2022 22:55:57 +0100 Subject: [PATCH 3/4] support signatures to signal decorator --- dbus_next/service.py | 13 ++++++------- test/service/test_decorators.py | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/dbus_next/service.py b/dbus_next/service.py index c37618e..4a6d3d2 100644 --- a/dbus_next/service.py +++ b/dbus_next/service.py @@ -122,17 +122,16 @@ def wrapped(*args, **kwargs): class _Signal: - def __init__(self, fn, name, disabled=False): + def __init__(self, fn, name, disabled=False, signature: Optional[str] = None): inspection = inspect.signature(fn) args = [] - signature = '' signature_tree = None - return_annotation = parse_annotation(inspection.return_annotation) + if signature is None: + signature = parse_annotation(inspection.return_annotation) - if return_annotation: - signature = return_annotation + if signature: signature_tree = SignatureTree._get(signature) for type_ in signature_tree.types: args.append(intr.Arg(type_, intr.ArgDirection.OUT)) @@ -147,7 +146,7 @@ def __init__(self, fn, name, disabled=False): self.introspection = intr.Signal(self.name, args) -def signal(name: str = None, disabled: bool = False): +def signal(name: str = None, disabled: bool = False, signature: Optional[str] = None): """A decorator to mark a class method of a :class:`ServiceInterface` to be a DBus signal. The signal is broadcast on the bus when the decorated class method is @@ -185,7 +184,7 @@ def two_strings_signal(self, val1, val2) -> 'ss': @no_type_check_decorator def decorator(fn): fn_name = name if name else fn.__name__ - signal = _Signal(fn, fn_name, disabled) + signal = _Signal(fn, fn_name, disabled, signature) @wraps(fn) def wrapped(self, *args, **kwargs): diff --git a/test/service/test_decorators.py b/test/service/test_decorators.py index 3c83c72..779880e 100644 --- a/test/service/test_decorators.py +++ b/test/service/test_decorators.py @@ -62,6 +62,10 @@ def foo_prop(self) -> int: def foo_prop(self, val: int): self._foo_prop = val + @signal(signature="as") + def foo_signal(self) -> List[str]: + return ['result'] + def test_method_decorator(): interface = ExampleInterface() @@ -96,7 +100,7 @@ def test_method_decorator(): assert not method.disabled assert type(method.introspection) is intr.Method - assert len(signals) == 2 + assert len(signals) == 3 signal = signals[0] assert signal.name == 'renamed_signal' @@ -105,6 +109,12 @@ def test_method_decorator(): assert type(signal.introspection) is intr.Signal signal = signals[1] + assert signal.name == 'foo_signal' + assert signal.signature == 'as' + assert not signal.disabled + assert type(signal.introspection) is intr.Signal + + signal = signals[2] assert signal.name == 'some_signal' assert signal.signature == 'as' assert not signal.disabled @@ -177,7 +187,7 @@ def test_interface_introspection(): signals = xml.findall('signal') properties = xml.findall('property') - assert len(xml) == 6 + assert len(xml) == 7 assert len(methods) == 2 - assert len(signals) == 1 + assert len(signals) == 2 assert len(properties) == 3 From c4fb5c7a7970c3ec579396920e3d51ebb3c88bcb Mon Sep 17 00:00:00 2001 From: Anders Hellerup Madsen Date: Thu, 6 Jan 2022 23:07:47 +0100 Subject: [PATCH 4/4] document signature parameter to decorators --- dbus_next/service.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dbus_next/service.py b/dbus_next/service.py index 4a6d3d2..8b1b922 100644 --- a/dbus_next/service.py +++ b/dbus_next/service.py @@ -84,6 +84,10 @@ def method(name: str = None, :type name: str :param disabled: If set to true, the method will not be visible to clients. :type disabled: bool + :param in_signature: If set, this signature string will be used and no parsing of method paramter type annotations will be done. + :type in_signature: str + :param out_signature: If set, this signature string will be used and no parsing of the method return annotation will be done. + :type out_signature: str :example: @@ -163,6 +167,8 @@ def signal(name: str = None, disabled: bool = False, signature: Optional[str] = :type name: str :param disabled: If set to true, the signal will not be visible to clients. :type disabled: bool + :param signature: If set, this signature string will be used and no parsing of method type annotations will be done. + :type signature: str :example: @@ -314,6 +320,8 @@ def dbus_property(access: PropertyAccess = PropertyAccess.READWRITE, :param disabled: If set to true, the property will not be visible to clients. :type disabled: bool + :param signature: If set, this signature string will be used and no parsing of method type annotations will be done. + :type signature: str :example: