|
| 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 |
0 commit comments