From 7adbd73d74fba968608c4eb9b378d2c6d189215e Mon Sep 17 00:00:00 2001 From: Fabian Neundorf Date: Tue, 25 Oct 2016 09:50:39 +0000 Subject: [PATCH 1/3] Support `signal` class to be a decorator If the `signal` class is used as a decorator it'll use the decorated function as the name and also cache the decorated method. --- pydbus/generic.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pydbus/generic.py b/pydbus/generic.py index abeb7ce..43d1298 100644 --- a/pydbus/generic.py +++ b/pydbus/generic.py @@ -76,10 +76,16 @@ class A: - they will be forwarded to all subscribed callbacks. """ - def __init__(self): + def __init__(self, method=None): + # if used as a decorator, method is defined + self.method = method self.map = {} - self.__qualname__ = "" # function uses ;) self.__doc__ = "Signal." + if method is None: + self.__qualname__ = "" # function uses ;) + else: + self.__qualname__ = "signal '{}'".format(method.__name__) + self.__name__ = method.__name__ def connect(self, object, callback): """Subscribe to the signal.""" From 281640381392f3823f91af3fef24e636a7bdaf89 Mon Sep 17 00:00:00 2001 From: Fabian Neundorf Date: Tue, 25 Oct 2016 11:29:45 +0000 Subject: [PATCH 2/3] Automatically generate XML introspection data It enables to automatically inspect a class and generate XML introspection data by looking through each method, signal and property. As the types are not defined it won't add them to the data. --- doc/tutorial.rst | 18 ++---- pydbus/tests/xml_generator.py | 117 ++++++++++++++++++++++++++++++++++ pydbus/xml_generator.py | 113 ++++++++++++++++++++++++++++++++ tests/run.sh | 1 + 4 files changed, 235 insertions(+), 14 deletions(-) create mode 100644 pydbus/tests/xml_generator.py create mode 100644 pydbus/xml_generator.py diff --git a/doc/tutorial.rst b/doc/tutorial.rst index e1d97c0..6243ac3 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -238,21 +238,11 @@ To prepare a class for exporting on the Bus, provide the dbus introspection XML in a ''dbus'' class property or in its ''docstring''. For example:: from pydbus.generic import signal + from pydbus.xml_generator import interface, signalled, attach_introspection_xml + @attach_introspection_xml + @interface("net.lew21.pydbus.TutorialExample") class Example(object): - """ - - - - - - - - - - - - """ def EchoString(self, s): """returns whatever is passed to it""" @@ -265,7 +255,7 @@ in a ''dbus'' class property or in its ''docstring''. For example:: def SomeProperty(self): return self._someProperty - @SomeProperty.setter + @signalled(SomeProperty) def SomeProperty(self, value): self._someProperty = value self.PropertiesChanged("net.lew21.pydbus.TutorialExample", {"SomeProperty": self.SomeProperty}, []) diff --git a/pydbus/tests/xml_generator.py b/pydbus/tests/xml_generator.py new file mode 100644 index 0000000..627577d --- /dev/null +++ b/pydbus/tests/xml_generator.py @@ -0,0 +1,117 @@ +from pydbus import xml_generator +from pydbus.generic import signal + + +@xml_generator.attach_introspection_xml +@xml_generator.interface("net.lvht.Foo1") +class Example(object): + + def __init__(self): + self._rw = 42 + + def OneParamReturn(self, parameter): + return 42 + + @xml_generator.emits_changed_signal + @property + def RwProperty(self): + return self._rw + + @RwProperty.setter + def RwProperty(self, value): + self._rw = value + + +@xml_generator.attach_introspection_xml +@xml_generator.interface("net.lvht.Foolback") +class MultiInterface(object): + + def MethodFoolback(self): + pass + + @xml_generator.interface("net.lvht.Barface") + def MethodBarface(self): + pass + + @signal + def SignalFoolback(self): + pass + + @xml_generator.interface("net.lvht.Barface") + @signal + def SignalBarface(self): + pass + + +def test_get_arguments(): + def nothing(self): + pass + + def arguments(self, arg1, arg2): + pass + + def ctx_argument(self, arg, dbus_context): + pass + + assert xml_generator.get_arguments(nothing) == [] + assert xml_generator.get_arguments(arguments) == ["arg1", "arg2"] + assert xml_generator.get_arguments(ctx_argument) == ["arg"] + + +def test_valid(): + assert not hasattr(Example.OneParamReturn, "dbus_interface") + + assert not hasattr(Example.RwProperty, "dbus_interface") + assert isinstance(Example.RwProperty, property) + assert Example.RwProperty.fget.causes_signal is True + assert Example.RwProperty.fset is not None + + assert Example.dbus == b'' + + +def test_multiple_interfaces(): + assert not hasattr(MultiInterface.MethodFoolback, "dbus_interface") + assert MultiInterface.MethodBarface.dbus_interface == "net.lvht.Barface" + assert not hasattr(MultiInterface.SignalFoolback, "dbus_interface") + assert MultiInterface.SignalBarface.dbus_interface == "net.lvht.Barface" + + assert MultiInterface.dbus == b'' + + +def test_invalid_function(): + """Test what happens if to many or to few types are defined in methods.""" + def Dummy(self, param=None): + pass + + try: + xml_generator.get_arguments(Dummy) + assert False + except ValueError as e: + assert str(e) == "Default values are not allowed for method 'Dummy'" + + E_NO_VARGS = ( + "Variable arguments arguments are not allowed for method 'Dummy'") + + def Dummy(self, *vargs): + pass + + try: + xml_generator.get_arguments(Dummy) + assert False + except ValueError as e: + assert str(e) == E_NO_VARGS + + def Dummy(self, **kwargs): + pass + + try: + xml_generator.get_arguments(Dummy) + assert False + except ValueError as e: + assert str(e) == E_NO_VARGS + + +test_get_arguments() +test_valid() +test_multiple_interfaces() +test_invalid_function() diff --git a/pydbus/xml_generator.py b/pydbus/xml_generator.py new file mode 100644 index 0000000..83000cb --- /dev/null +++ b/pydbus/xml_generator.py @@ -0,0 +1,113 @@ +"""Automatic XML documentation generator.""" +import inspect +import sys + +from xml.etree import ElementTree + +from pydbus.generic import signal + + +PROPERTY_EMITS_SIGNAL = "org.freedesktop.DBus.Property.EmitsChangedSignal" + + +# Python 2 treats them as methods, Python 3 as functions +ismethod = inspect.ismethod if sys.version_info[0] == 2 else inspect.isfunction + + +def get_arguments(function): + """Verify that the function is correctly defined.""" + args, vargs, kwargs, defaults = inspect.getargspec(function) + # Do not include 'dbus_*' parameters which have a special meaning + args = [a for a in args[1:] if not a.startswith("dbus_")] + if defaults is not None: + raise ValueError( + "Default values are not allowed for method " + "'{}'".format(function.__name__)) + if vargs is not None or kwargs is not None: + raise ValueError( + "Variable arguments arguments are not allowed for method " + "'{}'".format(function.__name__)) + return args + + +def generate_introspection_xml(cls): + """Generate introspection XML for the given class.""" + def get_interface(entry): + """Get the interface XML element for the given member.""" + if getattr(entry, "dbus_interface", None) is None: + interface = cls.dbus_interface + if interface is None: + raise ValueError( + "No interface defined for '{}'".format(entry.__name__)) + else: + interface = entry.dbus_interface + if interface not in interfaces: + interfaces[interface] = ElementTree.SubElement( + root, "interface", {"name": interface}) + return interfaces[interface] + + def valid_member(member): + """Only select members with the correct type and name.""" + if isinstance(member, property): + member = member.fget + elif not ismethod(member) and not isinstance(member, signal): + return False + return member.__name__[0].isupper() + + def add_arguments(**base_attributes): + for arg in args: + attrib = dict(base_attributes, name=arg) + ElementTree.SubElement(entry, "arg", attrib) + + + interfaces = {} + root = ElementTree.Element("node") + for name, value in inspect.getmembers(cls, predicate=valid_member): + entry = None # in case something gets through + attributes = {"name": name} + if isinstance(value, property): + entry = ElementTree.SubElement( + get_interface(value.fget), "property") + if value.fset is None: + attributes["access"] = "read" + else: + attributes["access"] = "readwrite" + if getattr(value.fget, "causes_signal", False) is True: + ElementTree.SubElement( + entry, "annotation", + {"name": PROPERTY_EMITS_SIGNAL, "value": "true"}) + elif isinstance(value, signal): + if hasattr(value, "method"): + args = get_arguments(value.method) + else: + args = tuple() + + entry = ElementTree.SubElement(get_interface(value), "signal") + add_arguments() + elif ismethod(value): + args = get_arguments(value) + entry = ElementTree.SubElement(get_interface(value), "method") + add_arguments(direction="in") + + entry.attrib = attributes + return ElementTree.tostring(root) + + +def attach_introspection_xml(cls): + """Generate and add introspection data to the class and return it.""" + cls.dbus = generate_introspection_xml(cls) + return cls + + +def emits_changed_signal(prop): + """Decorate a property to emit a changing signal.""" + prop.fget.causes_signal = True + return prop + + +def interface(name): + """Define an interface for a method, property or class.""" + def decorate(obj): + obj.dbus_interface = name + return obj + return decorate diff --git a/tests/run.sh b/tests/run.sh index 436c840..96449e3 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -17,6 +17,7 @@ PYTHON=${1:-python} "$PYTHON" -m pydbus.tests.context "$PYTHON" -m pydbus.tests.identifier +"$PYTHON" -m pydbus.tests.xml_generator if [ "$2" != "dontpublish" ] then "$PYTHON" -m pydbus.tests.publish From c8de073e00c2f99652dc18f5e8dbb66220d3b2eb Mon Sep 17 00:00:00 2001 From: Fabian Neundorf Date: Tue, 25 Oct 2016 11:28:03 +0000 Subject: [PATCH 3/3] 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. --- doc/tutorial.rst | 9 +- pydbus/strong_typing.py | 36 +++++++ pydbus/tests/strong_typing.py | 47 +++++++++ pydbus/tests/xml_generator.py | 87 +++++++++++++++-- pydbus/xml_generator.py | 174 +++++++++++++++++++++++++++++++--- tests/run.sh | 1 + 6 files changed, 334 insertions(+), 20 deletions(-) create mode 100644 pydbus/strong_typing.py create mode 100644 pydbus/tests/strong_typing.py diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 6243ac3..1036b5b 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -238,12 +238,14 @@ To prepare a class for exporting on the Bus, provide the dbus introspection XML in a ''dbus'' class property or in its ''docstring''. For example:: from pydbus.generic import signal - from pydbus.xml_generator import interface, signalled, attach_introspection_xml + from pydbus.strong_typing import typed_method, typed_property + from pydbus.xml_generator import interface, emits_changed_signal, attach_introspection_xml @attach_introspection_xml @interface("net.lew21.pydbus.TutorialExample") class Example(object): + @typed_method(("s", ), "s") def EchoString(self, s): """returns whatever is passed to it""" return s @@ -251,11 +253,12 @@ in a ''dbus'' class property or in its ''docstring''. For example:: def __init__(self): self._someProperty = "initial value" - @property + @emits_changed_signal + @typed_property("s") def SomeProperty(self): return self._someProperty - @signalled(SomeProperty) + @SomeProperty.setter def SomeProperty(self, value): self._someProperty = value self.PropertiesChanged("net.lew21.pydbus.TutorialExample", {"SomeProperty": self.SomeProperty}, []) diff --git a/pydbus/strong_typing.py b/pydbus/strong_typing.py new file mode 100644 index 0000000..c6f0d3a --- /dev/null +++ b/pydbus/strong_typing.py @@ -0,0 +1,36 @@ +"""Decorators for methods and properties to strongly typed the values.""" +import inspect + +from pydbus.xml_generator import get_arguments + + +def typed_property(value_type): + """ + Decorate a function as a dbus property getter. + + It alreay makes the method a property so another `@property` decorator may + not be used. + """ + def decorate(func): + func.prop_type = value_type + return property(func) + return decorate + + +def typed_method(argument_types, return_type): + """ + Decorate a function as a dbus method. + + Parameters + ---------- + argument_types : tuple + Required argument types for each argument except the first + return_type : string + Type of the returned value, must be None if it returns nothing + """ + def decorate(func): + func.arg_types = argument_types + func.ret_type = return_type + get_arguments(func) + return func + return decorate diff --git a/pydbus/tests/strong_typing.py b/pydbus/tests/strong_typing.py new file mode 100644 index 0000000..65a9b83 --- /dev/null +++ b/pydbus/tests/strong_typing.py @@ -0,0 +1,47 @@ +from pydbus.generic import signal +from pydbus.strong_typing import typed_method, typed_property + + +def test_signal(): + @signal + @typed_method(("s", ), None) + def dummy(self, parameter): + pass + + assert hasattr(dummy, 'method') + assert dummy.method.arg_types == ("s", ) + assert dummy.method.ret_type is None + + +def test_count_off(): + """Test what happens if to many or to few types are defined in methods.""" + try: + @typed_method(("s", "i", "o"), None) + def dummy(self, parameter): + pass + + assert False + except ValueError as e: + assert str(e) == "Number of argument types (3) differs from the number of parameters (1) in function 'dummy'" + + try: + @typed_method(("s", "i"), "o") + def dummy(self, parameter): + pass + + assert False + except ValueError as e: + assert str(e) == "Number of argument types (2) differs from the number of parameters (1) in function 'dummy'" + + try: + @typed_method(tuple(), None) + def dummy(self, parameter): + pass + + assert False + except ValueError as e: + assert str(e) == "Number of argument types (0) differs from the number of parameters (1) in function 'dummy'" + + +test_signal() +test_count_off() diff --git a/pydbus/tests/xml_generator.py b/pydbus/tests/xml_generator.py index 627577d..f99971f 100644 --- a/pydbus/tests/xml_generator.py +++ b/pydbus/tests/xml_generator.py @@ -1,5 +1,8 @@ +from sys import version_info + from pydbus import xml_generator from pydbus.generic import signal +from pydbus.strong_typing import typed_method, typed_property @xml_generator.attach_introspection_xml @@ -9,11 +12,20 @@ class Example(object): def __init__(self): self._rw = 42 + @typed_method(("s", ), "i") def OneParamReturn(self, parameter): return 42 + @typed_method(("s", ), None) + def OneParamNoReturn(self, parameter): + pass + + @typed_property("i") + def ReadProperty(self): + return 42 + @xml_generator.emits_changed_signal - @property + @typed_property("i") def RwProperty(self): return self._rw @@ -53,20 +65,43 @@ def arguments(self, arg1, arg2): def ctx_argument(self, arg, dbus_context): pass - assert xml_generator.get_arguments(nothing) == [] - assert xml_generator.get_arguments(arguments) == ["arg1", "arg2"] - assert xml_generator.get_arguments(ctx_argument) == ["arg"] + @typed_method(tuple(), None) + def typed_nothing(self): + pass + + @typed_method(("s", "i"), None) + def typed_arguments(self, arg1, arg2): + pass + + assert xml_generator.get_arguments(nothing) == (tuple(), None) + assert xml_generator.get_arguments(arguments) == ((("arg1", None), ("arg2", None)), None) + assert xml_generator.get_arguments(ctx_argument) == ((("arg", None), ), None) + + assert xml_generator.get_arguments(typed_nothing) == (tuple(), None) + assert xml_generator.get_arguments(typed_arguments) == ((("arg1", "s"), ("arg2", "i")), None) def test_valid(): assert not hasattr(Example.OneParamReturn, "dbus_interface") + assert Example.OneParamReturn.arg_types == ("s", ) + assert Example.OneParamReturn.ret_type == "i" + + assert not hasattr(Example.OneParamNoReturn, "dbus_interface") + assert Example.OneParamNoReturn.arg_types == ("s", ) + assert Example.OneParamNoReturn.ret_type is None + + assert not hasattr(Example.ReadProperty, "dbus_interface") + assert isinstance(Example.ReadProperty, property) + assert Example.ReadProperty.fget.prop_type == "i" + assert Example.ReadProperty.fset is None assert not hasattr(Example.RwProperty, "dbus_interface") assert isinstance(Example.RwProperty, property) assert Example.RwProperty.fget.causes_signal is True + assert Example.RwProperty.fget.prop_type == "i" assert Example.RwProperty.fset is not None - assert Example.dbus == b'' + assert Example.dbus == b'' def test_multiple_interfaces(): @@ -89,8 +124,13 @@ def Dummy(self, param=None): except ValueError as e: assert str(e) == "Default values are not allowed for method 'Dummy'" - E_NO_VARGS = ( + if version_info[0] == 2: + E_NO_VARGS = ( "Variable arguments arguments are not allowed for method 'Dummy'") + else: + E_NO_VARGS = E_NO_KWARGS = ( + "Variable arguments or keyword only arguments are not allowed for " + "method 'Dummy'") def Dummy(self, *vargs): pass @@ -111,7 +151,42 @@ def Dummy(self, **kwargs): assert str(e) == E_NO_VARGS +def test_require_strong_typing(): + try: + @xml_generator.attach_introspection_xml(True) + @xml_generator.interface("net.lvht.Foo1") + class Example(object): + + def Dummy(self, param): + pass + except ValueError as e: + assert str(e) == "No argument types defined for method 'Dummy'" + + @xml_generator.attach_introspection_xml(True) + @xml_generator.interface("net.lvht.Foo1") + class RequiredExample(object): + + @typed_method(("s", ), None) + def Dummy(self, param): + pass + + assert RequiredExample.Dummy.arg_types == ("s", ) + assert RequiredExample.Dummy.ret_type is None + + @xml_generator.attach_introspection_xml(False) + @xml_generator.interface("net.lvht.Foo1") + class OptionalExample(object): + + @typed_method(("s", ), None) + def Dummy(self, param): + pass + + assert OptionalExample.dbus == RequiredExample.dbus + assert OptionalExample is not RequiredExample + + test_get_arguments() test_valid() test_multiple_interfaces() test_invalid_function() +test_require_strong_typing() diff --git a/pydbus/xml_generator.py b/pydbus/xml_generator.py index 83000cb..da31aa5 100644 --- a/pydbus/xml_generator.py +++ b/pydbus/xml_generator.py @@ -2,6 +2,7 @@ import inspect import sys +from itertools import islice from xml.etree import ElementTree from pydbus.generic import signal @@ -14,11 +15,41 @@ ismethod = inspect.ismethod if sys.version_info[0] == 2 else inspect.isfunction -def get_arguments(function): - """Verify that the function is correctly defined.""" +def extract_membered_types(function, require_strong_typing, arg_count): + has_arg_types = hasattr(function, "arg_types") + if has_arg_types: + arg_types = function.arg_types + elif require_strong_typing: + raise ValueError( + "No argument types defined for method " + "'{}'".format(function.__name__)) + else: + arg_types = (None, ) * arg_count + + if hasattr(function, "ret_type"): + if not has_arg_types: + raise ValueError( + "Only explicit return type defined but no explicit " + "argument types for method '{}'".format(function.__name__)) + ret_type = function.ret_type + elif has_arg_types: + raise ValueError( + "Only explicit argument types defined but no explicit return " + "for method '{}'".format(function.__name__)) + else: + ret_type = None + + return arg_types, ret_type + + +def get_arguments_getargspec(function, require_strong_typing): + """Verify arguments using the getargspec function.""" args, vargs, kwargs, defaults = inspect.getargspec(function) # Do not include 'dbus_*' parameters which have a special meaning args = [a for a in args[1:] if not a.startswith("dbus_")] + arg_types, ret_type = extract_membered_types( + function, require_strong_typing, len(args)) + if defaults is not None: raise ValueError( "Default values are not allowed for method " @@ -27,10 +58,84 @@ def get_arguments(function): raise ValueError( "Variable arguments arguments are not allowed for method " "'{}'".format(function.__name__)) - return args + return args, arg_types, ret_type + + +def get_arguments_signature(function, require_strong_typing): + """Verify arguments using the Signature class in Python 3.""" + signature = inspect.signature(function) + # For whatever reason OrderedDict does not actually support slicing + # Also do not include 'dbus_*' parameters which have a special meaning + parameters = [ + p for p in islice(signature.parameters.values(), 1, None) + if not p.name.startswith("dbus_")] + if not all(param.default is param.empty for param in parameters): + raise ValueError( + "Default values are not allowed for method " + "'{}'".format(function.__name__)) + if not all(param.kind == param.POSITIONAL_OR_KEYWORD + for param in parameters): + raise ValueError( + "Variable arguments or keyword only arguments are not allowed for " + "method '{}'".format(function.__name__)) + + names = [p.name for p in parameters] + arg_types = [ + param.annotation for param in parameters + if param.annotation is not param.empty] + if arg_types and hasattr(function, "arg_types"): + raise ValueError( + "Annotations and explicit argument types are used together in " + "method '{}'".format(function.__name__)) + + ret_type = signature.return_annotation + if (ret_type is not signature.empty and + hasattr(function, "ret_type")): + raise ValueError( + "Annotations and explicit return type are used together in " + "method '{}'".format(function.__name__)) + + # Fall back to the explicit types only if there were no annotations, but + # that might be actually valid if the function returns nothing and has + # no parameters. + # So it also checks that the function has any parameter or it has either of + # the two attributes defined. + # So it won't actually raise an error if a function has no parameter and + # no annotations and no explicit types defined, because it is not possible + # to determine if a function returns something. + if (ret_type is signature.empty and not arg_types and + (parameters or hasattr(function, "arg_types") or + hasattr(function, "ret_type"))): + arg_types, ret_type = extract_membered_types( + function, require_strong_typing, len(parameters)) + + if ret_type is signature.empty: + # Instead of 'empty' we use None as each type should be strings + ret_type = None + + return names, arg_types, ret_type + + +def get_arguments(function, require_strong_typing=False): + """Verify that the function is correctly defined.""" + if sys.version_info[0] == 2: + verify_func = get_arguments_getargspec + else: + verify_func = get_arguments_signature + + names, arg_types, ret_type = verify_func(function, require_strong_typing) + if len(arg_types) != len(names): + raise ValueError( + "Number of argument types ({}) differs from the number of " + "parameters ({}) in function '{}'".format( + len(arg_types), len(names), function.__name__)) + arg_types = tuple(zip(names, arg_types)) -def generate_introspection_xml(cls): + return arg_types, ret_type + + +def generate_introspection_xml(cls, require_strong_typing=False): """Generate introspection XML for the given class.""" def get_interface(entry): """Get the interface XML element for the given member.""" @@ -55,8 +160,10 @@ def valid_member(member): return member.__name__[0].isupper() def add_arguments(**base_attributes): - for arg in args: + for arg, arg_type in arg_types: attrib = dict(base_attributes, name=arg) + if arg_type is not None: + attrib["type"] = arg_type ElementTree.SubElement(entry, "arg", attrib) @@ -68,6 +175,26 @@ def add_arguments(**base_attributes): if isinstance(value, property): entry = ElementTree.SubElement( get_interface(value.fget), "property") + if sys.version_info[0] == 3: + signature = inspect.signature(value.fget) + prop_type = signature.return_annotation + if prop_type is signature.empty: + prop_type = None + elif hasattr(function, "prop_type"): + raise ValueError( + "Annotations and explicit return type are used " + "together in method '{}'".format(function.__name__)) + else: + prop_type = None + if prop_type is None and hasattr(value.fget, "prop_type"): + prop_type = value.fget.prop_type + + if prop_type is not None: + attributes["type"] = prop_type + elif require_strong_typing: + raise ValueError( + "No type defined for property '{}'".format(name)) + if value.fset is None: attributes["access"] = "read" else: @@ -78,25 +205,50 @@ def add_arguments(**base_attributes): {"name": PROPERTY_EMITS_SIGNAL, "value": "true"}) elif isinstance(value, signal): if hasattr(value, "method"): - args = get_arguments(value.method) + arg_types, ret_type = get_arguments( + value.method, require_strong_typing) + if ret_type is not None: + raise ValueError( + "Return type defined for signal " + "'{}'".format(value.method.__name__)) + elif require_strong_typing: + raise ValueError( + "No argument definitions for signal " + "'{}'".format(value.method.__name__)) else: - args = tuple() + arg_types = tuple() entry = ElementTree.SubElement(get_interface(value), "signal") add_arguments() elif ismethod(value): - args = get_arguments(value) + arg_types, ret_type = get_arguments(value, require_strong_typing) entry = ElementTree.SubElement(get_interface(value), "method") add_arguments(direction="in") + if ret_type is not None: + ElementTree.SubElement( + entry, "arg", + {"name": "return", "direction": "out", "type": ret_type}) entry.attrib = attributes return ElementTree.tostring(root) def attach_introspection_xml(cls): - """Generate and add introspection data to the class and return it.""" - cls.dbus = generate_introspection_xml(cls) - return cls + """ + Generate and add introspection data to the class and return it. + + If used as a decorator without a parameter it won't require strong typing. + If the parameter is True or False, it'll require it depending ot it. + """ + def decorate(cls): + cls.dbus = generate_introspection_xml(cls, require_strong_typing) + return cls + if cls is True or cls is False: + require_strong_typing = cls + return decorate + else: + require_strong_typing = False + return decorate(cls) def emits_changed_signal(prop): diff --git a/tests/run.sh b/tests/run.sh index 96449e3..ae22bce 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -18,6 +18,7 @@ PYTHON=${1:-python} "$PYTHON" -m pydbus.tests.context "$PYTHON" -m pydbus.tests.identifier "$PYTHON" -m pydbus.tests.xml_generator +"$PYTHON" -m pydbus.tests.strong_typing if [ "$2" != "dontpublish" ] then "$PYTHON" -m pydbus.tests.publish