From b5f097c622ac89df3ccb97931decd45bae3c4bd2 Mon Sep 17 00:00:00 2001 From: Marko Kohtala Date: Tue, 13 May 2014 22:06:07 +0300 Subject: [PATCH] Implement dbus.service.property decorator and PropertiesInterface This adds dbus server side support for properties using a decorator. The decorator automagically adds PropertiesInterface to the object. It also simplifies the _dbus_class_table shared by all derived classes and containing them to a _dbus_interface_table on each derived class. --- dbus/decorators.py | 95 ++++++++++++++++++++++++++ dbus/service.py | 163 ++++++++++++++++++++++++++++++++++++++------ examples/example-client.py | 18 +++-- examples/example-service.py | 14 +++- 4 files changed, 261 insertions(+), 29 deletions(-) diff --git a/dbus/decorators.py b/dbus/decorators.py index b164582..d41869a 100644 --- a/dbus/decorators.py +++ b/dbus/decorators.py @@ -343,3 +343,98 @@ def signal(dbus_interface, signature=None, path_keyword=None, return emit_signal return decorator + + +class property(object): + """A decorator used to mark properties of a `dbus.service.Object`. + """ + + def __init__(self, dbus_interface=None, signature=None, + property_name=None, emits_changed_signal=None, + fget=None, fset=None, doc=None): + """Initialize the decorator used to mark properties of a + `dbus.service.Object`. + + :Parameters: + `dbus_interface` : str + The D-Bus interface owning the property + + `signature` : str + The signature of the property in the usual D-Bus notation. The + signature must be suitable to be carried in a variant. + + `property_name` : str + A name for the property. Defaults to the name of the getter or + setter function. + + `emits_changed_signal` : True, False, "invalidates", or None + Tells for introspection if the object emits PropertiesChanged + signal. + + `fget` : func + Getter function taking the instance from which to read the + property. + + `fset` : func + Setter function taking the instance to which set the property + and the property value. + + `doc` : str + Documentation string for the property. Defaults to documentation + string of getter function. + + :Since: 1.3.0 + """ + validate_interface_name(dbus_interface) + self._dbus_interface = dbus_interface + + self._init_property_name = property_name + if property_name is None: + if fget is not None: + property_name = fget.__name__ + elif fset is not None: + property_name = fset.__name__ + if property_name: + validate_member_name(property_name) + self.__name__ = property_name + + self._init_doc = doc + if doc is None and fget is not None: + doc = getattr(fget, "__doc__", None) + self.fget = fget + self.fset = fset + self.__doc__ = doc + + self._emits_changed_signal = emits_changed_signal + if len(tuple(Signature(signature))) != 1: + raise ValueError('signature must have only one item') + self._dbus_signature = signature + + def __get__(self, inst, type=None): + if inst is None: + return self + if self.fget is None: + raise AttributeError("unreadable attribute") + return self.fget(inst) + + def __set__(self, inst, value): + if self.fset is None: + raise AttributeError("can't set attribute") + self.fset(inst, value) + + def __call__(self, fget): + return self.getter(fget) + + def _copy(self, fget=None, fset=None): + return property(dbus_interface=self._dbus_interface, + signature=self._dbus_signature, + property_name=self._init_property_name, + emits_changed_signal=self._emits_changed_signal, + fget=fget or self.fget, fset=fset or self.fset, + doc=self._init_doc) + + def getter(self, fget): + return self._copy(fget=fget) + + def setter(self, fset): + return self._copy(fset=fset) diff --git a/dbus/service.py b/dbus/service.py index b1fc21d..fdb3ee4 100644 --- a/dbus/service.py +++ b/dbus/service.py @@ -23,7 +23,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -__all__ = ('BusName', 'Object', 'method', 'signal') +__all__ = ('BusName', 'Object', 'PropertiesInterface', 'method', 'property', 'signal') __docformat__ = 'restructuredtext' import sys @@ -34,8 +34,10 @@ from collections import Sequence import _dbus_bindings from dbus import ( - INTROSPECTABLE_IFACE, ObjectPath, SessionBus, Signature, Struct, - validate_bus_name, validate_object_path) + INTROSPECTABLE_IFACE, ObjectPath, PROPERTIES_IFACE, SessionBus, Signature, + Struct, validate_bus_name, validate_object_path) +_builtin_property = property +from dbus.decorators import method, signal, property from dbus.decorators import method, signal from dbus.exceptions import ( DBusException, NameExistsException, UnknownMethodException) @@ -297,20 +299,25 @@ def _method_reply_error(connection, message, exception): class InterfaceType(type): - def __init__(cls, name, bases, dct): - # these attributes are shared between all instances of the Interface - # object, so this has to be a dictionary that maps class names to - # the per-class introspection/interface data - class_table = getattr(cls, '_dbus_class_table', {}) - cls._dbus_class_table = class_table - interface_table = class_table[cls.__module__ + '.' + name] = {} + def __new__(cls, name, bases, dct): + # Properties require the PropertiesInterface base. + for func in dct.values(): + if isinstance(func, property): + for b in bases: + if issubclass(b, PropertiesInterface): + break + else: + bases += (PropertiesInterface,) + break + + interface_table = dct.setdefault('_dbus_interface_table', {}) # merge all the name -> method tables for all the interfaces # implemented by our base classes into our own for b in bases: - base_name = b.__module__ + '.' + b.__name__ - if getattr(b, '_dbus_class_table', False): - for (interface, method_table) in class_table[base_name].items(): + base_interface_table = getattr(b, '_dbus_interface_table', False) + if base_interface_table: + for (interface, method_table) in base_interface_table.items(): our_method_table = interface_table.setdefault(interface, {}) our_method_table.update(method_table) @@ -320,9 +327,9 @@ class InterfaceType(type): method_table = interface_table.setdefault(func._dbus_interface, {}) method_table[func.__name__] = func - super(InterfaceType, cls).__init__(name, bases, dct) + return type.__new__(cls, name, bases, dct) - # methods are different to signals, so we have two functions... :) + # methods are different to signals and properties, so we have three functions... :) def _reflect_on_method(cls, func): args = func._dbus_args @@ -370,12 +377,107 @@ class InterfaceType(type): return reflection_data + def _reflect_on_property(cls, descriptor): + signature = descriptor._dbus_signature + if signature is None: + signature = 'v' + + if descriptor.fget: + if descriptor.fset: + access = "readwrite" + else: + access = "read" + elif descriptor.fset: + access = "write" + else: + return "" + reflection_data = '