Skip to content

Commit

Permalink
Add ability to export objects with object manager
Browse files Browse the repository at this point in the history
Following methods were added to `DbusObjectManagerInterfaceAsync`:

* `export_with_manager` - takes path and object. Exports it and
  sends signal
* `remove_managed_object` - remove the passed object from being
  managed.
  • Loading branch information
igo95862 committed Jun 4, 2022
1 parent 79fb423 commit 9da0fa3
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 1 deletion.
47 changes: 47 additions & 0 deletions docs/asyncio_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,14 @@ Classes
but implements `ObjectManager <https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-objectmanager>`_
interface.

Example of serving objects with ObjectManager::

my_object_manager = DbusObjectManagerInterfaceAsync()
my_object_manager.export_to_dbus('/object/manager')

managed_object = DbusInterfaceCommonAsync()
my_object_manager.export_with_manager('/object/manager/example')

.. py:method:: get_managed_objects()
:async:

Expand Down Expand Up @@ -166,6 +174,45 @@ Classes
Interfaces list : List[str]
Interfaces names that were removed.

.. py:method:: export_with_manager(object_path, object_to_export, bus)
Export object to D-Bus and emit a signal that it was added.

ObjectManager must be exported first.

Path should be a subpath of where ObjectManager was exported.
Example, if ObjectManager exported to ``/object/manager``, the managed
object can be exported at ``/object/manager/test``.

ObjectManager will keep the reference to the object.

:param str object_path:
Object path that it will be available at.

:param DbusInterfaceCommonAsync object_to_export:
Object to export to D-Bus.

:param SdBus bus:
Optional dbus connection object.
If not passed the default dbus will be used.

:raises RuntimeError: ObjectManager was not exported.

.. py:method:: remove_managed_object(managed_object)
Emit signal that object was removed.

Releases reference to the object.

.. caution::
The object will still be accessible over D-Bus until
all references to it will be removed.

:param DbusInterfaceCommonAsync managed_object:
Object to remove from ObjectManager.

:raises RuntimeError: ObjectManager was not exported.
:raises KeyError: Passed object is not managed by ObjectManager.

Decorators
++++++++++++++++++++++++
Expand Down
2 changes: 1 addition & 1 deletion docs/asyncio_quick.rst
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ Example: ::
async def join_str(self, str_array: List[str]) -> str:
return ''.join(str_array)


class MultipleInterfaces(TestInterface, ExampleInterface):
...

Expand Down
49 changes: 49 additions & 0 deletions src/sdbus/dbus_proxy_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
SdBus,
SdBusInterface,
SdBusMessage,
SdBusSlot,
)

T_input = TypeVar('T_input')
Expand Down Expand Up @@ -908,6 +909,11 @@ class DbusObjectManagerInterfaceAsync(
interface_name='org.freedesktop.DBus.ObjectManager',
serving_enabled=False,
):
def __init__(self) -> None:
super().__init__()
self._object_manager_slot: Optional[SdBusSlot] = None
self._managed_object_to_path: Dict[DbusInterfaceBaseAsync, str] = {}

@dbus_method_async(result_signature='a{oa{sa{sv}}}')
async def get_managed_objects(
self) -> Dict[str, Dict[str, Dict[str, Any]]]:
Expand All @@ -920,3 +926,46 @@ def interfaces_added(self) -> Tuple[str, Dict[str, Dict[str, Any]]]:
@dbus_signal_async('oao')
def interfaces_removed(self) -> Tuple[str, List[str]]:
raise NotImplementedError

def export_to_dbus(
self,
object_path: str,
bus: Optional[SdBus] = None,
) -> None:
if bus is None:
bus = get_default_bus()

super().export_to_dbus(
object_path,
bus,
)
slot = bus.add_object_manager(object_path)
self._object_manager_slot = slot

def export_with_manager(
self,
object_path: str,
object_to_export: DbusInterfaceBaseAsync,
bus: Optional[SdBus] = None,
) -> None:
if self._object_manager_slot is None:
raise RuntimeError('ObjectManager not intitialized')

if bus is None:
bus = get_default_bus()

object_to_export.export_to_dbus(
object_path,
bus,
)
bus.emit_object_added(object_path)
self._managed_object_to_path[object_to_export] = object_path

def remove_managed_object(
self,
managed_object: DbusInterfaceBaseAsync) -> None:
if self._attached_bus is None:
raise RuntimeError('Object manager not exported')

removed_path = self._managed_object_to_path.pop(managed_object)
self._attached_bus.emit_object_removed(removed_path)
3 changes: 3 additions & 0 deletions src/sdbus/sd_bus_internals.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ def add_object_manager(self, path: str, /) -> SdBusSlot:
def emit_object_added(self, path: str, /) -> None:
raise NotImplementedError(__STUB_ERROR)

def emit_object_removed(self, path: str, /) -> None:
raise NotImplementedError(__STUB_ERROR)

def close(self) -> None:
raise NotImplementedError(__STUB_ERROR)

Expand Down
17 changes: 17 additions & 0 deletions src/sdbus/sd_bus_internals_bus.c
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,22 @@ static PyObject* SdBus_emit_object_added(SdBusObject* self, PyObject* args) {
Py_RETURN_NONE;
}

#ifndef Py_LIMITED_API
static PyObject* SdBus_emit_object_removed(SdBusObject* self, PyObject* const* args, Py_ssize_t nargs) {
SD_BUS_PY_CHECK_ARGS_NUMBER(1);
SD_BUS_PY_CHECK_ARG_CHECK_FUNC(0, PyUnicode_Check);

const char* removed_object_path = SD_BUS_PY_UNICODE_AS_CHAR_PTR(args[0]);
#else
static PyObject* SdBus_emit_object_removed(SdBusObject* self, PyObject* args) {
const char* removed_object_path = NULL;
CALL_PYTHON_BOOL_CHECK(PyArg_ParseTuple(args, "s", &removed_object_path, NULL));
#endif
CALL_SD_BUS_AND_CHECK(sd_bus_emit_object_removed(self->sd_bus_ref, removed_object_path));

Py_RETURN_NONE;
}

static PyObject* SdBus_close(SdBusObject* self, PyObject* Py_UNUSED(args)) {
sd_bus_close(self->sd_bus_ref);
Py_RETURN_NONE;
Expand Down Expand Up @@ -612,6 +628,7 @@ static PyMethodDef SdBus_methods[] = {
{"request_name", (SD_BUS_PY_FUNC_TYPE)SdBus_request_name, SD_BUS_PY_METH, "Request dbus name blocking"},
{"add_object_manager", (SD_BUS_PY_FUNC_TYPE)SdBus_add_object_manager, SD_BUS_PY_METH, "Add object manager at the path"},
{"emit_object_added", (SD_BUS_PY_FUNC_TYPE)SdBus_emit_object_added, SD_BUS_PY_METH, "Emit signal that object was added"},
{"emit_object_removed", (SD_BUS_PY_FUNC_TYPE)SdBus_emit_object_removed, SD_BUS_PY_METH, "Emit signal that object was removed"},
{"close", (PyCFunction)SdBus_close, METH_NOARGS, "Close connection"},
{"start", (PyCFunction)SdBus_start, METH_NOARGS, "Start connection"},
{NULL, NULL, 0, NULL},
Expand Down
140 changes: 140 additions & 0 deletions test/test_object_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# SPDX-License-Identifier: LGPL-2.1-or-later

# Copyright (C) 2020-2022 igo95862

# This file is part of python-sdbus

# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.

# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.

# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

from __future__ import annotations

from asyncio import get_running_loop, sleep, wait_for
from typing import Any, Dict, List, Tuple

from sdbus.unittest import IsolatedDbusTestCase

from sdbus import (
DbusInterfaceCommonAsync,
DbusObjectManagerInterfaceAsync,
dbus_method_async,
dbus_property_async,
)

HELLO_WORLD = 'Hello World!'
TEST_NUMBER = 1000


class ObjectManagerTestInterface(
DbusObjectManagerInterfaceAsync,
interface_name='org.test.test',
):
@dbus_method_async(
result_signature='s',
)
async def get_hello_world(self) -> str:
return HELLO_WORLD


OBJECT_MANAGER_PATH = '/object_manager'
CONNECTION_NAME = 'org.example.test'

MANAGED_INTERFACE_NAME = 'org.test.testing'


class ManagedInterface(
DbusInterfaceCommonAsync,
interface_name=MANAGED_INTERFACE_NAME,
):

@dbus_property_async('x')
def test_int(self) -> int:
return TEST_NUMBER


MANAGED_PATH = '/object_manager/test'


class TestObjectManager(IsolatedDbusTestCase):
async def test_object_manager(self) -> None:
loop = get_running_loop()
await self.bus.request_name_async(CONNECTION_NAME, 0)

object_manager = ObjectManagerTestInterface()
object_manager.export_to_dbus(OBJECT_MANAGER_PATH)

object_manager_connection = ObjectManagerTestInterface.new_proxy(
CONNECTION_NAME, OBJECT_MANAGER_PATH)

self.assertEqual(
await object_manager_connection.get_hello_world(),
HELLO_WORLD)

async def catch_interfaces_added() -> Tuple[str,
Dict[str,
Dict[str, Any]]]:
async for x in object_manager_connection.interfaces_added:
return x

raise RuntimeError

catch_added_task = loop.create_task(catch_interfaces_added())

async def catch_interfaces_removed() -> Tuple[str, List[str]]:
async for x in object_manager_connection.interfaces_removed:
return x

raise RuntimeError

catch_removed_task = loop.create_task(catch_interfaces_removed())

await sleep(0)

managed_object = ManagedInterface()

object_manager.export_with_manager(MANAGED_PATH, managed_object)

caught_added = await wait_for(catch_added_task, timeout=0.5)

added_path, added_attributes = caught_added

self.assertEqual(added_path, MANAGED_PATH)

self.assertEqual(
added_attributes[
MANAGED_INTERFACE_NAME][
'TestInt'][1],
TEST_NUMBER,
)

object_manager.remove_managed_object(managed_object)

path_removed, interfaces_removed = await wait_for(
catch_removed_task, timeout=1)

self.assertEqual(path_removed, MANAGED_PATH)

self.assertIn(MANAGED_INTERFACE_NAME, interfaces_removed)

def test_expot_with_no_manager(self) -> None:
object_manager = ObjectManagerTestInterface()

managed_object = ManagedInterface()

self.assertRaises(
RuntimeError,
object_manager.export_with_manager,
MANAGED_PATH,
managed_object,
)

0 comments on commit 9da0fa3

Please sign in to comment.