diff --git a/src/dbus_fast/introspection.py b/src/dbus_fast/introspection.py index 423009d..36dbb50 100644 --- a/src/dbus_fast/introspection.py +++ b/src/dbus_fast/introspection.py @@ -7,7 +7,25 @@ from .signature import SignatureType, get_signature_tree from .validators import assert_interface_name_valid, assert_member_name_valid # https://dbus.freedesktop.org/doc/dbus-specification.html#introspection-format -# TODO annotations + + +def _fetch_annotations(element: ET.Element) -> dict[str, str]: + annotations: dict[str, str] = {} + + for child in element: + if child.tag != "annotation": + continue + annotation_name = child.attrib["name"] + annotation_value = child.attrib["value"] + annotations[annotation_name] = annotation_value + + return annotations + + +def _extract_annotations(element: ET.Element, annotations: dict[str, str]) -> None: + for key, value in annotations.items(): + annotation = ET.Element("annotation", {"name": key, "value": value}) + element.append(annotation) class Arg: @@ -21,6 +39,8 @@ class Arg: :vartype type: :class:`SignatureType ` :ivar signature: The signature string of this argument. :vartype signature: str + :ivar annotations: The annotations of this arg. + :vartype annotations: dict[str, str] :raises: - :class:`InvalidMemberNameError ` - If the name of the arg is not valid. @@ -33,6 +53,7 @@ class Arg: signature: Union[SignatureType, str], direction: Optional[list[ArgDirection]] = None, name: Optional[str] = None, + annotations: Optional[dict[str, str]] = None, ): type_ = None if type(signature) is SignatureType: @@ -50,6 +71,7 @@ class Arg: self.signature = signature self.name = name self.direction = direction + self.annotations = annotations or {} def from_xml(element: ET.Element, direction: ArgDirection) -> "Arg": """Convert a :class:`xml.etree.ElementTree.Element` into a @@ -73,7 +95,9 @@ class Arg: 'a method argument must have a "type" attribute' ) - return Arg(signature, direction, name) + annotations = _fetch_annotations(element) + + return Arg(signature, direction, name, annotations) def to_xml(self) -> ET.Element: """Convert this :class:`Arg` into an :class:`xml.etree.ElementTree.Element`.""" @@ -85,6 +109,8 @@ class Arg: element.set("direction", self.direction.value) element.set("type", self.signature) + _extract_annotations(element, self.annotations) + return element @@ -97,18 +123,26 @@ class Signal: :vartype args: list(Arg) :ivar signature: The collected signature of the output arguments. :vartype signature: str + :ivar annotations: The annotations of this signal. + :vartype annotations: dict[str, str] :raises: - :class:`InvalidMemberNameError ` - If the name of the signal is not a valid member name. """ - def __init__(self, name: Optional[str], args: Optional[list[Arg]] = None): + def __init__( + self, + name: Optional[str], + args: Optional[list[Arg]] = None, + annotations: Optional[dict[str, str]] = None, + ): if name is not None: assert_member_name_valid(name) self.name = name self.args = args or [] self.signature = "".join(arg.signature for arg in self.args) + self.annotations = annotations or {} def from_xml(element): """Convert an :class:`xml.etree.ElementTree.Element` to a :class:`Signal`. @@ -132,7 +166,9 @@ class Signal: if child.tag == "arg": args.append(Arg.from_xml(child, ArgDirection.OUT)) - signal = Signal(name, args) + annotations = _fetch_annotations(element) + + signal = Signal(name, args, annotations) return signal @@ -144,6 +180,8 @@ class Signal: for arg in self.args: element.append(arg.to_xml()) + _extract_annotations(element, self.annotations) + return element @@ -160,12 +198,20 @@ class Method: :vartype in_signature: str :ivar out_signature: The collected signature string of the output arguments. :vartype out_signature: str + :ivar annotations: The annotations of this method. + :vartype annotations: dict[str, str] :raises: - :class:`InvalidMemberNameError ` - If the name of this method is not valid. """ - def __init__(self, name: str, in_args: list[Arg] = [], out_args: list[Arg] = []): + def __init__( + self, + name: str, + in_args: list[Arg] = [], + out_args: list[Arg] = [], + annotations: Optional[dict[str, str]] = None, + ): assert_member_name_valid(name) self.name = name @@ -173,6 +219,7 @@ class Method: self.out_args = out_args self.in_signature = "".join(arg.signature for arg in in_args) self.out_signature = "".join(arg.signature for arg in out_args) + self.annotations = annotations or {} def from_xml(element: ET.Element) -> "Method": """Convert an :class:`xml.etree.ElementTree.Element` to a :class:`Method`. @@ -203,7 +250,9 @@ class Method: elif direction == ArgDirection.OUT: out_args.append(arg) - return Method(name, in_args, out_args) + annotations = _fetch_annotations(element) + + return Method(name, in_args, out_args, annotations) def to_xml(self) -> ET.Element: """Convert this :class:`Method` into an :class:`xml.etree.ElementTree.Element`.""" @@ -215,6 +264,8 @@ class Method: for arg in self.out_args: element.append(arg.to_xml()) + _extract_annotations(element, self.annotations) + return element @@ -230,6 +281,8 @@ class Property: :vartype access: :class:`PropertyAccess ` :ivar type: The parsed type of this property. :vartype type: :class:`SignatureType ` + :ivar annotations: The annotations of this property. + :vartype annotations: dict[str, str] :raises: - :class:`InvalidIntrospectionError ` - If the property is not a single complete type. @@ -242,6 +295,7 @@ class Property: name: str, signature: str, access: PropertyAccess = PropertyAccess.READWRITE, + annotations: Optional[dict[str, str]] = None, validate: bool = True, ): if validate: @@ -257,6 +311,7 @@ class Property: self.signature = signature self.access = access self.type = tree.types[0] + self.annotations = annotations or {} def from_xml(element, validate: bool = True): """Convert an :class:`xml.etree.ElementTree.Element` to a :class:`Property`. @@ -278,7 +333,11 @@ class Property: if not signature: raise InvalidIntrospectionError('properties must have a "type" attribute') - return Property(name, signature, access, validate=validate) + annotations = _fetch_annotations(element) + + return Property( + name, signature, access, annotations=annotations, validate=validate + ) def to_xml(self) -> ET.Element: """Convert this :class:`Property` into an :class:`xml.etree.ElementTree.Element`.""" @@ -286,6 +345,7 @@ class Property: element.set("name", self.name) element.set("type", self.signature) element.set("access", self.access.value) + _extract_annotations(element, self.annotations) return element @@ -303,6 +363,8 @@ class Interface: :vartype signals: list(:class:`Signal`) :ivar properties: A list of properties exposed on this interface. :vartype properties: list(:class:`Property`) + :ivar annotations: The annotations of this interface. + :vartype annotations: dict[str, str] :raises: - :class:`InvalidInterfaceNameError ` - If the name is not a valid interface name. @@ -314,6 +376,7 @@ class Interface: methods: Optional[list[Method]] = None, signals: Optional[list[Signal]] = None, properties: Optional[list[Property]] = None, + annotations: Optional[dict[str, str]] = None, ): assert_interface_name_valid(name) @@ -321,6 +384,7 @@ class Interface: self.methods = methods if methods is not None else [] self.signals = signals if signals is not None else [] self.properties = properties if properties is not None else [] + self.annotations = annotations or {} @staticmethod def from_xml( @@ -353,6 +417,8 @@ class Interface: Property.from_xml(child, validate=validate_property_names) ) + interface.annotations = _fetch_annotations(element) + return interface def to_xml(self) -> ET.Element: @@ -367,6 +433,8 @@ class Interface: for prop in self.properties: element.append(prop.to_xml()) + _extract_annotations(element, self.annotations) + return element diff --git a/tests/test_introspection.py b/tests/test_introspection.py index b64c828..097dbac 100644 --- a/tests/test_introspection.py +++ b/tests/test_introspection.py @@ -97,8 +97,7 @@ def test_example_introspection_to_xml(): method = interface[0] assert method.tag == "method" assert method.get("name") == "Frobate" - # TODO annotations - assert len(method) == 3 + assert len(method) == 4 arg = method[0] assert arg.tag == "arg" @@ -106,6 +105,11 @@ def test_example_introspection_to_xml(): assert arg.attrib.get("type") == "i" assert arg.attrib.get("direction") == "in" + annotation = method[3] + assert annotation.tag == "annotation" + assert annotation.attrib.get("name") == "org.freedesktop.DBus.Deprecated" + assert annotation.attrib.get("value") == "true" + signal = interface[3] assert signal.tag == "signal" assert signal.attrib.get("name") == "Changed"