Skip to content

Commit 5f5ae14

Browse files
committed
BaseProxyObject: lazily introspect children as needed
The D-Bus specification says: “If a child <node> has any sub-elements, then they must represent a complete introspection of the child. If a child <node> is empty, then it may or may not have sub-elements; the child must be introspected in order to find out. The intent is that if an object knows that its children are "fast" to introspect it can go ahead and return their information, but otherwise it can omit it.” However, BaseProxyObject.get_children() has been assuming that all child <node> elements provide a complete introspection of their corresponding child objects. This causes child objects to appear to have no interfaces or children of their own, even when they in fact do. To implement the spec, replace BaseProxyObject.introspection with a lazily-computed property whose value is initialized as the intr.Node passed to the BaseProxyObject constructor (or parsed by it), if one was specified, or is otherwise computed upon first use by introspecting the child object (synchronously). The synchronous introspection required pulling the introspect_sync method up from glib.MessageBus into BaseMessageBus as an abstract method and then implementing it in aio.MessageBus by using asyncio.loop.run_until_complete(…).
1 parent d39fd5d commit 5f5ae14

File tree

6 files changed

+53
-12
lines changed

6 files changed

+53
-12
lines changed

src/dbus_fast/aio/message_bus.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,16 @@ async def introspect(
318318
finally:
319319
timer_handle.cancel()
320320

321+
def introspect_sync(
322+
self,
323+
bus_name: str,
324+
path: str,
325+
*args, **kwargs
326+
) -> intr.Node:
327+
return self._loop.run_until_complete(
328+
self.introspect(bus_name, path, *args, **kwargs)
329+
)
330+
321331
async def request_name(
322332
self, name: str, flags: NameFlag = NameFlag.NONE
323333
) -> RequestNameReply:

src/dbus_fast/aio/proxy_object.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ def __init__(
195195
self,
196196
bus_name: str,
197197
path: str,
198-
introspection: intr.Node | str | ET.Element,
198+
introspection: intr.Node | str | ET.Element | None,
199199
bus: BaseMessageBus,
200200
) -> None:
201201
super().__init__(bus_name, path, introspection, bus, ProxyInterface)

src/dbus_fast/glib/proxy_object.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ def __init__(
307307
self,
308308
bus_name: str,
309309
path: str,
310-
introspection: Union[intr.Node, str, ET.Element],
310+
introspection: intr.Node | str | ET.Element | None,
311311
bus: BaseMessageBus,
312312
):
313313
super().__init__(bus_name, path, introspection, bus, ProxyInterface)

src/dbus_fast/message_bus.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,9 @@ def reply_notify(reply: Message | None, err: Exception | None) -> None:
309309
reply_notify,
310310
)
311311

