Skip to content

Add options to explicitly specify DBus signatures in decorators #111

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 85 additions & 33 deletions dbus_next/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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
Expand All @@ -66,6 +84,10 @@ def method(name: str = None, disabled: bool = False):
: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:

Expand All @@ -91,25 +113,29 @@ 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

return decorator


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))
Expand All @@ -124,7 +150,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
Expand All @@ -141,6 +167,8 @@ def signal(name: str = None, disabled: bool = False):
: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:

Expand All @@ -162,7 +190,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):
Expand Down Expand Up @@ -205,18 +233,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)
Expand All @@ -226,26 +262,40 @@ 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)

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
Expand All @@ -270,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:

Expand All @@ -293,7 +345,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

Expand Down
65 changes: 55 additions & 10 deletions test/service/test_decorators.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
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):
super().__init__('test.interface')
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':
Expand Down Expand Up @@ -47,6 +50,22 @@ 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

@signal(signature="as")
def foo_signal(self) -> List[str]:
return ['result']


def test_method_decorator():
interface = ExampleInterface()
Expand All @@ -56,23 +75,32 @@ 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'
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'
Expand All @@ -81,12 +109,18 @@ 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
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'
Expand All @@ -95,7 +129,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'
Expand All @@ -106,7 +151,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'
Expand Down Expand Up @@ -142,7 +187,7 @@ def test_interface_introspection():
signals = xml.findall('signal')
properties = xml.findall('property')

assert len(xml) == 4
assert len(methods) == 1
assert len(signals) == 1
assert len(properties) == 2
assert len(xml) == 7
assert len(methods) == 2
assert len(signals) == 2
assert len(properties) == 3