From 5a6339c5cbd87729ababa28f9ee3957851bc830c 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 5ff3469c0..192c81a5f 100644 --- a/src/blueapi/core/context.py +++ b/src/blueapi/core/context.py @@ -445,9 +445,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 b5e6182f3..16de48e18 100644 --- a/tests/unit_tests/worker/test_task_worker.py +++ b/tests/unit_tests/worker/test_task_worker.py @@ -752,7 +752,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 3b61eb6b34baadae0c8287d5dc3505f0b51abb0e 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 192c81a5f..88455cc36 100644 --- a/src/blueapi/core/context.py +++ b/src/blueapi/core/context.py @@ -446,15 +446,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 16de48e18..c2a75b19c 100644 --- a/tests/unit_tests/worker/test_task_worker.py +++ b/tests/unit_tests/worker/test_task_worker.py @@ -752,7 +752,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 ad6b3619ba69be1a1b3fb8f47d87461cd080273b 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 88455cc36..a4526f6ce 100644 --- a/src/blueapi/core/context.py +++ b/src/blueapi/core/context.py @@ -448,9 +448,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 c2a75b19c..4b4d83408 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 @@ -756,6 +756,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