Skip to content

Update init_devices context manager to allow for granular selection of device mocks - #1429

Closed
jwlodek wants to merge 2 commits into
bluesky:mainfrom
jwlodek:init-devices-mock-options
Closed

jwlodek wants to merge 2 commits into
bluesky:mainfrom
jwlodek:init-devices-mock-options

Conversation

@jwlodek

@jwlodek jwlodek commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Updates init_devices to allow for more granular selection of device mocks, as discussed in #1344.

Now, one can pass in a type[DeviceMock], or a dict[type[Device], bool | type[DeviceMock]] instead of just a bool.

@jwlodek

jwlodek commented Sep 30, 2026 •

Copy link
Copy Markdown
Member Author

Question for reviewers. In the case where we pass just a type[DeviceMock], we can use what it's generic on to figure out whether a device can or cannot be mocked with it. In the event that there is a device that doesn't support it, should we fall back to a default mock, or connect for real?

For example, if I want to use a specific mock for Motor, but maybe default everything else I can use the dict option:

{Device: True, Motor: VelocityRespectingMotorMock}

should passing just VelocityRespectingMotorMock have the same behavior as above, or should it do

# extract `Motor` from VelocityRespectingMotorMock(DeviceMock[Motor])

{Motor: VelocityRespectingMotorMock}

with non-supported devices connecting for real?

For now I've made it work like the former. If the latter behavior is desired one can always fall back to the dict option.

@jwlodek
jwlodek requested a review from thopkins32 September 30, 2026 16:10

@coretl coretl left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've put #1446 up which fixes this in a more general way, please could you take a look at that? If it covers your use case I'd suggest we close this one in favour of it.

Question for reviewers. In the case where we pass just a type[DeviceMock], we can use what it's generic on to figure out whether a device can or cannot be mocked with it. In the event that there is a device that doesn't support it, should we fall back to a default mock, or connect for real?

I suggest we fall back to the default mock. In #1446 mock can either be a bool (like at the moment) or mapping of type[Device] to type[DeviceMock], so your example becomes:

with init_devices(mock={Motor: VeloAndAcclRespectingMotorMock}):
    ...

For each device, the first key where isinstance(device, key) gives the DeviceMock to instantiate and pass to connect(mock=. Any device with no match gets its default mock and the same mapping/bool is also propogated to child connects so you can reach children of the top level devices too.

You can't mix mock and real connects though in init_devices, is that a requirement?

@jwlodek

jwlodek commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

You can't mix mock and real connects though in init_devices, is that a requirement?

It would be nice if we could. I'd basically like to have a way to quickly decide on a subset of devices to mock when working at a beamline (for example, I want to mock all shutters, and all but one motor)

@coretl

coretl commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

It would be nice if we could. I'd basically like to have a way to quickly decide on a subset of devices to mock when working at a beamline (for example, I want to mock all shutters, and all but one motor)

@jwlodek I like the idea of mixing real and mocked Devices in the same mapping, but there's 2 ways we can do it. In both, mapping values are True, False or a DeviceMock subclass, and each Device uses the entry for the nearest class in its MRO.

A: any mix. init_devices(mock=mapping) and Device.connect(mock=mapping) both mean "look yourself up, connect real or mocked accordingly, and pass the mapping on to your children". Devices with no entry connect for real.

with init_devices(mock={Motor: True}):
    thing = ThingWithMotors("BL:THING:")  # thing real, its Motors mocked
with init_devices(mock={ThingWithMotors: True, Motor: False}):
    thing = ThingWithMotors("BL:THING:")  # thing mocked, its Motors real
await thing.connect(mock={Motor: True})   # also works on one Device

B: when you reach a real connect, everything under is real. Only init_devices takes the mapping, and Device.connect(mock=) stays bool | DeviceMock. Everything is mocked down the tree until a Device mapped to False, which connects for real along with everything below it. Devices with no entry are mocked, as now.

with init_devices(mock={Motor: False}):
    thing = ThingWithMotors("BL:THING:")  # thing mocked, its Motors real

So A can also mock inside a real tree, at the cost of a third type for Device.connect(mock=), and B can't. One trap with A: a catch-all Device key also matches Signals, so {Device: True, X: False} gives a real X whose Signals are mocked.

Which do you need for your use case?

@jwlodek

jwlodek commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

I think we'd prefer A - I want to be able to make child motors use Mock but not other children for example. Would it be possible to have this behavior, but specifically exclude signals? I.e. mock device with direct child signals makes the signals mocked? I don't really see a case where I'd want a real device and mocked signals, or a mocked device with real signals.

@coretl

coretl commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator

Ok, I think A with excluded Signals is sensible. I propose:

  • init_devices(mock: bool | Mapping[type[Device], bool | type[DeviceMock]] = False)
  • Device.connect(mock: bool | DeviceMock | Mapping[type[Device], bool | type[DeviceMock]] = False)
  • Each Device looks itself up by the nearest class in its MRO. With no entry it connects for real, so {Device: True} is how you say "mock by default".
  • Signal and Command types (and their subclasses) can't be keys; passing one raises TypeError. They always follow their parent Device.
  • When a Device connects its children, it passes the mapping on with Device set to its own decision. So a child with no entry of its own follows its parent, and "mock this device" means the whole subtree.

So:

# real, except motors (and everything under them) are mocked
with init_devices(mock={Motor: True}):
    ...
# mocked, except motors, which are real all the way down, signals included
with init_devices(mock={Device: True, Motor: False}):
    ...
# real, except motors, which use a specific mock
with init_devices(mock={Motor: VelocityRespectingMotorMock}):
    ...
# the whole detector mocked, plugins included
with init_devices(mock={AreaDetector: True}):
    ...

So to answer your earlier question, Device: False is the default. To override it you place Device: True into the dict.

Does that work for you?

@jwlodek

jwlodek commented Oct 13, 2026

Copy link
Copy Markdown
Member Author

Yes, the above would be great.

@coretl

coretl commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator

Ok, I'll make these changes in #1446

@coretl coretl closed this Nov 10, 2026
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.

2 participants