From c49eac616721645801d58e4d102267f7c2b8f0a7 Mon Sep 17 00:00:00 2001 From: black-desk Date: Fri, 10 Jan 2025 12:03:27 +0800 Subject: [PATCH] feat(introspect): add annotations in introspection D-Bus allow annotations in introspection data: > ... > > Method, interface, property, signal, and argument elements may have > "annotations", which are generic key/value pairs of metadata. They > are similar conceptually to Java's annotations and C# attributes. > > ... https://dbus.freedesktop.org/doc/dbus-specification.html#introspection-format This commit add new attribute "annotations" to: - dbus_fast.introspection.Arg - dbus_fast.introspection.Signal - dbus_fast.introspection.Method - dbus_fast.introspection.Property - dbus_fast.introspection.Interface --- src/dbus_fast/introspection.py | 82 +++++++++++++++++++++++++++++++--- tests/test_introspection.py | 8 +++- 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/src/dbus_fast/introspection.py b/src/dbus_fast/introspection.py index 2c97db5a..791a4c78 100644 --- a/src/dbus_fast/introspection.py +++ b/src/dbus_fast/introspection.py @@ -7,7 +7,27 @@ 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 = {} + + for child in element: + if child.tag != "annotation": + continue + annotation_name = child.attrib.get("name") + assert annotation_name is not None + annotation_value = child.attrib.get("value") + assert annotation_value is not None + annotations[annotation_name] = annotation_value + + return annotations + + +def _extract_annotations(element: ET.Element, annotations: dict[str, str]): + for key, value in annotations.items(): + annotation = ET.Element("annotation", {"name": key, "value": value}) + element.append(annotation) class Arg: @@ -21,6 +41,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 +55,7 @@ def __init__( signature: Union[SignatureType, str], direction: Optional[list[ArgDirection]] = None, name: Optional[str] = None, + annotations: Optional[dict[str, str]] = None, ): if name is not None: assert_member_name_valid(name) @@ -53,6 +76,7 @@ def __init__( 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 @@ -76,7 +100,9 @@ def from_xml(element: ET.Element, direction: ArgDirection) -> "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`.""" @@ -88,6 +114,8 @@ def to_xml(self) -> ET.Element: element.set("direction", self.direction.value) element.set("type", self.signature) + _extract_annotations(element, self.annotations) + return element @@ -100,18 +128,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`. @@ -135,7 +171,9 @@ def from_xml(element): 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 @@ -147,6 +185,8 @@ def to_xml(self) -> ET.Element: for arg in self.args: element.append(arg.to_xml()) + _extract_annotations(element, self.annotations) + return element @@ -163,12 +203,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 @@ -176,6 +224,7 @@ def __init__(self, name: str, in_args: list[Arg] = [], out_args: list[Arg] = []) 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`. @@ -206,7 +255,9 @@ def from_xml(element: ET.Element) -> "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`.""" @@ -218,6 +269,8 @@ def to_xml(self) -> ET.Element: for arg in self.out_args: element.append(arg.to_xml()) + _extract_annotations(element, self.annotations) + return element @@ -233,6 +286,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. @@ -245,6 +300,7 @@ def __init__( name: str, signature: str, access: PropertyAccess = PropertyAccess.READWRITE, + annotations: Optional[dict[str, str]] = None, ): assert_member_name_valid(name) @@ -258,6 +314,7 @@ def __init__( self.signature = signature self.access = access self.type = tree.types[0] + self.annotations = annotations or {} def from_xml(element): """Convert an :class:`xml.etree.ElementTree.Element` to a :class:`Property`. @@ -279,7 +336,9 @@ def from_xml(element): if not signature: raise InvalidIntrospectionError('properties must have a "type" attribute') - return Property(name, signature, access) + annotations = _fetch_annotations(element) + + return Property(name, signature, access, annotations) def to_xml(self) -> ET.Element: """Convert this :class:`Property` into an :class:`xml.etree.ElementTree.Element`.""" @@ -287,6 +346,7 @@ def to_xml(self) -> ET.Element: element.set("name", self.name) element.set("type", self.signature) element.set("access", self.access.value) + _extract_annotations(element, self.annotations) return element @@ -304,6 +364,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. @@ -315,6 +377,7 @@ def __init__( 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) @@ -322,6 +385,7 @@ def __init__( 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(element: ET.Element) -> "Interface": @@ -350,6 +414,8 @@ def from_xml(element: ET.Element) -> "Interface": elif child.tag == "property": interface.properties.append(Property.from_xml(child)) + interface.annotations = _fetch_annotations(element) + return interface def to_xml(self) -> ET.Element: @@ -364,6 +430,8 @@ def to_xml(self) -> ET.Element: 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 b110cc83..b9e45ed8 100644 --- a/tests/test_introspection.py +++ b/tests/test_introspection.py @@ -78,8 +78,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" @@ -87,6 +86,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"