Skip to content

Commit 45784ba

Browse files
committed
Support automatic XML introspection generation
This enables a class to generate automatic XML introspection data.
1 parent 4397404 commit 45784ba

File tree

4 files changed

+281
-18
lines changed

4 files changed

+281
-18
lines changed

doc/tutorial.rst

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -237,40 +237,27 @@ Class preparation
237237
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

240+
from pydbus.introspection import dbus_interface, dbus_method, dbus_property, signalled
240241
from pydbus.generic import signal
241242

243+
@dbus_interface("net.lew21.pydbus.TutorialExample")
242244
class Example(object):
243-
"""
244-
<node>
245-
<interface name='net.lew21.pydbus.TutorialExample'>
246-
<method name='EchoString'>
247-
<arg type='s' name='a' direction='in'/>
248-
<arg type='s' name='response' direction='out'/>
249-
</method>
250-
<property name="SomeProperty" type="s" access="readwrite">
251-
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
252-
</property>
253-
</interface>
254-
</node>
255-
"""
256245

246+
@dbus_method("s", "s")
257247
def EchoString(self, s):
258248
"""returns whatever is passed to it"""
259249
return s
260250

261251
def __init__(self):
262252
self._someProperty = "initial value"
263253

264-
@property
254+
@dbus_property("s")
265255
def SomeProperty(self):
266256
return self._someProperty
267257

268-
@SomeProperty.setter
258+
@signalled(SomeProperty)
269259
def SomeProperty(self, value):
270260
self._someProperty = value
271-
self.PropertiesChanged("net.lew21.pydbus.TutorialExample", {"SomeProperty": self.SomeProperty}, [])
272-
273-
PropertiesChanged = signal()
274261

275262
If you don't want to put XML in a Python file, you can add XML files to your Python package and use them this way::
276263

