diff --git a/src/blueapi/client/client.py b/src/blueapi/client/client.py index 68637594fa..034b9f6a98 100644 --- a/src/blueapi/client/client.py +++ b/src/blueapi/client/client.py @@ -120,23 +120,23 @@ def __repr__(self) -> str: return f"DeviceCache({len(self._cache)} devices)" -class DeviceRef(str): +class DeviceRef: + name: str model: DeviceModel _cache: DeviceCache - def __new__(cls, name: str, cache: DeviceCache, model: DeviceModel): - instance = super().__new__(cls, name) - instance.model = model - instance._cache = cache - return instance + def __init__(self, name: str, cache: DeviceCache, model: DeviceModel): + self.name = name + self.model = model + self._cache = cache def __getattr__(self, name) -> "DeviceRef": if name.startswith("_"): raise AttributeError(f"No child device named {name}") - return self._cache[f"{self}.{name}"] + return self._cache[f"{self.name}.{name}"] def __repr__(self): - return f"Device({self})" + return f"Device({self.name})" class Plan: diff --git a/src/blueapi/client/rest.py b/src/blueapi/client/rest.py index cacbc1656c..0bddb5c87c 100644 --- a/src/blueapi/client/rest.py +++ b/src/blueapi/client/rest.py @@ -11,8 +11,10 @@ start_as_current_span, ) from pydantic import BaseModel, TypeAdapter, ValidationError +from pydantic_core import PydanticSerializationError from blueapi import __version__ +from blueapi.client import client from blueapi.config import RestConfig from blueapi.service.authentication import JWTAuth, SessionManager from blueapi.service.model import ( @@ -241,7 +243,7 @@ def create_task(self, task: TaskRequest) -> TaskResponse: TaskResponse, method="POST", get_exception=_create_task_exceptions, - data=task.model_dump(), + data=task.model_dump(mode="json", fallback=_task_model_fallback), ) def clear_task(self, task_id: str) -> TaskResponse: @@ -363,3 +365,10 @@ def __getattr__(name: str): class ServiceUnavailableError(Exception): pass + + +def _task_model_fallback(obj: Any) -> Any: + """Fallback method for serializing TaskRequests""" + if isinstance(obj, client.DeviceRef): + return obj.name + raise PydanticSerializationError(f"Object of type {type(obj)} not serializable") diff --git a/tests/unit_tests/client/test_client.py b/tests/unit_tests/client/test_client.py index 169d727f62..125fc165c2 100644 --- a/tests/unit_tests/client/test_client.py +++ b/tests/unit_tests/client/test_client.py @@ -202,9 +202,9 @@ def test_get_child_device(mock_rest: Mock, client: BlueapiClient): else None ) foo = client.devices.foo - assert foo == "foo" + assert foo.name == "foo" x = client.devices.foo.x - assert x == "foo.x" + assert x.name == "foo.x" def test_state_property(client: BlueapiClient): diff --git a/tests/unit_tests/client/test_rest.py b/tests/unit_tests/client/test_rest.py index 0de1c94277..c3386ad552 100644 --- a/tests/unit_tests/client/test_rest.py +++ b/tests/unit_tests/client/test_rest.py @@ -8,9 +8,11 @@ import requests import responses from packaging.version import Version +from pydantic_core import PydanticSerializationError from responses import DELETE, GET, PUT, matchers from blueapi import __version__ +from blueapi.client.client import DeviceRef from blueapi.client.rest import ( BlueapiRestClient, BlueskyRemoteControlError, @@ -29,6 +31,7 @@ DeviceModel, EnvironmentResponse, PlanModel, + TaskRequest, TaskResponse, TasksListResponse, WorkerTask, @@ -77,6 +80,45 @@ def test_rest_error_code( rest.get_plans() +def test_create_task_serialization(): + rest = Mock(spec=BlueapiRestClient) + request = TaskRequest( + name="demo", + instrument_session="cm12345-1", + params={"devices": [DeviceRef(name="foo", cache=Mock(), model=Mock())]}, + ) + + BlueapiRestClient.create_task(rest, request) + + rest._request_and_deserialize.assert_called_once_with( + "/tasks", + TaskResponse, + method="POST", + get_exception=_create_task_exceptions, + data={ + "name": "demo", + "instrument_session": "cm12345-1", + "params": {"devices": ["foo"]}, + }, + ) + + +def test_create_task_serialization_error(): + class CustomType: + pass + + rest = Mock(spec=BlueapiRestClient) + request = TaskRequest( + name="demo", + instrument_session="cm12345-1", + params={"devices": [CustomType()]}, + ) + + with pytest.raises(PydanticSerializationError, match="not serializable"): + BlueapiRestClient.create_task(rest, request) + rest._request_and_deserialize.assert_not_called() + + @pytest.mark.parametrize( "code,content,expected_exception", [