From 90fd8d287140862a5576026b3faa2c61f21ee744 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 98f8e3bf66ed8509443be05c4645439c3f5d00a9 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 14d2d8d6c0de28b473b15039bef9cd1888564c09 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"), ),