pydbus/introspect.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
"""Automatic XML documentation generator."""
2+
import inspect
3+
import sys
4+
5+
from xml.etree import ElementTree
6+
7+
from pydbus.generic import signal
8+
9+
10+
PROPERTY_EMITS_SIGNAL = "org.freedesktop.DBus.Property.EmitsChangedSignal"
11+
12+
13+
# Python 2 treats them as methods, Python 3 as functions
14+
ismethod = inspect.ismethod if sys.version_info[0] == 2 else inspect.isfunction
15+
16+
17+
def verify_arguments(function):
18+
"""Verify that the function has enough types defined."""
19+
args, vargs, kwargs, defaults = inspect.getargspec(function)
20+
if len(function.dbus_types) + 1 < len(args):
21+
raise ValueError(
22+
"Number of dbus types ({}) does not cover all parameters ({}) in "
23+
"function {}".format(len(function.dbus_types), len(args),
24+
function.__name__))
25+
elif len(function.dbus_types) > len(args):
26+
raise ValueError(
27+
"Too many dbus types ({}) are defined for the parameters ({}) in "
28+
"function {}".format(len(function.dbus_types), len(args),
29+
function.__name__))
30+
if defaults is not None:
31+
raise ValueError("dbus methods do not allow default values")
32+
# TODO: Check if vargs or kwargs are actually allowed in dbus.
33+
if vargs is not None:
34+
raise ValueError("dbus methods do not allow variable argument functions")
35+
if kwargs is not None:
36+
raise ValueError("dbus methods do not allow variable keyword arguments")
37+
return args
38+
39+
40+
def gen_introspection(cls):
41+
"""Generate introspection XML for the given class."""
42+
def get_interface(entry):
43+
"""Get the interface XML element for the given member."""
44+
if getattr(entry, "interface", None) is None:
45+
interface = cls.dbus_interface
46+
if interface is None:
47+
raise ValueError("No interface defined for "
48+
"'{}'".format(entry.__name__))
49+
else:
50+
interface = entry.interface
51+
if interface not in interfaces:
52+
interfaces[interface] = ElementTree.SubElement(root, "interface",
53+
{"name": interface})
54+
return interfaces[interface]
55+
56+
def valid_member(member):
57+
"""Only select members with the correct type and attribute."""
58+
if isinstance(member, property):
59+
return hasattr(member.fget, "dbus_type")
60+
elif ismethod(member):
61+
return hasattr(member, "dbus_types")
62+
else:
63+
return False
64+
65+
66+
interfaces = {}
67+
root = ElementTree.Element("node")
68+
for name, value in inspect.getmembers(cls, predicate=valid_member):
69+
entry = None # in case something gets through
70+
attributes = {"name": name}
71+
if isinstance(value, property):
72+
entry = ElementTree.SubElement(get_interface(value.fget),
73+
"property")
74+
attributes["type"] = value.fget.dbus_type
75+
if value.fset is None:
76+
attributes["access"] = "read"
77+
else:
78+
if getattr(value.fset, "causes_signal", False) is True:
79+
ElementTree.SubElement(
80+
entry, "annotation",
81+
{"name": PROPERTY_EMITS_SIGNAL, "value": "true"})
82+
attributes["access"] = "readwrite"
83+
elif ismethod(value):
84+
# the dbus_types do not contain "self", so it ignores the first
85+
# parameter.
86+
# If the number of types matches the number of arguments (including
87+
# "self") the very last type is not matched to any argument, but
88+
# actually the type of the return value.
89+
args = verify_arguments(value)
90+
entry = ElementTree.SubElement(get_interface(value), "method")
91+
for arg, dbus_type in zip(args[1:], value.dbus_types):
92+
ElementTree.SubElement(
93+
entry, "arg",
94+
{"name": arg, "direction": "in", "type": dbus_type})
95+
if len(args) == len(value.dbus_types):
96+
ElementTree.SubElement(
97+
entry, "arg",
98+
{"name": "response", "direction": "out",
99+
"type": value.dbus_types[-1]})
100+
101+
entry.attrib = attributes
102+
return ElementTree.tostring(root)
103+
104+
105+
def signalled(prop):
106+
"""
107+
Decorate a function as a signalling property setter.
108+
109+
Whenever the setter is called it'll also emit a signal using the object's
110+
`PropertiesChanged` member. It will also automatically set the annotated
111+
function as the property's setter, so an additional `@....setter` decorator
112+
may not be used.
113+
"""
114+
def decorate(func):
115+
def wrapped(obj, value):
116+
func(obj, value)
117+
obj.PropertiesChanged(obj.dbus_interface,
118+
{prop.fget.__name__: prop.fget(obj)}, [])
119+
wrapped.causes_signal = True
120+
return prop.setter(wrapped)
121+
return decorate
122+
123+
124+
def dbus_property(value_type, interface=None):
125+
"""
126+
Decorate a function as a dbus property getter.
127+
128+
It alreay makes the method a property so another `@property` decorator may
129+
not be used. If the interface parameter is None it will use the interface
130+
defined by the class.
131+
"""
132+
def decorate(func):
133+
func.dbus_type = value_type
134+
func.interface = interface
135+
return property(func)
136+
return decorate
137+
138+
139+
def dbus_method(*value_types, **kwargs):
140+
"""
141+
Decorate a function as a dbus method.
142+
143+
The value types must contain one string for each argument except the first.
144+
If the method returns something it must contain an additional string for
145+
the type of the returned value.
146+
147+
The keyword argument may only contain "interface" which, if it is not None
148+
(default), defines the interface this method belongs to. If it is None, it
149+
will use the interface defined by the class.
150+
"""
151+
def decorate(func):
152+
func.dbus_types = value_types
153+
func.interface = interface
154+
verify_arguments(func)
155+
return func
156+
interface = kwargs.pop("interface", None)
157+
if kwargs:
158+
# other kwargs defined
159+
raise TypeError("pydbus.introspect.dbus_method got an unexpected keyword argument '{}'".format(next(iter(kwargs))))
160+
return decorate
161+
162+
163+
def dbus_interface(name=None, add_signal=True):
164+
"""
165+
Decorate a class as a dbus interface and generate the introspection info.
166+
167+
The interface itself is optional, but is used for every property or method
168+
which does not define their own interface.
169+
170+
If `add_signal` is set to `True`, it will add a `PropertiesChanged` member
171+
set `pydbus.generic.signal`.
172+
"""
173+
def wrap_class(cls):
174+
cls.dbus_interface = name
175+
cls.dbus = gen_introspection(cls)
176+
if add_signal:
177+
cls.PropertiesChanged = signal()
178+
return cls
179+
return wrap_class

