diff --git a/src/blueapi/core/context.py b/src/blueapi/core/context.py index 50623967e..219e736b7 100644 --- a/src/blueapi/core/context.py +++ b/src/blueapi/core/context.py @@ -7,14 +7,13 @@ from typing import Any, Generic, TypeVar, Union, get_args, get_origin, get_type_hints from bluesky.run_engine import RunEngine +from dodal.utils import make_all_devices +from ophyd_async.core import NotConnected from pydantic import create_model from pydantic.fields import FieldInfo, ModelField from blueapi.config import EnvironmentConfig, SourceKind -from blueapi.utils import ( - BlueapiPlanModelConfig, - load_module_all, -) +from blueapi.utils import BlueapiPlanModelConfig, load_module_all from .bluesky_types import ( BLUESKY_PROTOCOLS, @@ -104,11 +103,19 @@ def with_device_module(self, module: ModuleType) -> None: self.with_dodal_module(module) def with_dodal_module(self, module: ModuleType, **kwargs) -> None: - from dodal.utils import make_all_devices + devices, exceptions = make_all_devices(module, **kwargs) - for device in make_all_devices(module, **kwargs).values(): + for device in devices.values(): self.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(NotConnected(exceptions)) + def 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/startup/example_devices.py b/src/blueapi/startup/example_devices.py index 2244071d7..a472137dd 100644 --- a/src/blueapi/startup/example_devices.py +++ b/src/blueapi/startup/example_devices.py @@ -72,3 +72,10 @@ def current_det( Imax=1, labels={"detectors"}, ) + + +def unplugged_motor(name="unplugged_motor") -> SynAxisWithMotionEvents: + raise TimeoutError( + "This device is supposed to fail, blueapi " + "will mark it as not present and start up regardless" + ) diff --git a/tests/core/fake_device_module_failing.py b/tests/core/fake_device_module_failing.py new file mode 100644 index 000000000..80fb97d1c --- /dev/null +++ b/tests/core/fake_device_module_failing.py @@ -0,0 +1,5 @@ +from ophyd import EpicsMotor + + +def failing_device() -> EpicsMotor: + raise TimeoutError("FooBar") diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 4e6583209..aeb68f31f 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -8,6 +8,7 @@ from dls_bluesky_core.core import MsgGenerator, PlanGenerator, inject from ophyd.sim import SynAxis, SynGauss from pydantic import ValidationError, parse_obj_as +from pytest import LogCaptureFixture from blueapi.config import EnvironmentConfig, Source, SourceKind from blueapi.core import BlueskyContext, is_bluesky_compatible_device @@ -174,6 +175,19 @@ def test_add_devices_from_module(empty_context: BlueskyContext) -> None: } == empty_context.devices.keys() +def test_add_failing_deivces_from_module( + caplog: LogCaptureFixture, empty_context: BlueskyContext +) -> None: + import 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, ) -> None: @@ -182,7 +196,10 @@ def test_extra_kwargs_in_with_dodal_module_passed_to_make_all_devices( """ import tests.core.fake_device_module as device_module - with patch("dodal.utils.make_all_devices") as mock_make_all_devices: + 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" )