From e65b269d101ac5fb687a1c9981596ea3ffd1baa3 Mon Sep 17 00:00:00 2001 From: Marko Kohtala Date: Sun, 11 Jan 2015 22:27:26 +0200 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 | 102 ++++++++++++++++++++++++++ dbus/exceptions.py | 24 +++++++ dbus/service.py | 171 ++++++++++++++++++++++++++++++++++++++------ examples/example-client.py | 21 ++++-- examples/example-service.py | 22 +++++- 5 files changed, 309 insertions(+), 31 deletions(-) diff --git a/dbus/decorators.py b/dbus/decorators.py index b164582..e04fd8a 100644 --- a/dbus/decorators.py +++ b/dbus/decorators.py @@ -343,3 +343,105 @@ 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`. + + :Since: 1.3.0 + """ + + def __init__(self, dbus_interface, signature, + 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. It must be a single complete type, + i.e. something that can be be suitable to be placed + 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 or None + Getter function taking the instance from which to read the + property. + + `fset` : func or None + 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. + """ + validate_interface_name(dbus_interface) + self._dbus_interface = dbus_interface + + # Keep the given name for later assignment of setter + 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 is not None and not isinstance(property_name, str): + if not is_py2 or not isinstance(property_name, unicode): + raise TypeError("Invalid property name: '%s'" % 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 + + if emits_changed_signal not in (None, True, False, 'invalidates'): + raise ValueError("emits_changed_signal invalid value: '%s'" % + emits_changed_signal) + 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/exceptions.py b/dbus/exceptions.py index 0930425..23d5bb2 100644 --- a/dbus/exceptions.py +++ b/dbus/exceptions.py @@ -118,6 +118,14 @@ class IntrospectionParserException(DBusException): def __init__(self, msg=''): DBusException.__init__(self, "Error parsing introspect data: %s"%msg) +class UnknownInterfaceException(DBusException): + + include_traceback = True + _dbus_error_name = 'org.freedesktop.DBus.Error.UnknownInterface' + + def __init__(self, interface): + DBusException.__init__(self, "Unknown interface: %s" % interface) + class UnknownMethodException(DBusException): include_traceback = True @@ -126,6 +134,22 @@ class UnknownMethodException(DBusException): def __init__(self, method): DBusException.__init__(self, "Unknown method: %s"%method) +class UnknownPropertyException(DBusException): + + include_traceback = True + _dbus_error_name = 'org.freedesktop.DBus.Error.UnknownProperty' + + def __init__(self, property): + DBusException.__init__(self, "Unknown property: %s" % property) + +class PropertyReadOnlyException(DBusException): + + include_traceback = True + _dbus_error_name = 'org.freedesktop.DBus.Error.PropertyReadOnly' + + def __init__(self, property): + DBusException.__init__(self, "Property is read only: %s" % property) + class NameExistsException(DBusException): include_traceback = True diff --git a/dbus/service.py b/dbus/service.py index b1fc21d..4d5fcec 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,11 +34,14 @@ from collections import Sequence import _dbus_bindings from dbus import ( - INTROSPECTABLE_IFACE, ObjectPath, SessionBus, Signature, Struct, - validate_bus_name, validate_object_path) -from dbus.decorators import method, signal + 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.exceptions import ( - DBusException, NameExistsException, UnknownMethodException) + DBusException, NameExistsException, UnknownInterfaceException, + UnknownMethodException, UnknownPropertyException, + PropertyReadOnlyException) from dbus.lowlevel import ErrorMessage, MethodReturnMessage, MethodCallMessage from dbus.proxies import LOCAL_PATH from dbus._compat import is_py2 @@ -297,20 +300,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 +328,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 +378,110 @@ class InterfaceType(type): return reflection_data + def _reflect_on_property(cls, descriptor): + signature = descriptor._dbus_signature + + if descriptor.fget: + if descriptor.fset: + access = "readwrite" + else: + access = "read" + elif descriptor.fset: + access = "write" + else: + return "" + reflection_data = '