Skip to content

Commit

Permalink
feat(introspect): add annotations in introspection
Browse files Browse the repository at this point in the history
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
  • Loading branch information
black-desk committed Jan 11, 2025
1 parent c3779d9 commit c49eac6
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 9 deletions.
82 changes: 75 additions & 7 deletions src/dbus_fast/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -21,6 +41,8 @@ class Arg:
:vartype type: :class:`SignatureType <dbus_fast.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 <dbus_fast.InvalidMemberNameError>` - If the name of the arg is not valid.
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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`."""
Expand All @@ -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


Expand All @@ -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 <dbus_fast.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`.
Expand All @@ -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

Expand All @@ -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


Expand All @@ -163,19 +203,28 @@ 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 <dbus_fast.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
self.in_args = in_args
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`.
Expand Down Expand Up @@ -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`."""
Expand All @@ -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


Expand All @@ -233,6 +286,8 @@ class Property:
:vartype access: :class:`PropertyAccess <dbus_fast.PropertyAccess>`
:ivar type: The parsed type of this property.
:vartype type: :class:`SignatureType <dbus_fast.SignatureType>`
:ivar annotations: The annotations of this property.
:vartype annotations: dict[str, str]
:raises:
- :class:`InvalidIntrospectionError <dbus_fast.InvalidIntrospectionError>` - If the property is not a single complete type.
Expand All @@ -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)

Expand All @@ -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`.
Expand All @@ -279,14 +336,17 @@ 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`."""
element = ET.Element("property")
element.set("name", self.name)
element.set("type", self.signature)
element.set("access", self.access.value)
_extract_annotations(element, self.annotations)
return element


Expand All @@ -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 <dbus_fast.InvalidInterfaceNameError>` - If the name is not a valid interface name.
Expand All @@ -315,13 +377,15 @@ 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)

self.name = name
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":
Expand Down Expand Up @@ -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:
Expand All @@ -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


Expand Down
8 changes: 6 additions & 2 deletions tests/test_introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,19 @@ 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"
assert arg.attrib.get("name") == "foo"
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"
Expand Down

0 comments on commit c49eac6

Please sign in to comment.