From 9a7053433cc6ba0dc9f4b5c8715188759210471b Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Fri, 1 May 2026 15:45:15 +0100 Subject: [PATCH 01/11] Initial rough hacking out of dodal code --- src/blueapi/config.py | 15 +-- src/blueapi/core/context.py | 66 ------------- src/blueapi/utils/__init__.py | 5 +- src/blueapi/utils/connect_devices.py | 85 +--------------- tests/unit_tests/core/fake_device_module.py | 80 --------------- tests/unit_tests/core/test_context.py | 102 +------------------- tests/unit_tests/service/test_runner.py | 16 ++- tests/unit_tests/test_example_code.py | 2 +- tests/unit_tests/worker/test_task_worker.py | 6 +- 9 files changed, 22 insertions(+), 355 deletions(-) delete mode 100644 tests/unit_tests/core/fake_device_module.py diff --git a/src/blueapi/config.py b/src/blueapi/config.py index 83d6d70211..25bbf204e0 100644 --- a/src/blueapi/config.py +++ b/src/blueapi/config.py @@ -63,19 +63,6 @@ class PlanSource(Source): ) -class DeviceSource(Source): - kind: Literal[SourceKind.DEVICE_FUNCTIONS] = Field( - SourceKind.DEVICE_FUNCTIONS, init=False - ) - - -class DodalSource(Source): - kind: Literal[SourceKind.DODAL] = Field(SourceKind.DODAL, init=False) - mock: bool = Field( - description="If true, ophyd_async device connections are mocked", default=False - ) - - class DeviceManagerSource(Source): kind: Literal[SourceKind.DEVICE_MANAGER] = Field( SourceKind.DEVICE_MANAGER, init=False @@ -150,7 +137,7 @@ class EnvironmentConfig(BlueapiBaseModel): sources: list[ Annotated[ - PlanSource | DeviceSource | DodalSource | DeviceManagerSource, + PlanSource | DeviceManagerSource, Field(discriminator="kind"), ] ] = [ diff --git a/src/blueapi/core/context.py b/src/blueapi/core/context.py index 60df7a9d70..78290bf00e 100644 --- a/src/blueapi/core/context.py +++ b/src/blueapi/core/context.py @@ -9,8 +9,6 @@ from bluesky.protocols import HasName from bluesky.run_engine import RunEngine -from dodal.common.beamlines.beamline_utils import get_path_provider, set_path_provider -from dodal.utils import AnyDevice, make_all_devices from ophyd_async.core import NotConnectedError, PathProvider from pydantic import ( BaseModel, @@ -26,8 +24,6 @@ from blueapi.config import ( ApplicationConfig, DeviceManagerSource, - DeviceSource, - DodalSource, EnvironmentConfig, PlanSource, ServiceAccount, @@ -151,8 +147,6 @@ def __post_init__(self, configuration: ApplicationConfig | None): ) path_provider = StartDocumentPathProvider() - # TODO: Remove this when device manager is rolled out - set_path_provider(path_provider) self.run_engine.subscribe(path_provider.run_start, "start") self.run_engine.subscribe(path_provider.run_stop, "stop") @@ -173,13 +167,6 @@ async def _update_scan_num(md: dict[str, Any]) -> int: self.run_engine.scan_id_source = _update_scan_num self.with_config(configuration.env) - if configuration.numtracker and not isinstance( - get_path_provider(), StartDocumentPathProvider - ): - raise InvalidConfigError( - "Numtracker has been configured but a path provider was imported with " - "the devices. Remove this path provider to use numtracker." - ) if (tiled_conf := configuration.tiled) is not None and tiled_conf.enabled: if configuration.env.metadata is None: @@ -232,22 +219,6 @@ def with_config(self, config: EnvironmentConfig) -> None: case PlanSource(): LOGGER.info("Including plans from %s", source.module) self.with_plan_module(mod) - case DeviceSource(): - LOGGER.info("Including devices from %s", source.module) - LOGGER.warning( - "'devices' environment kind is deprecated - please convert " - "configuration to use deviceManager" - ) - self.with_device_module(mod) - case DodalSource(mock=mock): - LOGGER.info( - "Including devices from 'dodal' source %s", source.module - ) - LOGGER.warning( - "'dodal' environment kind is deprecated - please convert " - "configuration to use deviceManager" - ) - self.with_dodal_module(mod, mock=mock) case DeviceManagerSource(mock=mock, name=name): LOGGER.info( "Including devices from 'deviceManager' source %s:%s", @@ -333,43 +304,6 @@ def with_device_manager(self, manager: DeviceManager, mock: bool = False): **build_result.connection_errors, } - def with_device_module(self, module: ModuleType) -> None: - self.with_dodal_module(module) - - def with_dodal_module( - self, module: ModuleType, **kwargs - ) -> tuple[dict[str, AnyDevice], dict[str, Exception]]: - """ - Discover all device factories in the specified module, - construct devices by invoking them and register them with the device context, - Then attempt to connect to all the devices. - - Args: - module: The python module to inspect for factories - kwargs: keyword arguments that will be passed to make_all_devices() and - to connect_devices() for construction and connection respectively - Returns: - A tuple containing a map of device name to devices, and a map of - device name to any exceptions encountered. - """ - devices, exceptions = make_all_devices(module, **kwargs) - - exceptions |= utils.connect_devices(self.run_engine, module, devices, **kwargs) - - for device in devices.values(): - self.register_device(device) - - # If exceptions have occurred, we log them but we do not make blueapi - # fall over - if len(exceptions) > 0: - LOGGER.warning( - f"{len(exceptions)} exceptions occurred while instantiating devices" - ) - LOGGER.exception(NotConnectedError(exceptions)) - elif not devices: - LOGGER.warning("No devices were loaded from dodal module %s", module) - return devices, exceptions - def register_plan(self, plan: PlanGenerator) -> PlanGenerator: """ Register the argument as a plan in the context. Can be used as a decorator e.g. diff --git a/src/blueapi/utils/__init__.py b/src/blueapi/utils/__init__.py index 4b2e41f2c1..9555571549 100644 --- a/src/blueapi/utils/__init__.py +++ b/src/blueapi/utils/__init__.py @@ -3,7 +3,7 @@ from typing import ParamSpec, TypeVar from .base_model import BlueapiBaseModel, BlueapiModelConfig, BlueapiPlanModelConfig -from .connect_devices import connect_devices, report_successful_devices +from .connect_devices import report_successful_devices from .file_permissions import get_owner_gid, is_sgid_set from .invalid_config_error import InvalidConfigError from .modules import is_function_sourced_from_module, load_module_all @@ -20,10 +20,9 @@ "BlueapiPlanModelConfig", "InvalidConfigError", "NumtrackerClient", - "connect_devices", - "report_successful_devices", "is_sgid_set", "get_owner_gid", + "report_successful_devices", "is_function_sourced_from_module", "deprecated", ] diff --git a/src/blueapi/utils/connect_devices.py b/src/blueapi/utils/connect_devices.py index d9d2172cc5..55c607e37c 100644 --- a/src/blueapi/utils/connect_devices.py +++ b/src/blueapi/utils/connect_devices.py @@ -1,22 +1,12 @@ import logging from collections.abc import Mapping -from types import ModuleType - -from bluesky.run_engine import RunEngine -from dodal.utils import ( - AnyDevice, - DeviceInitializationController, - collect_factories, - filter_ophyd_devices, -) -from ophyd_async.core import NotConnectedError -from ophyd_async.plan_stubs import ensure_connected +from typing import Any LOGGER = logging.getLogger(__name__) def report_successful_devices( - devices: Mapping[str, AnyDevice], + devices: Mapping[str, Any], sim_backend: bool, ) -> None: sim_statement = " (sim mode)" if sim_backend else "" @@ -26,74 +16,3 @@ def report_successful_devices( LOGGER.info(f"{len(devices)} devices connected{sim_statement}:") LOGGER.info(connected_devices) - - -def _establish_device_connections( - run_engine: RunEngine, - devices: Mapping[str, AnyDevice], - sim_backend: bool, -) -> tuple[Mapping[str, AnyDevice], Mapping[str, Exception]]: - ophyd_devices, ophyd_async_devices = filter_ophyd_devices(devices) - exceptions = {} - - # Connect ophyd devices - for name, device in ophyd_devices.items(): - try: - device.wait_for_connection() - except Exception as ex: - exceptions[name] = ex - - # Connect ophyd-async devices - try: - run_engine(ensure_connected(*ophyd_async_devices.values(), mock=sim_backend)) - except NotConnectedError as ex: - exceptions = {**exceptions, **ex.sub_errors} - - # Only return the subset of devices that haven't raised an exception - successful_devices = { - name: device for name, device in devices.items() if name not in exceptions - } - return successful_devices, exceptions - - -def connect_devices( - run_engine: RunEngine, module: ModuleType, devices: dict[str, AnyDevice], **kwargs -) -> Mapping[str, Exception]: - factories = collect_factories(module, include_skipped=False) - - def is_simulated_device(name, factory, **kwargs): - device = devices.get(name, None) - mock_flag = kwargs.get("mock", kwargs.get("fake_with_ophyd_sim", False)) - return device is not None and ( - isinstance(factory, DeviceInitializationController) - and (factory._mock or mock_flag) # noqa: SLF001 - and isinstance(device, AnyDevice) - ) - - sim_devices = { - name: devices.get(name) - for name, factory in factories.items() - if is_simulated_device(name, factory, **kwargs) - } - real_devices = { - name: device - for name, device in devices.items() - if sim_devices.get(name, None) is None and (isinstance(device, AnyDevice)) - } - - exception_map = {} - if len(real_devices) > 0: - real_devices, exceptions = _establish_device_connections( - run_engine, real_devices, False - ) - report_successful_devices(real_devices, False) - exception_map |= exceptions - if len(sim_devices) > 0: - sim_devices, exceptions = _establish_device_connections( - run_engine, - sim_devices, # type: ignore - True, - ) - report_successful_devices(sim_devices, True) - exception_map |= exceptions - return exception_map diff --git a/tests/unit_tests/core/fake_device_module.py b/tests/unit_tests/core/fake_device_module.py deleted file mode 100644 index 25b97d2cf1..0000000000 --- a/tests/unit_tests/core/fake_device_module.py +++ /dev/null @@ -1,80 +0,0 @@ -from unittest.mock import MagicMock, NonCallableMock - -from dodal.common.beamlines.beamline_utils import device_factory -from dodal.utils import OphydV1Device, OphydV2Device -from ophyd_async.core import DEFAULT_TIMEOUT, LazyMock, StandardReadable -from ophyd_async.sim import SimMotor - - -def fake_motor_bundle_b( - fake_motor_x: SimMotor, - fake_motor_y: SimMotor, -) -> SimMotor: - return SimMotor("motor_bundle_b") - - -def fake_motor_x() -> SimMotor: - return SimMotor("motor_x") - - -class DeviceA(StandardReadable): - def __init__(self, name: str = "") -> None: - with self.add_children_as_readables(): - self.motor = SimMotor("X:SIZE") - super().__init__(name) - - -@device_factory(mock=True) -def device_a() -> DeviceA: - return DeviceA() - - -class UnconnectableOphydDevice(OphydV1Device): - def wait_for_connection( - self, - all_signals: bool = False, - timeout: object = 2.0, - ) -> None: - raise RuntimeError(f"{self.name}: fake connection error for tests") - - -def ophyd_device() -> UnconnectableOphydDevice: - return UnconnectableOphydDevice(name="ophyd_device") - - -class UnconnectableOphydAsyncDevice(OphydV2Device): - async def connect( - self, - mock: bool | LazyMock = False, - timeout: float = DEFAULT_TIMEOUT, - force_reconnect: bool = False, - ) -> None: - raise RuntimeError(f"{self.name}: fake connection error for tests") - - -def ophyd_async_device() -> UnconnectableOphydAsyncDevice: - return UnconnectableOphydAsyncDevice(name="ophyd_async_device") - - -def fake_motor_y() -> SimMotor: - return SimMotor("motor_y") - - -def fake_motor_bundle_a( - fake_motor_x: SimMotor, - fake_motor_y: SimMotor, -) -> SimMotor: - return SimMotor("motor_bundle_a") - - -def wrong_return_type() -> int: - return "str" # type: ignore - - -fetchable_non_callable = NonCallableMock() -fetchable_callable = MagicMock(return_value="string") - -fetchable_non_callable.__name__ = "fetchable_non_callable" -fetchable_non_callable.__module__ = fake_motor_bundle_a.__module__ -fetchable_callable.__name__ = "fetchable_callable" -fetchable_callable.__module__ = fake_motor_bundle_a.__module__ diff --git a/tests/unit_tests/core/test_context.py b/tests/unit_tests/core/test_context.py index 738a4c564c..b38dc47f57 100644 --- a/tests/unit_tests/core/test_context.py +++ b/tests/unit_tests/core/test_context.py @@ -4,7 +4,7 @@ from pathlib import Path from types import ModuleType, NoneType from typing import Any, Generic, TypeVar, Union -from unittest.mock import ANY, MagicMock, Mock, patch +from unittest.mock import MagicMock, Mock, patch import pytest import responses @@ -31,13 +31,10 @@ from ophyd_async.epics.motor import Motor from pydantic import TypeAdapter, ValidationError from pydantic.json_schema import SkipJsonSchema -from pytest import LogCaptureFixture from blueapi.config import ( ApplicationConfig, DeviceManagerSource, - DeviceSource, - DodalSource, EnvironmentConfig, MetadataConfig, OIDCConfig, @@ -48,7 +45,6 @@ from blueapi.core import BlueskyContext, is_bluesky_compatible_device from blueapi.core.context import DefaultFactory, generic_bounds, qualified_name from blueapi.core.protocols import DeviceConnectResult, DeviceManager -from blueapi.utils.connect_devices import _establish_device_connections from blueapi.utils.invalid_config_error import InvalidConfigError SIM_MOTOR_NAME = "sim" @@ -262,81 +258,6 @@ def test_override_device_name(empty_context: BlueskyContext, sim_motor: Motor): assert empty_context.devices["foo"] is sim_motor -def test_add_devices_from_module(empty_context: BlueskyContext): - import tests.unit_tests.core.fake_device_module as device_module - - empty_context.with_device_module(device_module) - assert { - "motor_x", - "motor_y", - "motor_bundle_a", - "motor_bundle_b", - "device_a", - "ophyd_device", - "ophyd_async_device", - } == empty_context.devices.keys() - - -def test_add_failing_deivces_from_module( - caplog: LogCaptureFixture, empty_context: BlueskyContext -): - import tests.unit_tests.core.fake_device_module_failing as device_module - - caplog.set_level(10) - empty_context.with_device_module(device_module) - logs = caplog.get_records("call") - - assert any("TimeoutError: FooBar" in log.message for log in logs) - assert len(empty_context.devices.keys()) == 0 - - -def test_extra_kwargs_in_with_dodal_module_passed_to_make_all_devices( - empty_context: BlueskyContext, -): - """ - Note that this functionality is currently used by hyperion. - """ - import tests.unit_tests.core.fake_device_module as device_module - - with patch( - "blueapi.core.context.make_all_devices", - return_value=({}, {}), - ) as mock_make_all_devices: - empty_context.with_dodal_module( - device_module, some_argument=1, another_argument="two" - ) - - mock_make_all_devices.assert_called_once_with( - device_module, some_argument=1, another_argument="two" - ) - - -def test_with_dodal_module_returns_connection_exceptions(empty_context: BlueskyContext): - import tests.unit_tests.core.fake_device_module as device_module - - def connect_sim_backend(run_engine: RunEngine, devices, sim_backend): - return _establish_device_connections(run_engine, devices, True) - - with patch( - "blueapi.utils.connect_devices._establish_device_connections", - side_effect=connect_sim_backend, - ): - names_to_devices, exceptions = empty_context.with_dodal_module(device_module) - - assert set(names_to_devices.keys()) == { - "motor_y", - "ophyd_async_device", - "ophyd_device", - "device_a", - "motor_x", - "motor_bundle_a", - "motor_bundle_b", - } - assert len(exceptions) == 2 - assert isinstance(exceptions["ophyd_device"], RuntimeError) - assert isinstance(exceptions["ophyd_async_device"], RuntimeError) - - @pytest.mark.parametrize( "addr", ["sim", "sim_det", "sim.user_setpoint", ["sim"], ["sim", "user_setpoint"]] ) @@ -407,27 +328,6 @@ def test_add_metadata_with_config( assert md in empty_context.run_engine.md.items() -@pytest.mark.parametrize("mock", [True, False]) -def test_with_config_passes_mock_to_with_dodal_module( - empty_context: BlueskyContext, - mock: bool, -): - with patch.object(empty_context, "with_dodal_module") as mock_with_dodal_module: - empty_context.with_config( - EnvironmentConfig( - sources=[ - DodalSource( - module="tests.unit_tests.core.fake_device_module", mock=mock - ), - PlanSource( - module="tests.unit_tests.core.fake_plan_module", - ), - ] - ) - ) - mock_with_dodal_module.assert_called_once_with(ANY, mock=mock) - - def test_function_spec(empty_context: BlueskyContext): spec = empty_context._type_spec_for_function(has_some_params) assert spec["foo"][0] is int diff --git a/tests/unit_tests/service/test_runner.py b/tests/unit_tests/service/test_runner.py index b139baccc3..b41b3fd4c7 100644 --- a/tests/unit_tests/service/test_runner.py +++ b/tests/unit_tests/service/test_runner.py @@ -2,7 +2,7 @@ from collections.abc import Callable from multiprocessing.pool import Pool as PoolClass from typing import Any, Generic, TypeVar -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, Mock, NonCallableMock, patch import pytest from observability_utils.tracing import ( @@ -188,19 +188,27 @@ def run_rpc_function( ) +fetchable_non_callable = NonCallableMock( + __name__="fetchable_non_callable", __module__=__name__ +) + + +def wrong_return_type() -> str: + return "" + + def test_non_callable_excepts(started_runner: WorkerDispatcher): # Not a valid target on main or sub process - from tests.unit_tests.core.fake_device_module import fetchable_non_callable + # from tests.unit_tests.core.fake_device_module import fetchable_non_callable with pytest.raises( RpcError, match="fetchable_non_callable: Object in subprocess is not a function", ): - run_rpc_function(fetchable_non_callable, Mock) + run_rpc_function(fetchable_non_callable, Mock) # type: ignore - the error is the point def test_clear_message_for_wrong_return(started_runner: WorkerDispatcher): - from tests.unit_tests.core.fake_device_module import wrong_return_type with pytest.raises( ValidationError, diff --git a/tests/unit_tests/test_example_code.py b/tests/unit_tests/test_example_code.py index e5788468f9..e7d8547c89 100644 --- a/tests/unit_tests/test_example_code.py +++ b/tests/unit_tests/test_example_code.py @@ -31,7 +31,7 @@ def test_example_device_module_is_detectable(): context = BlueskyContext() module = importlib.import_module(module_name) - context.with_dodal_module(module) + # context.with_dodal_module(module) assert device_name in context.devices diff --git a/tests/unit_tests/worker/test_task_worker.py b/tests/unit_tests/worker/test_task_worker.py index ee5db34f7f..866c52aa0a 100644 --- a/tests/unit_tests/worker/test_task_worker.py +++ b/tests/unit_tests/worker/test_task_worker.py @@ -20,7 +20,7 @@ ) from ophyd_async.core import AsyncStatus -from blueapi.config import DeviceSource, EnvironmentConfig +from blueapi.config import EnvironmentConfig from blueapi.core import BlueskyContext, EventStream from blueapi.core.bluesky_types import DataEvent from blueapi.service.model import PlanModel @@ -110,7 +110,7 @@ def second_fake_device() -> FakeDevice: def context(fake_device: FakeDevice, second_fake_device: FakeDevice) -> BlueskyContext: ctx = BlueskyContext() ctx_config = EnvironmentConfig() - ctx_config.sources.append(DeviceSource(module="devices")) + # ctx_config.sources.append(DeviceSource(module="devices")) ctx.register_plan(failing_plan) ctx.register_device(fake_device) ctx.register_device(second_fake_device) @@ -122,7 +122,7 @@ def context(fake_device: FakeDevice, second_fake_device: FakeDevice) -> BlueskyC def context_without_devices() -> BlueskyContext: ctx = BlueskyContext() ctx_config = EnvironmentConfig() - ctx_config.sources.append(DeviceSource(module="devices")) + # ctx_config.sources.append(DeviceSource(module="devices")) ctx.with_config(ctx_config) return ctx From 1146418c6efe2169262f768c485ac4a907920379 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Fri, 1 May 2026 16:15:18 +0100 Subject: [PATCH 02/11] Remove dodalsource and devicesource from schema --- helm/blueapi/config_schema.json | 58 ------------------- helm/blueapi/values.schema.json | 54 ----------------- .../plans_and_devices.yaml | 4 +- 3 files changed, 2 insertions(+), 114 deletions(-) diff --git a/helm/blueapi/config_schema.json b/helm/blueapi/config_schema.json index b5d0c9bf3d..ae89e3da28 100644 --- a/helm/blueapi/config_schema.json +++ b/helm/blueapi/config_schema.json @@ -100,56 +100,6 @@ "type": "object", "$id": "DeviceManagerSource" }, - "DeviceSource": { - "additionalProperties": false, - "properties": { - "module": { - "description": "Module to be imported", - "title": "Module", - "type": "string" - }, - "kind": { - "const": "deviceFunctions", - "default": "deviceFunctions", - "title": "Kind", - "type": "string" - } - }, - "required": [ - "module" - ], - "title": "DeviceSource", - "type": "object", - "$id": "DeviceSource" - }, - "DodalSource": { - "additionalProperties": false, - "properties": { - "module": { - "description": "Module to be imported", - "title": "Module", - "type": "string" - }, - "kind": { - "const": "dodal", - "default": "dodal", - "title": "Kind", - "type": "string" - }, - "mock": { - "default": false, - "description": "If true, ophyd_async device connections are mocked", - "title": "Mock", - "type": "boolean" - } - }, - "required": [ - "module" - ], - "title": "DodalSource", - "type": "object", - "$id": "DodalSource" - }, "EnvironmentConfig": { "additionalProperties": false, "description": "Config for the RunEngine environment", @@ -168,9 +118,7 @@ "items": { "discriminator": { "mapping": { - "deviceFunctions": "DeviceSource", "deviceManager": "DeviceManagerSource", - "dodal": "DodalSource", "planFunctions": "PlanSource" }, "propertyName": "kind" @@ -179,12 +127,6 @@ { "$ref": "PlanSource" }, - { - "$ref": "DeviceSource" - }, - { - "$ref": "DodalSource" - }, { "$ref": "DeviceManagerSource" } diff --git a/helm/blueapi/values.schema.json b/helm/blueapi/values.schema.json index 74deedadb2..344266485d 100644 --- a/helm/blueapi/values.schema.json +++ b/helm/blueapi/values.schema.json @@ -533,54 +533,6 @@ }, "additionalProperties": false }, - "DeviceSource": { - "$id": "DeviceSource", - "title": "DeviceSource", - "type": "object", - "required": [ - "module" - ], - "properties": { - "kind": { - "title": "Kind", - "default": "deviceFunctions", - "const": "deviceFunctions" - }, - "module": { - "title": "Module", - "description": "Module to be imported", - "type": "string" - } - }, - "additionalProperties": false - }, - "DodalSource": { - "$id": "DodalSource", - "title": "DodalSource", - "type": "object", - "required": [ - "module" - ], - "properties": { - "kind": { - "title": "Kind", - "default": "dodal", - "const": "dodal" - }, - "mock": { - "title": "Mock", - "description": "If true, ophyd_async device connections are mocked", - "default": false, - "type": "boolean" - }, - "module": { - "title": "Module", - "description": "Module to be imported", - "type": "string" - } - }, - "additionalProperties": false - }, "EnvironmentConfig": { "$id": "EnvironmentConfig", "title": "EnvironmentConfig", @@ -618,12 +570,6 @@ { "$ref": "PlanSource" }, - { - "$ref": "DeviceSource" - }, - { - "$ref": "DodalSource" - }, { "$ref": "DeviceManagerSource" } diff --git a/tests/unit_tests/valid_example_config/plans_and_devices.yaml b/tests/unit_tests/valid_example_config/plans_and_devices.yaml index d12ec638a0..fd85df93af 100644 --- a/tests/unit_tests/valid_example_config/plans_and_devices.yaml +++ b/tests/unit_tests/valid_example_config/plans_and_devices.yaml @@ -2,9 +2,9 @@ env: sources: - kind: planFunctions module: my_plan_library.tomography.plans - - kind: dodal + - kind: device_manager module: dodal.beamlines.i04 mock: true - - kind: deviceManager + - kind: device_manager module: dodal.beamlines.ixx mock: True From f9581f8ef09d58277b39df1ab8bc2d49732a444b Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Fri, 1 May 2026 17:13:38 +0100 Subject: [PATCH 03/11] Rest of dodal removal --- src/blueapi/config.py | 4 +- .../unit_tests/code_examples/device_module.py | 6 +- tests/unit_tests/core/fake_device_module.py | 87 +++++++++++++++++++ .../core/fake_device_module_failing.py | 4 + tests/unit_tests/core/test_context.py | 12 ++- .../example_beamline_with_path_provider.py | 16 ---- tests/unit_tests/service/test_interface.py | 64 -------------- tests/unit_tests/test_config.py | 11 +-- tests/unit_tests/test_example_code.py | 2 +- .../plans_and_devices.yaml | 4 +- tests/unit_tests/worker/devices.py | 6 +- tests/unit_tests/worker/test_task_worker.py | 6 +- 12 files changed, 119 insertions(+), 103 deletions(-) create mode 100644 tests/unit_tests/core/fake_device_module.py delete mode 100644 tests/unit_tests/service/example_beamline_with_path_provider.py diff --git a/src/blueapi/config.py b/src/blueapi/config.py index 25bbf204e0..6ce9e32118 100644 --- a/src/blueapi/config.py +++ b/src/blueapi/config.py @@ -71,7 +71,9 @@ class DeviceManagerSource(Source): description="If true, ophyd_async device connections are mocked", default=False ) name: str = Field( - default="devices", description="Name of the device manager in the module" + default="devices", + description="Name of the device manager in the module", + exclude_if=lambda v: v == "devices", ) diff --git a/tests/unit_tests/code_examples/device_module.py b/tests/unit_tests/code_examples/device_module.py index bdf3ac828a..6078401a62 100644 --- a/tests/unit_tests/code_examples/device_module.py +++ b/tests/unit_tests/code_examples/device_module.py @@ -1,7 +1,9 @@ -from dodal.common.beamlines.beamline_utils import device_factory +from dodal.device_manager import DeviceManager from dodal.devices.bimorph_mirror import BimorphMirror +devices = DeviceManager() -@device_factory(mock=True) + +@devices.factory(mock=True) def oav() -> BimorphMirror: return BimorphMirror("BLXXI-BMRPH-01:", number_of_channels=8) diff --git a/tests/unit_tests/core/fake_device_module.py b/tests/unit_tests/core/fake_device_module.py new file mode 100644 index 0000000000..48bf6dfd50 --- /dev/null +++ b/tests/unit_tests/core/fake_device_module.py @@ -0,0 +1,87 @@ +from unittest.mock import MagicMock, NonCallableMock + +from dodal.device_manager import DeviceManager, OphydV1Device, OphydV2Device +from ophyd_async.core import DEFAULT_TIMEOUT, LazyMock, StandardReadable +from ophyd_async.sim import SimMotor + +devices = DeviceManager() + + +@devices.factory(mock=True) +def fake_motor_bundle_b( + fake_motor_x: SimMotor, + fake_motor_y: SimMotor, +) -> SimMotor: + return SimMotor("motor_bundle_b") + + +@devices.factory(mock=True) +def fake_motor_x() -> SimMotor: + return SimMotor("motor_x") + + +class DeviceA(StandardReadable): + def __init__(self, name: str = "") -> None: + with self.add_children_as_readables(): + self.motor = SimMotor("X:SIZE") + super().__init__(name) + + +@devices.factory(mock=True) +def device_a() -> DeviceA: + return DeviceA() + + +class UnconnectableOphydDevice(OphydV1Device): + def wait_for_connection( + self, + all_signals: bool = False, + timeout: object = 2.0, + ) -> None: + raise RuntimeError(f"{self.name}: fake connection error for tests") + + +@devices.v1_init(UnconnectableOphydDevice, prefix="NOT:A:REAL:DEVICE") +def ophyd_device(dev: UnconnectableOphydDevice) -> UnconnectableOphydDevice: + return dev + + +class UnconnectableOphydAsyncDevice(OphydV2Device): + async def connect( + self, + mock: bool | LazyMock = False, + timeout: float = DEFAULT_TIMEOUT, + force_reconnect: bool = False, + ) -> None: + raise RuntimeError(f"{self.name}: fake connection error for tests") + + +@devices.factory +def ophyd_async_device() -> UnconnectableOphydAsyncDevice: + return UnconnectableOphydAsyncDevice(name="ophyd_async_device") + + +@devices.factory +def fake_motor_y() -> SimMotor: + return SimMotor("motor_y") + + +@devices.factory +def fake_motor_bundle_a( + fake_motor_x: SimMotor, + fake_motor_y: SimMotor, +) -> SimMotor: + return SimMotor("motor_bundle_a") + + +def wrong_return_type() -> int: + return "str" # type: ignore + + +fetchable_non_callable = NonCallableMock() +fetchable_callable = MagicMock(return_value="string") + +fetchable_non_callable.__name__ = "fetchable_non_callable" +fetchable_non_callable.__module__ = fake_motor_bundle_a.__module__ +fetchable_callable.__name__ = "fetchable_callable" +fetchable_callable.__module__ = fake_motor_bundle_a.__module__ diff --git a/tests/unit_tests/core/fake_device_module_failing.py b/tests/unit_tests/core/fake_device_module_failing.py index 1fb64c9451..cec0de526d 100644 --- a/tests/unit_tests/core/fake_device_module_failing.py +++ b/tests/unit_tests/core/fake_device_module_failing.py @@ -1,5 +1,9 @@ +from dodal.device_manager import DeviceManager from ophyd_async.epics.motor import Motor +devices = DeviceManager() + +@devices.factory def failing_device() -> Motor: raise TimeoutError("FooBar") diff --git a/tests/unit_tests/core/test_context.py b/tests/unit_tests/core/test_context.py index b38dc47f57..abea95be51 100644 --- a/tests/unit_tests/core/test_context.py +++ b/tests/unit_tests/core/test_context.py @@ -295,7 +295,7 @@ def test_add_devices_and_plans_from_modules_with_config( empty_context.with_config( EnvironmentConfig( sources=[ - DeviceSource( + DeviceManagerSource( module="tests.unit_tests.core.fake_device_module", ), PlanSource( @@ -305,13 +305,11 @@ def test_add_devices_and_plans_from_modules_with_config( ) ) assert { - "motor_x", - "motor_y", - "motor_bundle_a", - "motor_bundle_b", + "fake_motor_x", + "fake_motor_y", + "fake_motor_bundle_a", + "fake_motor_bundle_b", "device_a", - "ophyd_device", - "ophyd_async_device", } == empty_context.devices.keys() assert EXPECTED_PLANS == empty_context.plans.keys() diff --git a/tests/unit_tests/service/example_beamline_with_path_provider.py b/tests/unit_tests/service/example_beamline_with_path_provider.py deleted file mode 100644 index 83b016eec7..0000000000 --- a/tests/unit_tests/service/example_beamline_with_path_provider.py +++ /dev/null @@ -1,16 +0,0 @@ -from pathlib import Path - -from dodal.common.beamlines.beamline_utils import ( - set_path_provider, -) -from dodal.common.visit import LocalDirectoryServiceClient, StaticVisitPathProvider - -BL = "test" - -set_path_provider( - StaticVisitPathProvider( - BL, - Path("/tmp"), - client=LocalDirectoryServiceClient(), - ) -) diff --git a/tests/unit_tests/service/test_interface.py b/tests/unit_tests/service/test_interface.py index ccef2f0971..39115a9a21 100644 --- a/tests/unit_tests/service/test_interface.py +++ b/tests/unit_tests/service/test_interface.py @@ -9,11 +9,6 @@ from bluesky.protocols import Stoppable from bluesky.utils import MsgGenerator from bluesky_stomp.messaging import StompClient -from dodal.common.beamlines.beamline_utils import ( - clear_path_provider, - get_path_provider, - set_path_provider, -) from ophyd_async.epics.motor import Motor from pydantic import HttpUrl from pytest_httpx import HTTPXMock @@ -25,7 +20,6 @@ MetadataConfig, NumtrackerConfig, OIDCConfig, - PlanSource, ScratchConfig, StompConfig, TiledConfig, @@ -44,7 +38,6 @@ ) from blueapi.utils.invalid_config_error import InvalidConfigError from blueapi.utils.numtracker import NumtrackerClient -from blueapi.utils.path_provider import StartDocumentPathProvider from blueapi.worker.event import ( TaskResult, TaskStatus, @@ -613,63 +606,6 @@ def test_numtracker_requires_instrument_metadata(): interface.set_config(ApplicationConfig()) -def test_setup_without_numtracker_with_existing_provider_does_not_overwrite_provider(): - conf = ApplicationConfig() - mock_provider = Mock() - set_path_provider(mock_provider) - - assert get_path_provider() == mock_provider - interface.setup(conf) - assert get_path_provider() == mock_provider - - clear_path_provider() - - -def test_setup_without_numtracker_without_existing_provider_does_not_make_one(): - conf = ApplicationConfig() - interface.setup(conf) - - with pytest.raises(NameError): - get_path_provider() - - -def test_setup_with_numtracker_makes_start_document_provider(): - conf = ApplicationConfig( - env=EnvironmentConfig(metadata=MetadataConfig(instrument="p46")), - numtracker=NumtrackerConfig(), - ) - interface.setup(conf) - - path_provider = get_path_provider() - - assert isinstance(path_provider, StartDocumentPathProvider) - - clear_path_provider() - - -def test_setup_with_numtracker_raises_if_provider_is_defined_in_device_module(): - conf = ApplicationConfig( - env=EnvironmentConfig( - sources=[ - PlanSource( - module="tests.unit_tests.service.example_beamline_with_path_provider", - ), - ], - metadata=MetadataConfig(instrument="p46"), - ), - numtracker=NumtrackerConfig(), - ) - - with pytest.raises( - InvalidConfigError, - match="Numtracker has been configured but a path provider was imported" - " with the devices. Remove this path provider to use numtracker.", - ): - interface.setup(conf) - - clear_path_provider() - - @patch("blueapi.utils.numtracker.NumtrackerClient.create_scan") async def test_numtracker_create_scan_called_with_arguments_from_metadata( mock_create_scan, diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py index 30fe551c4c..b3840b9dcc 100644 --- a/tests/unit_tests/test_config.py +++ b/tests/unit_tests/test_config.py @@ -230,7 +230,8 @@ def temp_yaml_config_file( { "env": { "sources": [ - {"kind": "dodal", "module": "dodal.adsim"}, + {"kind": "deviceManager", "module": "dodal.adsim"}, + {"kind": "deviceManager", "module": "dodal.ixx", "name": "manager"}, {"kind": "planFunctions", "module": "dodal.plans"}, {"kind": "planFunctions", "module": "dodal.plan_stubs.wrapped"}, ], @@ -241,7 +242,7 @@ def temp_yaml_config_file( "stomp": {"enabled": True}, "env": { "sources": [ - {"kind": "dodal", "module": "dodal.adsim"}, + {"kind": "deviceManager", "module": "dodal.adsim"}, {"kind": "planFunctions", "module": "dodal.plans"}, {"kind": "planFunctions", "module": "dodal.plan_stubs.wrapped"}, ], @@ -301,7 +302,7 @@ def test_config_yaml_parsed(temp_yaml_config_file): "instrument": "p01", }, "sources": [ - {"kind": "dodal", "module": "dodal.adsim", "mock": True}, + {"kind": "deviceManager", "module": "dodal.adsim", "mock": True}, {"kind": "planFunctions", "module": "dodal.plans"}, { "kind": "planFunctions", @@ -352,7 +353,7 @@ def test_config_yaml_parsed(temp_yaml_config_file): "auth_token_path": None, "env": { "sources": [ - {"kind": "dodal", "module": "dodal.adsim", "mock": False}, + {"kind": "deviceManager", "module": "dodal.adsim", "mock": False}, {"kind": "planFunctions", "module": "dodal.plans"}, { "kind": "planFunctions", @@ -433,7 +434,7 @@ def test_config_yaml_parsed_complete(temp_yaml_config_file: dict): "auth_token_path": None, "env": { "sources": [ - {"kind": "dodal", "module": "dodal.adsim"}, + {"kind": "deviceManager", "module": "dodal.adsim"}, {"kind": "planFunctions", "module": "dodal.plans"}, {"kind": "planFunctions", "module": "dodal.plan_stubs.wrapped"}, ], diff --git a/tests/unit_tests/test_example_code.py b/tests/unit_tests/test_example_code.py index e7d8547c89..0d16a871f7 100644 --- a/tests/unit_tests/test_example_code.py +++ b/tests/unit_tests/test_example_code.py @@ -31,7 +31,7 @@ def test_example_device_module_is_detectable(): context = BlueskyContext() module = importlib.import_module(module_name) - # context.with_dodal_module(module) + context.with_device_manager(module.devices) assert device_name in context.devices diff --git a/tests/unit_tests/valid_example_config/plans_and_devices.yaml b/tests/unit_tests/valid_example_config/plans_and_devices.yaml index fd85df93af..c8cd7012b0 100644 --- a/tests/unit_tests/valid_example_config/plans_and_devices.yaml +++ b/tests/unit_tests/valid_example_config/plans_and_devices.yaml @@ -2,9 +2,9 @@ env: sources: - kind: planFunctions module: my_plan_library.tomography.plans - - kind: device_manager + - kind: deviceManager module: dodal.beamlines.i04 mock: true - - kind: device_manager + - kind: deviceManager module: dodal.beamlines.ixx mock: True diff --git a/tests/unit_tests/worker/devices.py b/tests/unit_tests/worker/devices.py index a291c950e8..23d5f56cfe 100644 --- a/tests/unit_tests/worker/devices.py +++ b/tests/unit_tests/worker/devices.py @@ -1,7 +1,9 @@ -from dodal.common.beamlines.beamline_utils import device_factory +from dodal.device_manager import DeviceManager from ophyd_async.epics.motor import Motor +devices = DeviceManager() -@device_factory(mock=True) + +@devices.factory(mock=True) def motor() -> Motor: return Motor("FOO:") diff --git a/tests/unit_tests/worker/test_task_worker.py b/tests/unit_tests/worker/test_task_worker.py index 866c52aa0a..41041c5018 100644 --- a/tests/unit_tests/worker/test_task_worker.py +++ b/tests/unit_tests/worker/test_task_worker.py @@ -20,7 +20,7 @@ ) from ophyd_async.core import AsyncStatus -from blueapi.config import EnvironmentConfig +from blueapi.config import DeviceManagerSource, EnvironmentConfig from blueapi.core import BlueskyContext, EventStream from blueapi.core.bluesky_types import DataEvent from blueapi.service.model import PlanModel @@ -110,7 +110,7 @@ def second_fake_device() -> FakeDevice: def context(fake_device: FakeDevice, second_fake_device: FakeDevice) -> BlueskyContext: ctx = BlueskyContext() ctx_config = EnvironmentConfig() - # ctx_config.sources.append(DeviceSource(module="devices")) + ctx_config.sources.append(DeviceManagerSource(module="devices")) ctx.register_plan(failing_plan) ctx.register_device(fake_device) ctx.register_device(second_fake_device) @@ -122,7 +122,7 @@ def context(fake_device: FakeDevice, second_fake_device: FakeDevice) -> BlueskyC def context_without_devices() -> BlueskyContext: ctx = BlueskyContext() ctx_config = EnvironmentConfig() - # ctx_config.sources.append(DeviceSource(module="devices")) + ctx_config.sources.append(DeviceManagerSource(module="devices")) ctx.with_config(ctx_config) return ctx From 8563f600ddc84d79583a1f5d7a95753cfb9ce760 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Fri, 1 May 2026 17:14:24 +0100 Subject: [PATCH 04/11] Move dodal to dev-dependencies --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 659779994a..702357ee8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,6 @@ dependencies = [ "fastapi>=0.112.0", "uvicorn", "requests", - "dls-dodal>=1.69.0", "super-state-machine", # https://github.com/DiamondLightSource/blueapi/issues/553 "GitPython", "event-model==1.23.1", # https://github.com/DiamondLightSource/blueapi/issues/684 @@ -47,6 +46,7 @@ requires-python = ">=3.11" dev = [ "ophyd_async[sim]", "copier", + "dls-dodal>=1.69.0", "myst-parser", "prek", "pydata-sphinx-theme>=0.15.4", From 94a55cb9fffb04d78be179b1c0dd801818c3bd31 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Fri, 1 May 2026 17:15:25 +0100 Subject: [PATCH 05/11] Remove source variants --- src/blueapi/config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/blueapi/config.py b/src/blueapi/config.py index 6ce9e32118..5790bd325e 100644 --- a/src/blueapi/config.py +++ b/src/blueapi/config.py @@ -48,8 +48,6 @@ def _expand_env(loader: yaml.Loader, node: yaml.ScalarNode) -> str: class SourceKind(StrEnum): PLAN_FUNCTIONS = "planFunctions" - DEVICE_FUNCTIONS = "deviceFunctions" - DODAL = "dodal" DEVICE_MANAGER = "deviceManager" From 04f4299368758e02a50f5e7a6675b77bd6026cbf Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Fri, 1 May 2026 17:16:34 +0100 Subject: [PATCH 06/11] Use local definition of plan generator --- src/blueapi/core/bluesky_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blueapi/core/bluesky_types.py b/src/blueapi/core/bluesky_types.py index a15b4752e2..dad97561f9 100644 --- a/src/blueapi/core/bluesky_types.py +++ b/src/blueapi/core/bluesky_types.py @@ -24,12 +24,12 @@ WritesExternalAssets, ) from bluesky.utils import Msg, MsgGenerator -from dodal.common import PlanGenerator from ophyd_async.core import Device as AsyncDevice from pydantic import BaseModel, Field from blueapi.utils import BlueapiBaseModel +PlanGenerator = Callable[..., MsgGenerator] PlanWrapper = Callable[[MsgGenerator], MsgGenerator] #: An object that encapsulates the device to do useful things to produce From db78f04fac622202540dfb7b4f38b93e46c24de5 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Fri, 1 May 2026 17:23:38 +0100 Subject: [PATCH 07/11] Remove unused planwrpper --- src/blueapi/core/bluesky_types.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/blueapi/core/bluesky_types.py b/src/blueapi/core/bluesky_types.py index dad97561f9..d05904a926 100644 --- a/src/blueapi/core/bluesky_types.py +++ b/src/blueapi/core/bluesky_types.py @@ -30,7 +30,6 @@ from blueapi.utils import BlueapiBaseModel PlanGenerator = Callable[..., MsgGenerator] -PlanWrapper = Callable[[MsgGenerator], MsgGenerator] #: An object that encapsulates the device to do useful things to produce # data (e.g. move and read) From fee6efba4770bb12223a36d2141e15d9ad4336db Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Fri, 1 May 2026 17:41:03 +0100 Subject: [PATCH 08/11] Update lock file --- uv.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index 2ea1869e05..ed3601da30 100644 --- a/uv.lock +++ b/uv.lock @@ -423,7 +423,6 @@ dependencies = [ { name = "bluesky", extra = ["plotting"] }, { name = "bluesky-stomp" }, { name = "click" }, - { name = "dls-dodal" }, { name = "event-model" }, { name = "fastapi" }, { name = "gitpython" }, @@ -449,6 +448,7 @@ dependencies = [ dev = [ { name = "copier" }, { name = "deepdiff" }, + { name = "dls-dodal" }, { name = "jwcrypto" }, { name = "mock" }, { name = "myst-parser" }, @@ -484,7 +484,6 @@ requires-dist = [ { name = "bluesky", extras = ["plotting"], specifier = ">=1.14.0" }, { name = "bluesky-stomp", specifier = ">=0.1.6" }, { name = "click", specifier = ">=8.2.0" }, - { name = "dls-dodal", specifier = ">=1.69.0" }, { name = "event-model", specifier = "==1.23.1" }, { name = "fastapi", specifier = ">=0.112.0" }, { name = "gitpython" }, @@ -510,6 +509,7 @@ requires-dist = [ dev = [ { name = "copier" }, { name = "deepdiff" }, + { name = "dls-dodal", specifier = ">=1.69.0" }, { name = "jwcrypto" }, { name = "mock" }, { name = "myst-parser" }, From 0254371ae4b7d286b1a223ef2a5adfe0d1e8d02e Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Fri, 1 May 2026 17:53:48 +0100 Subject: [PATCH 09/11] Put report_successful_devices back where it was --- src/blueapi/utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blueapi/utils/__init__.py b/src/blueapi/utils/__init__.py index 9555571549..7743d07b4f 100644 --- a/src/blueapi/utils/__init__.py +++ b/src/blueapi/utils/__init__.py @@ -20,9 +20,9 @@ "BlueapiPlanModelConfig", "InvalidConfigError", "NumtrackerClient", + "report_successful_devices", "is_sgid_set", "get_owner_gid", - "report_successful_devices", "is_function_sourced_from_module", "deprecated", ] From 9152766f2e673b6254c80e39259e3150922adb1e Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Fri, 1 May 2026 18:19:21 +0100 Subject: [PATCH 10/11] Reinstate removed tests --- tests/unit_tests/core/test_context.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/unit_tests/core/test_context.py b/tests/unit_tests/core/test_context.py index abea95be51..23462a1480 100644 --- a/tests/unit_tests/core/test_context.py +++ b/tests/unit_tests/core/test_context.py @@ -31,6 +31,7 @@ from ophyd_async.epics.motor import Motor from pydantic import TypeAdapter, ValidationError from pydantic.json_schema import SkipJsonSchema +from pytest import LogCaptureFixture from blueapi.config import ( ApplicationConfig, @@ -258,6 +259,32 @@ def test_override_device_name(empty_context: BlueskyContext, sim_motor: Motor): assert empty_context.devices["foo"] is sim_motor +def test_add_devices_from_module(empty_context: BlueskyContext): + import tests.unit_tests.core.fake_device_module as device_module + + empty_context.with_device_manager(device_module.devices) # type: ignore - protocol uses Any to avoid dependency on dodal + assert { + "fake_motor_x", + "fake_motor_y", + "fake_motor_bundle_a", + "fake_motor_bundle_b", + "device_a", + } == empty_context.devices.keys() + + +def test_add_failing_devices_from_module( + caplog: LogCaptureFixture, empty_context: BlueskyContext +): + import tests.unit_tests.core.fake_device_module_failing as device_module + + caplog.set_level(10) + empty_context.with_device_manager(device_module.devices) # type: ignore - protocol uses Any to avoid dependency on dodal + logs = caplog.get_records("call") + + assert any("TimeoutError('FooBar')" in log.message for log in logs) + assert len(empty_context.devices.keys()) == 0 + + @pytest.mark.parametrize( "addr", ["sim", "sim_det", "sim.user_setpoint", ["sim"], ["sim", "user_setpoint"]] ) From 44b9c16053d768d27290e1c7770529ce41b770dd Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Fri, 1 May 2026 18:22:02 +0100 Subject: [PATCH 11/11] Update dodal reference in docs --- docs/how-to/add-plans-and-devices.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-to/add-plans-and-devices.md b/docs/how-to/add-plans-and-devices.md index 35f09ff7b4..702497fe2f 100644 --- a/docs/how-to/add-plans-and-devices.md +++ b/docs/how-to/add-plans-and-devices.md @@ -26,7 +26,7 @@ To add plans, you would add the following into your configuration file: ``` -Devices are added similarly, using `dodal` as the `kind`, like so: +Devices are added similarly, using `deviceManager` as the `kind`, like so: ```{literalinclude} ../../tests/unit_tests/valid_example_config/plans_and_devices.yaml :language: yaml ```