pydbus/tests/introspect.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from pydbus import introspect
2+
from pydbus.generic import signal
3+
4+
5+
@introspect.dbus_interface("net.lvht.Foo1", True)
6+
class Example(object):
7+
8+
def __init__(self):
9+
self._rw = 42
10+
11+
@introspect.dbus_method("s", "i")
12+
def one_param_return(self, parameter):
13+
return 42
14+
15+
@introspect.dbus_method("s")
16+
def one_param_no_return(self, parameter):
17+
pass
18+
19+
@introspect.dbus_property("i")
20+
def read_property(self):
21+
return 42
22+
23+
@introspect.dbus_property("i")
24+
def rw_property(self):
25+
return self._rw
26+
27+
@introspect.signalled(rw_property)
28+
def rw_property(self, value):
29+
self._rw = value
30+
31+
32+
def test_valid():
33+
assert Example.one_param_return.dbus_types == ("s", "i")
34+
assert Example.one_param_return.interface is None
35+
36+
assert Example.one_param_no_return.dbus_types == ("s",)
37+
assert Example.one_param_no_return.interface is None
38+
39+
assert isinstance(Example.read_property, property)
40+
assert Example.read_property.fget.dbus_type == "i"
41+
assert Example.read_property.fset is None
42+
43+
assert isinstance(Example.rw_property, property)
44+
assert Example.rw_property.fget.dbus_type == "i"
45+
assert Example.rw_property.fset is not None
46+
47+
assert Example.dbus == '<node><interface name="net.lvht.Foo1"><method name="one_param_no_return"><arg direction="in" name="parameter" type="s" /></method><method name="one_param_return"><arg direction="in" name="parameter" type="s" /><arg direction="out" name="response" type="i" /></method><property access="read" name="read_property" type="i" /><property access="readwrite" name="rw_property" type="i"><annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true" /></property></interface></node>'
48+
49+
50+
def test_count_off():
51+
"""Test what happens if to many or to few types are defined in methods."""
52+
try:
53+
class Example(object):
54+
55+
@introspect.dbus_method("s", "i", "o")
56+
def dummy(self, parameter):
57+
pass
58+
59+
assert False
60+
except ValueError as e:
61+
assert e.message == "Too many dbus types (3) are defined for the parameters (2) in function dummy"
62+
63+
try:
64+
class Example(object):
65+
66+
@introspect.dbus_method()
67+
def dummy(self, parameter):
68+
pass
69+
70+
assert False
71+
except ValueError as e:
72+
assert e.message == "Number of dbus types (0) does not cover all parameters (2) in function dummy"
73+
74+
75+
def test_signalled():
76+
def dummy_signal(*a):
77+
assert len(a) == 4
78+
assert a[1] == "net.lvht.Foo1"
79+
assert a[2] == {"rw_property": expected_value}
80+
assert a[3] == []
81+
82+
expected_value = 1337
83+
# Due to signal() having defined __set__ we can't just overwrite it in the
84+
# instance itself, so we have to patch it here.
85+
original = Example.PropertiesChanged
86+
try:
87+
Example.PropertiesChanged = dummy_signal
88+
example = Example()
89+
example.rw_property = expected_value
90+
finally:
91+
Example.PropertiesChanged = original
92+
93+
94+
test_valid()
95+
test_count_off()
96+
test_signalled()

tests/run.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ PYTHON=${1:-python}
1717

1818
"$PYTHON" -m pydbus.tests.context
1919
"$PYTHON" -m pydbus.tests.identifier
20+
"$PYTHON" -m pydbus.tests.introspect
2021
if [ "$2" != "dontpublish" ]
2122
then
2223
"$PYTHON" -m pydbus.tests.publish

0 commit comments

Comments
 (0)