312+
def introspect_sync(self, bus_name: str, path: str) -> intr.Node:
313+
raise NotImplementedError("this must be implemented in the inheriting class")
314+
312315
def _emit_interface_added(self, path: str, interface: ServiceInterface) -> None:
313316
"""Emit the ``org.freedesktop.DBus.ObjectManager.InterfacesAdded`` signal.
314317

src/dbus_fast/proxy_object.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@ class BaseProxyObject:
234234
:ivar path: The object path exported on the client that owns the bus name.
235235
:vartype path: str
236236
:ivar introspection: Parsed introspection data for the proxy object.
237+
May be ``None`` if introspection has not yet been performed, in which case
238+
the object will be introspected lazily upon first use.
237239
:vartype introspection: :class:`Node <dbus_fast.introspection.Node>`
238240
:ivar bus: The message bus this proxy object is connected to.
239241
:vartype bus: :class:`BaseMessageBus <dbus_fast.message_bus.BaseMessageBus>`
@@ -252,7 +254,7 @@ def __init__(
252254
self,
253255
bus_name: str,
254256
path: str,
255-
introspection: intr.Node | str | ET.Element,
257+
introspection: intr.Node | str | ET.Element | None,
256258
bus: message_bus.BaseMessageBus,
257259
ProxyInterface: type[BaseProxyInterface],
258260
) -> None:
@@ -264,12 +266,12 @@ def __init__(
264266
if not issubclass(ProxyInterface, BaseProxyInterface):
265267
raise TypeError("ProxyInterface must be an instance of BaseProxyInterface")
266268

267-
if type(introspection) is intr.Node:
268-
self.introspection = introspection
269+
if introspection is None or type(introspection) is intr.Node:
270+
self._introspection = introspection
269271
elif type(introspection) is str:
270-
self.introspection = intr.Node.parse(introspection)
272+
self._introspection = intr.Node.parse(introspection)
271273
elif type(introspection) is ET.Element:
272-
self.introspection = intr.Node.from_xml(introspection)
274+
self._introspection = intr.Node.from_xml(introspection)
273275
else:
274276
raise TypeError(
275277
"introspection must be xml node introspection or introspection.Node class"
@@ -279,13 +281,39 @@ def __init__(
279281
self.path = path
280282
self.bus = bus
281283
self.ProxyInterface = ProxyInterface
282-
self.child_paths = [f"{path}/{n.name}" for n in self.introspection.nodes]
284+
self._child_paths = None
283285

284286
self._interfaces = {}
285287

286288
# lazy loaded by get_children()
287289
self._children = None
288290

291+
@property
292+
def introspection(self) -> intr.Node:
293+
"""Access the introspection of this object, performing it (synchronously) if necessary.
294+
295+
:raises:
296+
- :class:`InvalidBusNameError <dbus_fast.InvalidBusNameError>` - If this object's bus name is not valid.
297+
- :class:`InvalidObjectPathError <dbus_fast.InvalidObjectPathError>` - If this object's path is not valid.
298+
- :class:`InvalidIntrospectionError <dbus_fast.InvalidIntrospectionError>` - If the introspection data for the node is not valid.
299+
"""
300+
if self._introspection is None:
301+
self._introspection = self.bus.introspect_sync(self.bus_name, self.path)
302+
return self._introspection
303+
304+
@property
305+
def child_paths(self) -> List[str]:
306+
"""Access the list of paths of this object's children, fetching it (synchronously) if necessary.
307+
308+
:raises:
309+
- :class:`InvalidBusNameError <dbus_fast.InvalidBusNameError>` - If this object's bus name is not valid.
310+
- :class:`InvalidObjectPathError <dbus_fast.InvalidObjectPathError>` - If this object's path is not valid.
311+
- :class:`InvalidIntrospectionError <dbus_fast.InvalidIntrospectionError>` - If the introspection data for the node is not valid.
312+
"""
313+
if self._child_paths is None:
314+
self._child_paths = [f"{self.path}/{n.name}" for n in self.introspection.nodes]
315+
return self._child_paths
316+
289317
def get_interface(self, name: str) -> BaseProxyInterface:
290318
"""Get an interface exported on this proxy object and connect it to the bus.
291319
@@ -352,7 +380,8 @@ def get_children(self) -> list[BaseProxyObject]:
352380
if self._children is None:
353381
self._children = [
354382
self.__class__(self.bus_name, f"{self.path}/{child.name}",
355-
child, self.bus)
383+
child if child.interfaces or child.nodes else None,
384+
self.bus)
356385
for child in self.introspection.nodes
357386
]
358387

tests/test_introspection.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
)
1111
from dbus_fast import introspection as intr
1212
from dbus_fast.constants import ErrorType
13-
from dbus_fast.errors import DBusError, InterfaceNotFoundError
13+
from dbus_fast.errors import DBusError
1414
from dbus_fast.message_bus import BaseMessageBus
1515
from dbus_fast.proxy_object import BaseProxyObject, BaseProxyInterface
1616

@@ -200,7 +200,7 @@ class MockProxyObject(BaseProxyObject):
200200
def __init__(self,
201201
bus_name: str,
202202
path: str,
203-
introspection: intr.Node | str | ET.Element,
203+
introspection: intr.Node | str | ET.Element | None,
204204
bus: BaseMessageBus
205205
) -> None:
206206
super().__init__(bus_name, path, introspection, bus, MockProxyInterface)
@@ -254,7 +254,6 @@ def test_inline_child():
254254
assert bus.introspect_count == 1
255255

256256

257-
@pytest.mark.xfail(reason='buggy BaseProxyObject.get_children', raises=InterfaceNotFoundError, strict=True)
258257
def test_noninline_child():
259258
obj0_node = intr.Node.parse(strict_data)
260259
bus = MockMessageBus({

0 commit comments

Comments
 (0)