feat(introspect): implement annotations (#359)

Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
black_desk 2025-01-16 04:22:24 +08:00 committed by GitHub
parent f58f1a6466
commit 5b61869bae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 81 additions and 9 deletions

View File

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

View File

@ -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"