Skip to content

Commit 53af457

Browse files
authored
fix: Serialize plan results to JSON compatible types earlier (#1464)
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. Fixes #1447
1 parent 862f403 commit 53af457

2 files changed

Lines changed: 22 additions & 2 deletions

File tree

src/blueapi/worker/event.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from bluesky.run_engine import RunEngineStateMachine
66
from pydantic import Field, PydanticSchemaGenerationError, TypeAdapter
7+
from pydantic_core import PydanticSerializationError
78
from super_state_machine.extras import PropertyMachine, ProxyString
89

910
from blueapi.utils import BlueapiBaseModel
@@ -73,8 +74,8 @@ class TaskResult(BlueapiBaseModel):
7374
def from_result(cls, result: Any) -> Self:
7475
type_str = type(result).__name__
7576
try:
76-
value = TypeAdapter(type(result)).dump_python(result)
77-
except PydanticSchemaGenerationError:
77+
value = TypeAdapter(type(result)).dump_python(result, mode="json")
78+
except (PydanticSchemaGenerationError, PydanticSerializationError):
7879
value = None
7980
return cls(result=value, type=type_str)
8081

tests/unit_tests/worker/test_task_worker.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,3 +893,22 @@ def test_plan_module_with_composite_devices_can_be_loaded_before_device_module(
893893
params = Task(name="injected_device_plan").prepare_params(context_without_devices)
894894
assert params["composite"].fake_device == fake_device
895895
assert params["composite"].second_fake_device == second_fake_device
896+
897+
898+
@pytest.mark.parametrize(
899+
"plan_result,task_result,type_name",
900+
(
901+
(Unreturnable(foo=1, bar=[]), None, "Unreturnable"),
902+
((Unreturnable(foo=2, bar=[]),), None, "tuple"),
903+
(ComplexReturn(foo=3, bar=["a"]), {"foo": 3, "bar": ["a"]}, "ComplexReturn"),
904+
((ComplexReturn(foo=4, bar=["b"]),), [{"foo": 4, "bar": ["b"]}], "tuple"),
905+
(ModelReturn(foo=5, bar=["c"]), {"foo": 5, "bar": ["c"]}, "ModelReturn"),
906+
((ModelReturn(foo=6, bar=["d"]),), [{"foo": 6, "bar": ["d"]}], "tuple"),
907+
(42, 42, "int"),
908+
((1, 2), [1, 2], "tuple"),
909+
),
910+
)
911+
def test_task_result_serialization(plan_result, task_result, type_name):
912+
res = TaskResult.from_result(plan_result)
913+
assert res.result == task_result
914+
assert res.type == type_name

0 commit comments

Comments
 (0)