2121from blueapi .config import EnvironmentConfig , Source , SourceKind
2222from blueapi .core import BlueskyContext , EventStream
2323from blueapi .core .bluesky_types import DataEvent
24+ from blueapi .utils .base_model import BlueapiBaseModel
2425from blueapi .worker import (
2526 Task ,
2627 TaskStatus ,
5354class FakeDevice (Movable [float ]):
5455 event : threading .Event
5556
56- @property
57- def name (self ) -> str :
58- return "fake_device"
59-
60- def __init__ (self ) -> None :
57+ def __init__ (self , name : str = "fake_device" ) -> None :
6158 self .event = threading .Event ()
59+ self .name = name
6260
6361 def set (self , value : float ) -> Status :
6462 def when_done (_ : Status ):
@@ -84,14 +82,31 @@ def fake_device() -> FakeDevice:
8482
8583
8684@pytest .fixture
87- def context (fake_device : FakeDevice ) -> BlueskyContext :
85+ def second_fake_device () -> FakeDevice :
86+ return FakeDevice ("second_fake_device" )
87+
88+
89+ @pytest .fixture
90+ def context (fake_device : FakeDevice , second_fake_device : FakeDevice ) -> BlueskyContext :
8891 ctx = BlueskyContext ()
8992 ctx_config = EnvironmentConfig ()
9093 ctx_config .sources .append (
9194 Source (kind = SourceKind .DEVICE_FUNCTIONS , module = "devices" )
9295 )
9396 ctx .register_plan (failing_plan )
9497 ctx .register_device (fake_device )
98+ ctx .register_device (second_fake_device )
99+ ctx .with_config (ctx_config )
100+ return ctx
101+
102+
103+ @pytest .fixture
104+ def context_without_devices () -> BlueskyContext :
105+ ctx = BlueskyContext ()
106+ ctx_config = EnvironmentConfig ()
107+ ctx_config .sources .append (
108+ Source (kind = SourceKind .DEVICE_FUNCTIONS , module = "devices" )
109+ )
95110 ctx .with_config (ctx_config )
96111 return ctx
97112
@@ -681,3 +696,38 @@ def test_cycle_without_otel_context(mock_logger: Mock, inert_worker: TaskWorker)
681696 task .is_complete = False
682697 task .is_pending = True
683698 mock_logger .info .assert_called_with (f"Got new task: { task } " )
699+
700+
701+ class MyComposite (BlueapiBaseModel ):
702+ dev_a : FakeDevice = inject (fake_device .name )
703+ dev_b : FakeDevice = inject (second_fake_device .name )
704+
705+ model_config = {"arbitrary_types_allowed" : True }
706+
707+
708+ def injected_device_plan (composite : MyComposite = inject ("" )) -> MsgGenerator :
709+ yield from ()
710+
711+
712+ def test_injected_composite_devices_are_found (
713+ fake_device : FakeDevice ,
714+ second_fake_device : FakeDevice ,
715+ context : BlueskyContext ,
716+ ):
717+ context .register_plan (injected_device_plan )
718+ params = Task (name = "injected_device_plan" ).prepare_params (context )
719+ assert params ["composite" ].dev_a == fake_device
720+ assert params ["composite" ].dev_b == second_fake_device
721+
722+
723+ def test_plan_module_with_composite_devices_can_be_loaded_before_device_module (
724+ context_without_devices : BlueskyContext ,
725+ fake_device : FakeDevice ,
726+ second_fake_device : FakeDevice ,
727+ ):
728+ context_without_devices .register_plan (injected_device_plan )
729+ context_without_devices .register_device (fake_device )
730+ context_without_devices .register_device (second_fake_device )
731+ params = Task (name = "injected_device_plan" ).prepare_params (context_without_devices )
732+ assert params ["composite" ].dev_a == fake_device
733+ assert params ["composite" ].dev_b == second_fake_device
0 commit comments