Skip to content

Allow per-type mock overrides anywhere in a device tree - #1446

Open
coretl wants to merge 1 commit into
bluesky:mainfrom
coretl:mockfactory
Open

coretl wants to merge 1 commit into
bluesky:mainfrom
coretl:mockfactory

Conversation

@coretl

@coretl coretl commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Allow the mock class to be chosen per device type anywhere in a device tree, including children that only exist after connect (PVI, Tango, Command).

Supersedes #1429, which lets init_devices choose mocks for the devices it creates but cannot reach children further down the tree.

Usage

with init_devices(mock={Motor: VeloAndAcclRespectingMotorMock}):
    motor = Motor("PREFIX:")             # gets VeloAndAcclRespectingMotorMock
    stage = ThingWithMotors("PREFIX:")   # so do its Motor children

ensure_connected(..., mock={...}) takes the same argument. Both validate mock at runtime and raise TypeError for a bad one; Device.connect is checked by static analysis only.

Behaviour changes

  • init_devices(mock=...), ensure_connected(mock=...) and the new connect_devices take bool | Mapping[type[Device], bool | type[DeviceMock]], so you can mix real and mocked Devices anywhere in a tree (e.g. {Device: True, Motor: False}). An entry under Device is the default for the first level only. ensure_connected no longer accepts a DeviceMock instance.
  • Device.connect(mock=...) takes bool | DeviceMock; a DeviceMock instance's mocked and mock_types decide the Device and its children.
  • DeviceMock.__init__(name="", parent=None, mocked=True, *, mock_types=None). A real Device holds a DeviceMock too, so get_mock works on any connected Device and raises if its last connect has not succeeded.
  • DeviceConnector.connect_mock and DeviceConnector.connect_real are replaced by one DeviceConnector.connect(device, mock, timeout, force_reconnect); overrides branch on mock.mocked and call super().connect(...). An out-of-tree connector that still overrides the old names is silently ignored, so its backend never connects: rename the override.
  • Mocked connects are now cached like real ones. A second connect(mock=True) with the same choice keeps mock values and call records; pass force_reconnect=True to rebuild. Any connect that is not cached connects everything below it again.
  • Children connect in parallel with the caller's timeout and force_reconnect, also under a mocked parent.
  • The DeviceMock.connect hook now runs for Signals and Commands too.
  • LazyMock is deprecated; use DeviceMock.

AI Disclaimer

The code and this description were generated with AI and reviewed/tweaked by the author.

@coretl

coretl commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Converting back to draft, I've got a simplified design in the works, I'll re-request review when it's ready...

init_devices(mock=...), connect_devices and ensure_connected take
bool | Mapping[type[Device], bool | type[DeviceMock]], and
Device.connect(mock=...) also accepts a DeviceMock instance. Each Device
looks itself up by the nearest class in its MRO. True uses the Device's
default mock, a DeviceMock subclass uses that class, False connects for
real. An entry under Device is the default for the first level only;
children otherwise inherit their parent's decision unless they have
their own entry. Signal and Command types cannot be keys (TypeError):
they always follow their parent Device. This is the API agreed in bluesky#1429.

- DeviceConnector.connect(device, mock, timeout, force_reconnect)
  replaces connect_real and connect_mock; connectors branch on
  mock.mocked. Out-of-tree overrides of the old names are no longer
  called.
- DeviceMock(name="", parent=None, mocked=True, *, mock_types=None)
- Children connect in parallel with the caller's timeout and
  force_reconnect, also under a mocked parent; when nothing below can
  be real they connect serially, which is faster for large mock trees
- Device.connect caches mocked as well as real connects (same mock
  class, mocked and mock_types, last connect succeeded); an uncached
  connect connects everything below it again. A second connect(mock=True)
  keeps mock values and call records; force_reconnect=True rebuilds them
- get_mock raises if the last connect has not succeeded
- DeviceMock.connect hook runs after the connector, so Signal and
  Command mocks get it too; SignalR drops its cache when its backend is
  replaced
- LazyMock deprecated via DeprecationWarning
- docs: per-type overrides, the default mock rule, caching and
  re-connect caveats
- CLAUDE.md: keep each CI unit/system test stage to about a minute

Assisted-by: Claude Opus 5.5
@coretl
coretl marked this pull request as ready for review November 11, 2026 14:04
@coretl

coretl commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

@jwlodek this is now ready to review

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant