From 6438a073055d690db8f91890b9fcf82d1eea0de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 23 Jun 2025 09:01:01 +0200 Subject: [PATCH 1/3] client: updates proxy._service_representation every time the client connects --- src/pydase/client/client.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pydase/client/client.py b/src/pydase/client/client.py index ec1b81d..2deb715 100644 --- a/src/pydase/client/client.py +++ b/src/pydase/client/client.py @@ -240,12 +240,11 @@ class Client: self.proxy, serialized_object=serialized_object ) serialized_object["type"] = "DeviceConnection" - if self.proxy._service_representation is not None: - # need to use object.__setattr__ to not trigger an observer notification - object.__setattr__(self.proxy, "_service_representation", serialized_object) + # need to use object.__setattr__ to not trigger an observer notification + object.__setattr__(self.proxy, "_service_representation", serialized_object) - if TYPE_CHECKING: - self.proxy._service_representation = serialized_object # type: ignore + if TYPE_CHECKING: + self.proxy._service_representation = serialized_object # type: ignore self.proxy._notify_changed("", self.proxy) self.proxy._connected = True From 0f1ca84df585ecb911aa45d49e59ed3ded3ab492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 23 Jun 2025 09:08:50 +0200 Subject: [PATCH 2/3] client: updates proxy.serialize logic The proxy needs to properly handle serialization requests. If such a requests comes from the asyncio loop used by the socketio client, this would result in a deadlock. This happens, for example, when the observer is notified of a change triggered within a socketio event. To prevent this, I am checking the current loop against the socketio client loop. If it's the same, return the _service_representation value, which is set when pydase.Client connects to the server. I do the same when the client is not connected (to prevent BadNamespaceErrors). Every other invokation of serialize results in an API call to the server. --- src/pydase/client/proxy_class.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/pydase/client/proxy_class.py b/src/pydase/client/proxy_class.py index f1c8c62..51ae799 100644 --- a/src/pydase/client/proxy_class.py +++ b/src/pydase/client/proxy_class.py @@ -65,19 +65,31 @@ class ProxyClass(ProxyClassMixin, pydase.components.DeviceConnection): self.reconnect = reconnect def serialize(self) -> SerializedObject: - if self._service_representation is None: - serialization_future = cast( + current_loop = asyncio.get_event_loop() + + if not self.connected or current_loop == self._loop: + logger.debug( + "Client not connected, or called from within client event loop - using " + "fallback serialization" + ) + if self._service_representation is None: + serialized_service = pydase.components.DeviceConnection().serialize() + else: + serialized_service = self._service_representation + + else: + future = cast( "asyncio.Future[SerializedDataService]", asyncio.run_coroutine_threadsafe( self._sio.call("service_serialization"), self._loop ), ) + result = future.result() # need to use object.__setattr__ to not trigger an observer notification - object.__setattr__( - self, "_service_representation", serialization_future.result() - ) + object.__setattr__(self, "_service_representation", result) if TYPE_CHECKING: - self._service_representation = serialization_future.result() + self._service_representation = result + serialized_service = result device_connection_value = cast( "dict[str, SerializedObject]", @@ -93,7 +105,7 @@ class ProxyClass(ProxyClassMixin, pydase.components.DeviceConnection): "dict[str, SerializedObject]", # need to deepcopy to not overwrite the _service_representation dict # when adding a prefix with add_prefix_to_full_access_path - deepcopy(self._service_representation["value"]), + deepcopy(serialized_service["value"]), ), **device_connection_value, } From 243b46aadb6189c981be038b3ae8d0244fc7e6e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 23 Jun 2025 14:09:52 +0200 Subject: [PATCH 3/3] test: adds test for ProxyClass This test timed out before implementing the changes. --- tests/client/test_proxy_class.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/client/test_proxy_class.py diff --git a/tests/client/test_proxy_class.py b/tests/client/test_proxy_class.py new file mode 100644 index 0000000..c674462 --- /dev/null +++ b/tests/client/test_proxy_class.py @@ -0,0 +1,22 @@ +import asyncio +from unittest.mock import AsyncMock, call, patch + +import pytest + +from pydase import components +from pydase.client.proxy_class import ProxyClass + + +@pytest.mark.asyncio +async def test_serialize_fallback_inside_event_loop() -> None: + loop = asyncio.get_running_loop() + mock_sio = AsyncMock() + proxy = ProxyClass(sio_client=mock_sio, loop=loop, reconnect=lambda: None) + + with patch.object( + components.DeviceConnection, "serialize", return_value={"value": {}} + ) as mock_fallback: + result = proxy.serialize() + + mock_fallback.assert_has_calls(calls=[call(), call()]) + assert isinstance(result, dict)