diff --git a/src/dbus_fast/aio/proxy_object.py b/src/dbus_fast/aio/proxy_object.py index 6fc1ac6a..5126f570 100644 --- a/src/dbus_fast/aio/proxy_object.py +++ b/src/dbus_fast/aio/proxy_object.py @@ -186,7 +186,7 @@ async def property_setter(val: Any) -> None: class ProxyObject(BaseProxyObject): - """The proxy object implementation for the GLib :class:`MessageBus `. + """The proxy object implementation for the asyncio :class:`MessageBus `. For more information, see the :class:`BaseProxyObject `. """ diff --git a/src/dbus_fast/glib/proxy_object.py b/src/dbus_fast/glib/proxy_object.py index c149f806..d9799ecd 100644 --- a/src/dbus_fast/glib/proxy_object.py +++ b/src/dbus_fast/glib/proxy_object.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import xml.etree.ElementTree as ET -from typing import Union from .. import introspection as intr from ..constants import ErrorType @@ -307,7 +308,7 @@ def __init__( self, bus_name: str, path: str, - introspection: Union[intr.Node, str, ET.Element], + introspection: intr.Node | str | ET.Element, bus: BaseMessageBus, ): super().__init__(bus_name, path, introspection, bus, ProxyInterface) @@ -315,5 +316,5 @@ def __init__( def get_interface(self, name: str) -> ProxyInterface: return super().get_interface(name) - def get_children(self) -> list["ProxyObject"]: + def get_children(self) -> list[ProxyObject]: return super().get_children() diff --git a/src/dbus_fast/proxy_object.py b/src/dbus_fast/proxy_object.py index a1e67507..38f35245 100644 --- a/src/dbus_fast/proxy_object.py +++ b/src/dbus_fast/proxy_object.py @@ -264,26 +264,34 @@ def __init__( if not issubclass(ProxyInterface, BaseProxyInterface): raise TypeError("ProxyInterface must be an instance of BaseProxyInterface") + self.bus_name = bus_name + self.path = path + self.introspection = introspection + self.bus = bus + self.ProxyInterface = ProxyInterface + + @property + def introspection(self) -> intr.Node: + return self._introspection + + @introspection.setter + def introspection(self, introspection: intr.Node | str | ET.Element) -> None: if type(introspection) is intr.Node: - self.introspection = introspection + self._introspection = introspection elif type(introspection) is str: - self.introspection = intr.Node.parse(introspection) + self._introspection = intr.Node.parse(introspection) elif type(introspection) is ET.Element: - self.introspection = intr.Node.from_xml(introspection) + self._introspection = intr.Node.from_xml(introspection) else: raise TypeError( "introspection must be xml node introspection or introspection.Node class" ) + self.child_paths = [f"{self.path}/{n.name}" for n in self._introspection.nodes] - self.bus_name = bus_name - self.path = path - self.bus = bus - self.ProxyInterface = ProxyInterface - self.child_paths = [f"{path}/{n.name}" for n in self.introspection.nodes] - + # lazily populated by get_interface self._interfaces = {} - # lazy loaded by get_children() + # lazily initialized by get_children self._children = None def get_interface(self, name: str) -> BaseProxyInterface: @@ -348,10 +356,25 @@ def get_owner_notify(msg: Message, err: Exception | None) -> None: return interface def get_children(self) -> list[BaseProxyObject]: - """Get the child nodes of this proxy object according to the introspection data.""" + """Get the child nodes of this proxy object according to the introspection data. + + This method does not introspect the children, so if the parent object's + introspection did not include introspections of the children, then the + children returned by this method will appear to have no interfaces and + no children of their own. In that case, you should overwrite each + returned child proxy object's :ivar introspection: with the actual + introspection of that child object, either from static XML data + included in your project (recommended) or retrieved from the + ``org.freedesktop.DBus.Introspectable`` interface at runtime. + """ if self._children is None: self._children = [ - self.__class__(self.bus_name, self.path, child, self.bus) + self.__class__( + self.bus_name, + f"{self.path}/{child.name}", + child, + self.bus, + ) for child in self.introspection.nodes ] diff --git a/tests/test_introspection.py b/tests/test_introspection.py index b02170aa..4c99e4de 100644 --- a/tests/test_introspection.py +++ b/tests/test_introspection.py @@ -1,4 +1,9 @@ +from __future__ import annotations + import os +import xml.etree.ElementTree as ET + +import pytest from dbus_fast import ( ArgDirection, @@ -7,6 +12,10 @@ SignatureType, ) from dbus_fast import introspection as intr +from dbus_fast.constants import ErrorType +from dbus_fast.errors import DBusError, InterfaceNotFoundError +from dbus_fast.message_bus import BaseMessageBus +from dbus_fast.proxy_object import BaseProxyInterface, BaseProxyObject with open(f"{os.path.dirname(__file__)}/data/strict-introspection.xml") as f: strict_data = f.read() @@ -145,3 +154,137 @@ def test_default_interfaces(): # just make sure it doesn't throw default = intr.Node.default() assert type(default) is intr.Node + + +class MockMessageBus(BaseMessageBus): + def __init__(self, nodes: dict[str, dict[str, intr.Node]]) -> None: + super().__init__(ProxyObject=MockProxyObject) + self.nodes = nodes + + def introspect_sync(self, bus_name: str, path: str) -> intr.Node: + service = self.nodes.get(bus_name) + if service is None: + raise DBusError(ErrorType.NAME_HAS_NO_OWNER, f"unknown service: {bus_name}") + node = service.get(path) + if node is None: + raise DBusError(ErrorType.UNKNOWN_OBJECT, f"unknown object: {path}") + return node + + def _setup_socket(self) -> None: + pass + + def _init_high_level_client(self) -> None: + pass + + +class MockProxyInterface(BaseProxyInterface): + def _add_method(self, intr_method: intr.Method) -> None: + pass + + def _add_property(self, intr_property: intr.Property) -> None: + pass + + +class MockProxyObject(BaseProxyObject): + def __init__( + self, + bus_name: str, + path: str, + introspection: intr.Node | str | ET.Element, + bus: BaseMessageBus, + ) -> None: + super().__init__(bus_name, path, introspection, bus, MockProxyInterface) + # defeat name owner tracking for testing purposes + if bus_name and not bus_name.startswith(":"): + bus._name_owners[bus_name] = ":" + + +def test_inline_child(): + bus = MockMessageBus( + { + "com.example": { + "/com/example/parent_object": intr.Node.parse( + """ + + + + + + + + + +""" + ) + } + } + ) + introspection = bus.introspect_sync("com.example", "/com/example/parent_object") + + parent = bus.get_proxy_object( + "com.example", "/com/example/parent_object", introspection + ) + assert parent.bus_name == "com.example" + assert parent.path == "/com/example/parent_object" + assert parent.child_paths == ["/com/example/parent_object/child_object"] + interface = parent.get_interface("com.example.ParentInterface") + assert interface.bus_name == "com.example" + assert interface.path == "/com/example/parent_object" + assert [method.name for method in interface.introspection.methods] == [ + "ParentMethod" + ] + + child = next(iter(parent.get_children())) + assert child.bus_name == "com.example" + assert child.path == "/com/example/parent_object/child_object" + interface = child.get_interface("com.example.ChildInterface") + assert [prop.name for prop in interface.introspection.properties] == [ + "ChildProperty" + ] + + +def test_noninline_child(): + obj0_node = intr.Node.parse(strict_data) + bus = MockMessageBus( + { + "com.example": { + obj0_node.name: obj0_node, + f"{obj0_node.name}/child_of_sample_object": intr.Node.parse( + f""" + + + + +""" + ), + } + } + ) + introspection = bus.introspect_sync("com.example", "/com/example/sample_object0") + parent = bus.get_proxy_object( + "com.example", "/com/example/sample_object0", introspection + ) + + assert parent.path == "/com/example/sample_object0" + assert parent.child_paths == [ + "/com/example/sample_object0/child_of_sample_object", + "/com/example/sample_object0/another_child_of_sample_object", + ] + children = parent.get_children() + assert [child.path for child in children] == parent.child_paths + + child = next( + child + for child in children + if child.path == "/com/example/sample_object0/child_of_sample_object" + ) + with pytest.raises(InterfaceNotFoundError): + interface = child.get_interface("com.example.ChildInterface") + child.introspection = bus.introspect_sync(child.bus_name, child.path) + interface = child.get_interface("com.example.ChildInterface") + + assert [prop.name for prop in interface.introspection.properties] == [ + "ChildProperty" + ]