Skip to content

Commit 14338be

Browse files
committed
Add type decorators for XML generation
The decorators add information about the types a method or property can accept or return. The XML generator can optionally require that every method and property has the information defined. Alternatively it adds the possibility to use annotations instead, but that is a Python 3 only feature.
1 parent fe6c759 commit 14338be

File tree

6 files changed

+282
-12
lines changed

6 files changed

+282
-12
lines changed

doc/tutorial.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,20 +238,22 @@ To prepare a class for exporting on the Bus, provide the dbus introspection XML
238238
in a ''dbus'' class property or in its ''docstring''. For example::
239239

240240
from pydbus.generic import signal
241+
from pydbus.strong_typing import typed_method, typed_property
241242
from pydbus.xml_generator import interface, signalled, attach_introspection_xml
242243

243244
@attach_introspection_xml
244245
@interface("net.lew21.pydbus.TutorialExample")
245246
class Example(object):
246247

248+
@typed_method(("s", ), "s")
247249
def EchoString(self, s):
248250
"""returns whatever is passed to it"""
249251
return s
250252

251253
def __init__(self):
252254
self._someProperty = "initial value"
253255

254-
@property
256+
@typed_property("s")
255257
def SomeProperty(self):
256258
return self._someProperty
257259

pydbus/strong_typing.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Decorators for methods and properties to strongly typed the values."""
2+
import inspect
3+
4+
from pydbus.xml_generator import verify_arguments
5+
6+
7+
def typed_property(value_type):
8+
"""
9+
Decorate a function as a dbus property getter.
10+
11+
It alreay makes the method a property so another `@property` decorator may
12+
not be used.
13+
"""
14+
def decorate(func):
15+
func.prop_type = value_type
16+
return property(func)
17+
return decorate
18+
19+
20+
def typed_method(argument_types, return_type):
21+
"""
22+
Decorate a function as a dbus method.
23+
24+
Parameters
25+
----------
26+
argument_types : tuple
27+
Required argument types for each argument except the first
28+
return_type : string
29+
Type of the returned value, must be None if it returns nothing
30+
"""
31+
def decorate(func):
32+
func.arg_types = argument_types
33+
func.ret_type = return_type
34+
verify_arguments(func)
35+
return func
36+
return decorate

pydbus/tests/strong_typing.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from pydbus.generic import signal
2+
from pydbus.strong_typing import typed_method, typed_property
3+
from pydbus.xml_generator import interface, signalled, attach_introspection_xml
4+
5+
6+
def test_count_off():
7+
"""Test what happens if to many or to few types are defined in methods."""
8+
try:
9+
@typed_method(("s", "i", "o"), None)
10+
def dummy(self, parameter):
11+
pass
12+
13+
assert False
14+
except ValueError as e:
15+
assert str(e) == "Number of argument types (3) differs from the number of parameters (1) in function dummy"
16+
17+
try:
18+
@typed_method(("s", "i"), "o")
19+
def dummy(self, parameter):
20+
pass
21+
22+
assert False
23+
except ValueError as e:
24+
assert str(e) == "Number of argument types (2) differs from the number of parameters (1) in function dummy"
25+
26+
try:
27+
@typed_method(tuple(), None)
28+
def dummy(self, parameter):
29+
pass
30+
31+
assert False
32+
except ValueError as e:
33+
assert str(e) == "Number of argument types (0) differs from the number of parameters (1) in function dummy"
34+
35+
36+
test_count_off()

pydbus/tests/xml_generator.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from pydbus.generic import signal
2+
from pydbus.strong_typing import typed_method, typed_property
23
from pydbus.xml_generator import interface, signalled, attach_introspection_xml
34

45

@@ -9,10 +10,19 @@ class Example(object):
910
def __init__(self):
1011
self._rw = 42
1112

13+
@typed_method(("s", ), "i")
1214
def OneParamReturn(self, parameter):
1315
return 42
1416

15-
@property
17+
@typed_method(("s", ), None)
18+
def OneParamNoReturn(self, parameter):
19+
pass
20+
21+
@typed_property("i")
22+
def ReadProperty(self):
23+
return 42
24+
25+
@typed_property("i")
1626
def RwProperty(self):
1727
return self._rw
1828

@@ -23,13 +33,25 @@ def RwProperty(self, value):
2333

2434
def test_valid():
2535
assert not hasattr(Example.OneParamReturn, 'interface')
36+
assert Example.OneParamReturn.arg_types == ("s", )
37+
assert Example.OneParamReturn.ret_type == "i"
38+
39+
assert not hasattr(Example.OneParamNoReturn, 'interface')
40+
assert Example.OneParamNoReturn.arg_types == ("s", )
41+
assert Example.OneParamNoReturn.ret_type is None
42+
43+
assert not hasattr(Example.ReadProperty, 'interface')
44+
assert isinstance(Example.ReadProperty, property)
45+
assert Example.ReadProperty.fget.prop_type == "i"
46+
assert Example.ReadProperty.fset is None
2647

2748
assert not hasattr(Example.RwProperty, 'interface')
2849
assert isinstance(Example.RwProperty, property)
50+
assert Example.RwProperty.fget.prop_type == "i"
2951
assert Example.RwProperty.fset is not None
3052
assert Example.RwProperty.fset.causes_signal is True
3153

32-
assert Example.dbus == '<node><interface name="net.lvht.Foo1"><method name="OneParamReturn"><arg direction="in" name="parameter" /></method><property access="readwrite" name="RwProperty"><annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true" /></property></interface></node>'
54+
assert Example.dbus == '<node><interface name="net.lvht.Foo1"><method name="OneParamNoReturn"><arg direction="in" name="parameter" type="s" /></method><method name="OneParamReturn"><arg direction="in" name="parameter" type="s" /><arg direction="out" name="return" type="i" /></method><property access="read" name="ReadProperty" type="i" /><property access="readwrite" name="RwProperty" type="i"><annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true" /></property></interface></node>'
3355

3456

3557
def test_invalid_function():
@@ -71,5 +93,40 @@ def Dummy(self, **kwargs):
7193
assert str(e) == "dbus methods do not allow variable keyword arguments"
7294

7395

96+
def test_require_strong_typing():
97+
try:
98+
@attach_introspection_xml(True)
99+
@interface("net.lvht.Foo1")
100+
class Example(object):
101+
102+
def Dummy(self, param):
103+
pass
104+
except ValueError as e:
105+
assert str(e) == "No argument types defined for method 'Dummy'"
106+
107+
@attach_introspection_xml(True)
108+
@interface("net.lvht.Foo1")
109+
class RequiredExample(object):
110+
111+
@typed_method(('s', ), None)
112+
def Dummy(self, param):
113+
pass
114+
115+
assert RequiredExample.Dummy.arg_types == ('s', )
116+
assert RequiredExample.Dummy.ret_type is None
117+
118+
@attach_introspection_xml(False)
119+
@interface("net.lvht.Foo1")
120+
class OptionalExample(object):
121+
122+
@typed_method(('s', ), None)
123+
def Dummy(self, param):
124+
pass
125+
126+
assert OptionalExample.dbus == RequiredExample.dbus
127+
assert OptionalExample is not RequiredExample
128+
129+
74130
test_valid()
75131
test_invalid_function()
132+
test_require_strong_typing()

pydbus/xml_generator.py

Lines changed: 147 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,120 @@
1212
ismethod = inspect.ismethod if sys.version_info[0] == 2 else inspect.isfunction
1313

1414

15-
def verify_arguments(function):
16-
"""Verify that the function is correctly defined."""
15+
def extract_membered_types(function, require_strong_typing, arg_count):
16+
has_arg_types = hasattr(function, "arg_types")
17+
if has_arg_types:
18+
arg_types = function.arg_types
19+
elif require_strong_typing:
20+
raise ValueError(
21+
"No argument types defined for method "
22+
"'{}'".format(function.__name__))
23+
else:
24+
arg_types = (None, ) * arg_count
25+
26+
if hasattr(function, "ret_type"):
27+
if not has_arg_types:
28+
raise ValueError(
29+
"Only explicit return type defined but no explicit "
30+
"argument types for method '{}'".format(function.__name__))
31+
ret_type = function.ret_type
32+
elif has_arg_types:
33+
raise ValueError(
34+
"Only explicit argument types defined but no explicit return "
35+
"for method '{}'".format(function.__name__))
36+
else:
37+
ret_type = None
38+
39+
return arg_types, ret_type
40+
41+
42+
def verify_arguments_getargspec(function, require_strong_typing):
43+
"""Verify arguments using the getargspec function."""
1744
args, vargs, kwargs, defaults = inspect.getargspec(function)
45+
args = args[1:]
46+
arg_types, ret_type = extract_membered_types(
47+
function, require_strong_typing, len(args))
48+
1849
if defaults is not None:
1950
raise ValueError("dbus methods do not allow default values")
2051
# TODO: Check if vargs or kwargs are actually allowed in dbus.
2152
if vargs is not None:
2253
raise ValueError("dbus methods do not allow variable argument functions")
2354
if kwargs is not None:
2455
raise ValueError("dbus methods do not allow variable keyword arguments")
25-
return args
56+
return args, arg_types, ret_type
57+
58+
59+
def verify_arguments_signature(function, require_strong_typing):
60+
"""Verify arguments using the Signature class in Python 3."""
61+
signature = inspect.signature(function)
62+
parameters = signature.parameters[1:]
63+
if not all(param.default is param.empty for param in parameters):
64+
raise ValueError(
65+
"Default values are not allowed for method "
66+
"'{}'".format(function.__name__))
67+
if not all(param.kind == param.POSITIONAL_OR_KEYWORD
68+
for param in parameters):
69+
raise ValueError(
70+
"Variable arguments or keyword only arguments are not allowed for "
71+
"method '{}'".format(function.__name__))
72+
73+
names = [p.name for p in parameters]
74+
arg_types = [param.annotation for param in parameters]
75+
empty_arg = arg_types.count(inspect.Parameter.empty)
76+
if 0 < empty_arg < len(arg_types):
77+
raise ValueError(
78+
"Only partially defined types for method "
79+
"'{}'".format(function.__name__))
80+
elif arg_types and empty_arg > 0 and hasattr(function, "arg_types"):
81+
raise ValueError(
82+
"Annotations and explicit argument types are used together in "
83+
"method '{}'".format(function.__name__))
84+
85+
ret_type = signature.return_annotation
86+
if (ret_type is not signature.empty and
87+
hasattr(function, "ret_type")):
88+
raise ValueError(
89+
"Annotations and explicit return type are used together in "
90+
"method '{}'".format(function.__name__))
91+
92+
# Fall back to the explicit types only if there were no annotations, but
93+
# that might be actually valid if the function returns nothing and has
94+
# no parameters.
95+
# So it also checks that the function has any parameter or it has either of
96+
# the two attributes defined.
97+
# So it won't actually raise an error if a function has no parameter and
98+
# no annotations and no explicit types defined, because it is not possible
99+
# to determine if a function returns something.
100+
if (ret_type is signature.empty and empty_arg == len(arg_types) and
101+
(len(arg_types) > 0 or hasattr(function, "arg_types") or
102+
hasattr(function, "ret_type"))):
103+
arg_types, ret_type = extract_membered_types(
104+
function, require_strong_typing, len(arg_types))
105+
106+
return names, arg_types, ret_type
107+
108+
109+
def verify_arguments(function, require_strong_typing=False):
110+
"""Verify that the function is correctly defined."""
111+
if sys.version_info[0] == 2:
112+
verify_func = verify_arguments_getargspec
113+
else:
114+
verify_func = verify_arguments_signature
115+
116+
names, arg_types, ret_type = verify_func(function, require_strong_typing)
117+
if len(arg_types) != len(names):
118+
raise ValueError(
119+
"Number of argument types ({}) differs from the number of "
120+
"parameters ({}) in function {}".format(
121+
len(arg_types), len(names), function.__name__))
26122

123+
arg_types = dict(zip(names, arg_types))
27124

28-
def generate_introspection_xml(cls):
125+
return arg_types, ret_type
126+
127+
128+
def generate_introspection_xml(cls, require_strong_typing=False):
29129
"""Generate introspection XML for the given class."""
30130
def get_interface(entry):
31131
"""Get the interface XML element for the given member."""
@@ -58,6 +158,26 @@ def valid_member(member):
58158
if isinstance(value, property):
59159
entry = ElementTree.SubElement(
60160
get_interface(value.fget), "property")
161+
if sys.version_info[0] == 3:
162+
signature = inspect.signature(value.fget)
163+
prop_type = signature.return_annotation
164+
if prop_type is signature.empty:
165+
prop_type = None
166+
elif hasattr(function, "prop_type"):
167+
raise ValueError(
168+
"Annotations and explicit return type are used "
169+
"together in method '{}'".format(function.__name__))
170+
else:
171+
prop_type = None
172+
if prop_type is None and hasattr(value.fget, "prop_type"):
173+
prop_type = value.fget.prop_type
174+
175+
if prop_type is not None:
176+
attributes["type"] = prop_type
177+
elif require_strong_typing:
178+
raise ValueError(
179+
"No type defined for property '{}'".format(name))
180+
61181
if value.fset is None:
62182
attributes["access"] = "read"
63183
else:
@@ -67,21 +187,39 @@ def valid_member(member):
67187
{"name": PROPERTY_EMITS_SIGNAL, "value": "true"})
68188
attributes["access"] = "readwrite"
69189
elif ismethod(value):
70-
args = verify_arguments(value)
190+
arg_types, ret_type = verify_arguments(value)
71191
entry = ElementTree.SubElement(get_interface(value), "method")
72192
# Ignore the first parameter (which is self and not public)
73-
for arg in args[1:]:
193+
for arg, arg_type in arg_types.items():
74194
attrib = {"name": arg, "direction": "in"}
195+
if arg_type is not None:
196+
attrib["type"] = arg_type
75197
ElementTree.SubElement(entry, "arg", attrib)
198+
if ret_type is not None:
199+
ElementTree.SubElement(
200+
entry, "arg",
201+
{"name": "return", "direction": "out", "type": ret_type})
76202

77203
entry.attrib = attributes
78204
return ElementTree.tostring(root)
79205

80206

81207
def attach_introspection_xml(cls):
82-
"""Generate and add introspection data to the class and return it."""
83-
cls.dbus = generate_introspection_xml(cls)
84-
return cls
208+
"""
209+
Generate and add introspection data to the class and return it.
210+
211+
If used as a decorator without a parameter it won't require strong typing.
212+
If the parameter is True or False, it'll require it depending ot it.
213+
"""
214+
def decorate(cls):
215+
cls.dbus = generate_introspection_xml(cls, require_strong_typing)
216+
return cls
217+
if cls is True or cls is False:
218+
require_strong_typing = cls
219+
return decorate
220+
else:
221+
require_strong_typing = False
222+
return decorate(cls)
85223

86224

87225
def signalled(prop):

tests/run.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ PYTHON=${1:-python}
1818
"$PYTHON" -m pydbus.tests.context
1919
"$PYTHON" -m pydbus.tests.identifier
2020
"$PYTHON" -m pydbus.tests.xml_generator
21+
"$PYTHON" -m pydbus.tests.strong_typing
2122
if [ "$2" != "dontpublish" ]
2223
then
2324
"$PYTHON" -m pydbus.tests.publish

0 commit comments

Comments
 (0)