From 7b5c008c156016d2a913ee9d28a51c4f87f5d4eb Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Mon, 8 Dec 2025 18:36:01 +0000 Subject: [PATCH 1/4] Add include method to DeviceManager Allow one device manager to include all the devices and fixtures defined by another. Intended to allow modules of shared devices to be used by multiple beamlines, for instance two branch lines could share devices for common optics. --- src/dodal/device_manager.py | 14 +++++++++++++ tests/test_device_manager.py | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/dodal/device_manager.py b/src/dodal/device_manager.py index 2eb490dfd27..4099b9c652d 100644 --- a/src/dodal/device_manager.py +++ b/src/dodal/device_manager.py @@ -360,6 +360,20 @@ def fixture(self, func: Callable[[], T]) -> Callable[[], T]: self._fixtures[func.__name__] = func return func + def include(self, other: "DeviceManager"): + common = self._factories.keys() & other._factories # noqa SLF001 + common |= self._v1_factories.keys() & other._v1_factories # noqa SLF001 + common |= self._factories.keys() & other._v1_factories # noqa SLF001 + common |= self._v1_factories.keys() & other._factories # noqa SLF001 + if common: + raise ValueError(f"Duplicate factories in included device manager: {common}") + + self._factories.update(other._factories) # noqa SLF001 + self._v1_factories.update(other._v1_factories) # noqa SLF001 + + # duplicate fixtures are not checked as fixtures can be overridden + self._fixtures.update(other._fixtures) # noqa SLF001 + def v1_init( self, factory: type[V1], diff --git a/tests/test_device_manager.py b/tests/test_device_manager.py index afb4243f825..1643d94cb5b 100644 --- a/tests/test_device_manager.py +++ b/tests/test_device_manager.py @@ -693,6 +693,46 @@ def foo(s, one, two): s1().set_up_with.assert_called_once_with("one", 2) +def test_inherited_device_manager(dm: DeviceManager): + s1 = Mock(return_value=Mock(spec=OphydV2Device)) + s2 = Mock(return_value=Mock(spec=OphydV2Device)) + + @dm.factory + def foo(): + return s1() + + dm2 = DeviceManager() + + @dm2.factory + def bar(foo): + return s2(foo) + + dm2.include(dm) + + built_bar = bar.build() + s1.assert_called_once() + s2.assert_called_once_with(s1()) + assert built_bar is s2(s1()) + +def test_inherited_device_manager_duplicate_name(): + s1 = Mock(return_value=Mock(spec=OphydV2Device)) + s2 = Mock(return_value=Mock(spec=OphydV2Device)) + + dm = DeviceManager() + dm2 = DeviceManager() + + @dm.factory + def foo(): + return s1() + + @dm2.factory + def foo(): + return s2() + + with pytest.raises(ValueError, match="{'foo'}"): + dm.include(dm2) + + def test_lazy_fixtures_non_lazy(): lf = LazyFixtures(provided={"foo": "bar"}, factories={}) assert lf["foo"] == "bar" From 00027a3a5a99e6a98164fddac0d63ee67c1f4509 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Mon, 8 Dec 2025 18:36:01 +0000 Subject: [PATCH 2/4] Add include method to DeviceManager Allow one device manager to include all the devices and fixtures defined by another. Intended to allow modules of shared devices to be used by multiple beamlines, for instance two branch lines could share devices for common optics. --- src/dodal/device_manager.py | 14 +++++++++++++ tests/test_device_manager.py | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/dodal/device_manager.py b/src/dodal/device_manager.py index 2eb490dfd27..4099b9c652d 100644 --- a/src/dodal/device_manager.py +++ b/src/dodal/device_manager.py @@ -360,6 +360,20 @@ def fixture(self, func: Callable[[], T]) -> Callable[[], T]: self._fixtures[func.__name__] = func return func + def include(self, other: "DeviceManager"): + common = self._factories.keys() & other._factories # noqa SLF001 + common |= self._v1_factories.keys() & other._v1_factories # noqa SLF001 + common |= self._factories.keys() & other._v1_factories # noqa SLF001 + common |= self._v1_factories.keys() & other._factories # noqa SLF001 + if common: + raise ValueError(f"Duplicate factories in included device manager: {common}") + + self._factories.update(other._factories) # noqa SLF001 + self._v1_factories.update(other._v1_factories) # noqa SLF001 + + # duplicate fixtures are not checked as fixtures can be overridden + self._fixtures.update(other._fixtures) # noqa SLF001 + def v1_init( self, factory: type[V1], diff --git a/tests/test_device_manager.py b/tests/test_device_manager.py index afb4243f825..1643d94cb5b 100644 --- a/tests/test_device_manager.py +++ b/tests/test_device_manager.py @@ -693,6 +693,46 @@ def foo(s, one, two): s1().set_up_with.assert_called_once_with("one", 2) +def test_inherited_device_manager(dm: DeviceManager): + s1 = Mock(return_value=Mock(spec=OphydV2Device)) + s2 = Mock(return_value=Mock(spec=OphydV2Device)) + + @dm.factory + def foo(): + return s1() + + dm2 = DeviceManager() + + @dm2.factory + def bar(foo): + return s2(foo) + + dm2.include(dm) + + built_bar = bar.build() + s1.assert_called_once() + s2.assert_called_once_with(s1()) + assert built_bar is s2(s1()) + +def test_inherited_device_manager_duplicate_name(): + s1 = Mock(return_value=Mock(spec=OphydV2Device)) + s2 = Mock(return_value=Mock(spec=OphydV2Device)) + + dm = DeviceManager() + dm2 = DeviceManager() + + @dm.factory + def foo(): + return s1() + + @dm2.factory + def foo(): + return s2() + + with pytest.raises(ValueError, match="{'foo'}"): + dm.include(dm2) + + def test_lazy_fixtures_non_lazy(): lf = LazyFixtures(provided={"foo": "bar"}, factories={}) assert lf["foo"] == "bar" From 48dadac61f20659fb0e4850ec8ca013f8973e36b Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Thu, 11 Dec 2025 10:00:39 +0000 Subject: [PATCH 3/4] Fix lints --- src/dodal/device_manager.py | 18 ++++++++++-------- tests/test_device_manager.py | 1 + 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/dodal/device_manager.py b/src/dodal/device_manager.py index 4099b9c652d..1c99792cd4f 100644 --- a/src/dodal/device_manager.py +++ b/src/dodal/device_manager.py @@ -361,18 +361,20 @@ def fixture(self, func: Callable[[], T]) -> Callable[[], T]: return func def include(self, other: "DeviceManager"): - common = self._factories.keys() & other._factories # noqa SLF001 - common |= self._v1_factories.keys() & other._v1_factories # noqa SLF001 - common |= self._factories.keys() & other._v1_factories # noqa SLF001 - common |= self._v1_factories.keys() & other._factories # noqa SLF001 + common = self._factories.keys() & other._factories # noqa SLF001 + common |= self._v1_factories.keys() & other._v1_factories # noqa SLF001 + common |= self._factories.keys() & other._v1_factories # noqa SLF001 + common |= self._v1_factories.keys() & other._factories # noqa SLF001 if common: - raise ValueError(f"Duplicate factories in included device manager: {common}") + raise ValueError( + f"Duplicate factories in included device manager: {common}" + ) - self._factories.update(other._factories) # noqa SLF001 - self._v1_factories.update(other._v1_factories) # noqa SLF001 + self._factories.update(other._factories) # noqa SLF001 + self._v1_factories.update(other._v1_factories) # noqa SLF001 # duplicate fixtures are not checked as fixtures can be overridden - self._fixtures.update(other._fixtures) # noqa SLF001 + self._fixtures.update(other._fixtures) # noqa SLF001 def v1_init( self, diff --git a/tests/test_device_manager.py b/tests/test_device_manager.py index 1643d94cb5b..e8e75132fe1 100644 --- a/tests/test_device_manager.py +++ b/tests/test_device_manager.py @@ -714,6 +714,7 @@ def bar(foo): s2.assert_called_once_with(s1()) assert built_bar is s2(s1()) + def test_inherited_device_manager_duplicate_name(): s1 = Mock(return_value=Mock(spec=OphydV2Device)) s2 = Mock(return_value=Mock(spec=OphydV2Device)) From 2580252d16f7751c86755915f21be6c5c823671a Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Thu, 11 Dec 2025 10:11:49 +0000 Subject: [PATCH 4/4] Fix lint by updating test_inherited_device_manager_duplicate_name by tagging foo with two device managers --- tests/test_device_manager.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_device_manager.py b/tests/test_device_manager.py index e8e75132fe1..fca3fdf09a1 100644 --- a/tests/test_device_manager.py +++ b/tests/test_device_manager.py @@ -716,19 +716,15 @@ def bar(foo): def test_inherited_device_manager_duplicate_name(): - s1 = Mock(return_value=Mock(spec=OphydV2Device)) - s2 = Mock(return_value=Mock(spec=OphydV2Device)) + device = Mock(return_value=Mock(spec=OphydV2Device)) dm = DeviceManager() dm2 = DeviceManager() @dm.factory - def foo(): - return s1() - @dm2.factory def foo(): - return s2() + return device() with pytest.raises(ValueError, match="{'foo'}"): dm.include(dm2)