From c82f7e06d7b1737b3089a929cba0af05e6d218b5 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Tue, 31 Mar 2026 14:11:19 +0100 Subject: [PATCH 1/3] Serialize plan results to JSON compatible types earlier When a plan returns a value that is serializable but contains a nested value that is not (eg a list of an unserializable type), the check for whether it could be serialized was too lenient and the unserializable type would be stored only to fail later when it was sent to the message bus. Converting the result to a json compatible type (using mode='json') earlier allows any issues with nested fields to be caught and handled appropriately. --- src/blueapi/worker/event.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/blueapi/worker/event.py b/src/blueapi/worker/event.py index 367464457e..df9b54bb45 100644 --- a/src/blueapi/worker/event.py +++ b/src/blueapi/worker/event.py @@ -4,6 +4,7 @@ from bluesky.run_engine import RunEngineStateMachine from pydantic import Field, PydanticSchemaGenerationError, TypeAdapter +from pydantic_core import PydanticSerializationError from super_state_machine.extras import PropertyMachine, ProxyString from blueapi.utils import BlueapiBaseModel @@ -73,8 +74,8 @@ class TaskResult(BlueapiBaseModel): def from_result(cls, result: Any) -> Self: type_str = type(result).__name__ try: - value = TypeAdapter(type(result)).dump_python(result) - except PydanticSchemaGenerationError: + value = TypeAdapter(type(result)).dump_python(result, mode="json") + except (PydanticSchemaGenerationError, PydanticSerializationError): value = None return cls(result=value, type=type_str) From 38832bdaabc7d9fc9deb9c011907dea222952473 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Tue, 7 Apr 2026 16:05:25 +0100 Subject: [PATCH 2/3] Test nested result serialization --- tests/unit_tests/worker/test_task_worker.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/unit_tests/worker/test_task_worker.py b/tests/unit_tests/worker/test_task_worker.py index 4b4d83408c..5b52ff4ff3 100644 --- a/tests/unit_tests/worker/test_task_worker.py +++ b/tests/unit_tests/worker/test_task_worker.py @@ -893,3 +893,22 @@ def test_plan_module_with_composite_devices_can_be_loaded_before_device_module( params = Task(name="injected_device_plan").prepare_params(context_without_devices) assert params["composite"].fake_device == fake_device assert params["composite"].second_fake_device == second_fake_device + + +class NotSerializable: + pass + + +@pytest.mark.parametrize( + "plan_result,task_result,type_name", + ( + (NotSerializable(), None, "NotSerializable"), + ((NotSerializable(), NotSerializable()), None, "tuple"), + (42, 42, "int"), + ((1, 2), [1, 2], "tuple"), + ), +) +def test_task_result_serialization(plan_result, task_result, type_name): + res = TaskResult.from_result(plan_result) + assert res.result == task_result + assert res.type == type_name From ee2fa44411465c82a5915c4d17c11ff559bb7a53 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Wed, 8 Apr 2026 14:24:13 +0100 Subject: [PATCH 3/3] Test more result types --- tests/unit_tests/worker/test_task_worker.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit_tests/worker/test_task_worker.py b/tests/unit_tests/worker/test_task_worker.py index 5b52ff4ff3..ee5db34f7f 100644 --- a/tests/unit_tests/worker/test_task_worker.py +++ b/tests/unit_tests/worker/test_task_worker.py @@ -895,15 +895,15 @@ def test_plan_module_with_composite_devices_can_be_loaded_before_device_module( assert params["composite"].second_fake_device == second_fake_device -class NotSerializable: - pass - - @pytest.mark.parametrize( "plan_result,task_result,type_name", ( - (NotSerializable(), None, "NotSerializable"), - ((NotSerializable(), NotSerializable()), None, "tuple"), + (Unreturnable(foo=1, bar=[]), None, "Unreturnable"), + ((Unreturnable(foo=2, bar=[]),), None, "tuple"), + (ComplexReturn(foo=3, bar=["a"]), {"foo": 3, "bar": ["a"]}, "ComplexReturn"), + ((ComplexReturn(foo=4, bar=["b"]),), [{"foo": 4, "bar": ["b"]}], "tuple"), + (ModelReturn(foo=5, bar=["c"]), {"foo": 5, "bar": ["c"]}, "ModelReturn"), + ((ModelReturn(foo=6, bar=["d"]),), [{"foo": 6, "bar": ["d"]}], "tuple"), (42, 42, "int"), ((1, 2), [1, 2], "tuple"), ),