From 5983eb7e51ed85eb6277bdf072825313361aa159 Mon Sep 17 00:00:00 2001 From: Dominic Oram Date: Thu, 4 Sep 2025 18:50:40 +0100 Subject: [PATCH 1/3] Give better error message on non-existant device --- src/blueapi/core/context.py | 10 +++++++--- tests/unit_tests/worker/test_task_worker.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/blueapi/core/context.py b/src/blueapi/core/context.py index 9bf29aec3..c116c7967 100644 --- a/src/blueapi/core/context.py +++ b/src/blueapi/core/context.py @@ -432,9 +432,13 @@ def __get_pydantic_core_schema__( ) -> CoreSchema: def valid(value): val = self.find_device(value) - if not val or not is_compatible( - val, cls.origin or target, cls.args - ): + if not val: + device_names = list(self.devices.keys()) + raise ValueError( + f"Device {value} cannot be found, " + f"available devices are: {device_names}" + ) + elif not is_compatible(val, cls.origin or target, cls.args): required = qualified_generic_name(target) raise ValueError( f"Device {value} is not of type {required}" diff --git a/tests/unit_tests/worker/test_task_worker.py b/tests/unit_tests/worker/test_task_worker.py index 2793e8807..787108d0d 100644 --- a/tests/unit_tests/worker/test_task_worker.py +++ b/tests/unit_tests/worker/test_task_worker.py @@ -681,7 +681,7 @@ def missing_injection(dev: FakeDevice = inject("does_not_exist")) -> MsgGenerato yield from () context.register_plan(missing_injection) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="does not exist"): Task(name="missing_injection").prepare_params(context) From 50c51928d09609ded94e7d87bddedda7a7f026e7 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Thu, 29 Jan 2026 15:55:28 +0000 Subject: [PATCH 2/3] Remove device listing from device-not-found error --- src/blueapi/core/context.py | 10 +++------- tests/unit_tests/worker/test_task_worker.py | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/blueapi/core/context.py b/src/blueapi/core/context.py index c116c7967..8a477d304 100644 --- a/src/blueapi/core/context.py +++ b/src/blueapi/core/context.py @@ -433,15 +433,11 @@ def __get_pydantic_core_schema__( def valid(value): val = self.find_device(value) if not val: - device_names = list(self.devices.keys()) - raise ValueError( - f"Device {value} cannot be found, " - f"available devices are: {device_names}" - ) + raise ValueError(f"Device {value} cannot be found") elif not is_compatible(val, cls.origin or target, cls.args): - required = qualified_generic_name(target) + req = qualified_generic_name(target) raise ValueError( - f"Device {value} is not of type {required}" + f"Device {value} ({type(val)}) is not of type {req}" ) return val diff --git a/tests/unit_tests/worker/test_task_worker.py b/tests/unit_tests/worker/test_task_worker.py index 787108d0d..72bb7fdab 100644 --- a/tests/unit_tests/worker/test_task_worker.py +++ b/tests/unit_tests/worker/test_task_worker.py @@ -681,7 +681,7 @@ def missing_injection(dev: FakeDevice = inject("does_not_exist")) -> MsgGenerato yield from () context.register_plan(missing_injection) - with pytest.raises(ValueError, match="does not exist"): + with pytest.raises(ValueError, match="Device does_not_exist cannot be found"): Task(name="missing_injection").prepare_params(context) From 6277271994835f72e28fa8440a1a81c91fb303ff Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Fri, 6 Feb 2026 16:50:50 +0000 Subject: [PATCH 3/3] Add test for the found-but-invalid device lookup case --- src/blueapi/core/context.py | 5 +++-- tests/unit_tests/worker/test_task_worker.py | 19 ++++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/blueapi/core/context.py b/src/blueapi/core/context.py index 8a477d304..fed2ebc94 100644 --- a/src/blueapi/core/context.py +++ b/src/blueapi/core/context.py @@ -435,9 +435,10 @@ def valid(value): if not val: raise ValueError(f"Device {value} cannot be found") elif not is_compatible(val, cls.origin or target, cls.args): - req = qualified_generic_name(target) + actual = qualified_name(type(val)) + required = qualified_generic_name(target) raise ValueError( - f"Device {value} ({type(val)}) is not of type {req}" + f"Device {value} ({actual}) is not of type {required}" ) return val diff --git a/tests/unit_tests/worker/test_task_worker.py b/tests/unit_tests/worker/test_task_worker.py index 72bb7fdab..71040e420 100644 --- a/tests/unit_tests/worker/test_task_worker.py +++ b/tests/unit_tests/worker/test_task_worker.py @@ -10,7 +10,7 @@ import pydantic import pytest -from bluesky.protocols import Movable, Status +from bluesky.protocols import Movable, Readable, Status from bluesky.utils import MsgGenerator from dodal.common import inject from dodal.common.types import UpdatingPathProvider @@ -685,6 +685,23 @@ def missing_injection(dev: FakeDevice = inject("does_not_exist")) -> MsgGenerato Task(name="missing_injection").prepare_params(context) +def test_invalid_injected_devices_fail_early( + context: BlueskyContext, +): + def invalid_injection(dev: Readable = inject("fake_device")) -> MsgGenerator: + yield from () + + context.register_plan(invalid_injection) + with pytest.raises( + ValueError, + match=( + r"Device fake_device \(test_task_worker.FakeDevice\) " + "is not of type bluesky.protocols.Readable" + ), + ): + Task(name="invalid_injection").prepare_params(context) + + @patch("blueapi.worker.task_worker.plan_tag_filter_context") def test_worker_uses_plan_tag_filter_context( mock_context: Mock, inert_worker: TaskWorker