Skip to content

Conversation

@NoxHarmonium
Copy link

@NoxHarmonium NoxHarmonium commented May 14, 2025

As per #1232 we have noticed that a lot of Ophyd objects such as Signal flag a lot of issues with type checkers when you use them with Bluesky plans, but they work fine at runtime.

This is due to a mismatch between the Ophyd types and the Bluesky protocols (https://github.com/bluesky/bluesky/blob/5f5d2c1db8ee5c52fbc9a5f9e4fb3513b2d8625e/src/bluesky/protocols.py).

This is my attempt at updating the types in Ophyd to keep the type checker happy. Sadly in a lot of the cases the fix was to make the types looser, rather than stricter. This is because we can't bring the Bluesky/event-model types into Ophyd (at least as far as I know, maybe we can?) so we would have to copy/paste heaps of types around and they might get out of sync. Instead I just made a lot of things Any so the type checker can infer/ignore things.

There were tests that were designed to make sure that the Ophyd objects remained compatible with the Bluesky protocols, but somewhere along the line they stopped working so I've given them an uplift and added pyright as a typechecker option since it is pretty popular now being part of VSCode via pylance.

There was one protocol (Subscribable) that I couldn't get Ophyd to conform too and that I'll need some extra guidance on (see the PR comments)

I have tested it with one of our beamline libraries, and it reduced the number of type errors we were flagging from 133 -> 0 🎉 . However, I am still a bit worried about changing the public API of base Ophyd objects like Device and Signal as people may be overriding them in different ways. I'd love your opinion on that.

I'll try and annotate the PR to help with the review.

Let me know what you think!

Before:
Before Screenshot
After:
After Screenshot

@@ -1,5 +1,3 @@
# type: ignore
Copy link
Author

Choose a reason for hiding this comment

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

I'm not sure why this type ignore statement was here, as well as the one at the top of device.py.
It was preventing mypy from traversing the submodules properly so I removed it. It didn't seem to break anything in the build, or when I imported it in to our beamline library so maybe its OK?

Copy link

Choose a reason for hiding this comment

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

Removal of this wouldn't break anything other than type checking, which CI here doesn't do.

The question is whether it breaks type checking anywhere downstream using these methods... which I don't think it would since the methods aren't type hinted 🤔

DEFAULT_CONNECTION_TIMEOUT = object()


class OrderedDictType(Dict[A, B]):
Copy link
Author

Choose a reason for hiding this comment

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

I think this was here for compatibility with Python < 3.8
IIRC, it was the source of most of the type issues.
Since 3.8 is the minimum target now, I think we can just replace it with TypedDict

Copy link

Choose a reason for hiding this comment

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

I think we can just replace it with dict by itself? We don't need the OrderedDict since insertion order would work fine.

from typing import List, Literal, Tuple, TypedDict


class Hints(TypedDict, total=False):
Copy link
Author

Choose a reason for hiding this comment

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

This was the one type that I brought over from Bluesky because it was kind of small and I couldn't find a way around it. Typing the return type of hints() as Any or dict didn't seem to work.

Copy link

Choose a reason for hiding this comment

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

Can we not use bluesky as a dependency and use the protocols like ophyd-async does?

@danielballan thoughts?

@pytest.mark.skip()
def test_subscribable(run):
cmd = imports + "from bluesky.protocols import Subscribable;"
run(cmd + "foo: Subscribable = hw.signal1")
Copy link
Author

Choose a reason for hiding this comment

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

signal1 doesn't seem to exist. It may have been removed at some point (or maybe it never existed) but the type checker wasn't picking it up because --disallow-any-unimported wasn't on

run(cmd + "foo: Flyable = sim.TrivialFlyer()")


def test_hinted():
Copy link
Author

Choose a reason for hiding this comment

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

Replaced with test_hashints because Hinted doesn't exist anymore



def test_subscribable():
# TODO: Ophyd signature is incompatible with bluesky protocol
Copy link
Author

Choose a reason for hiding this comment

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

I have a feeling that Ophyd devices shouldn't conform to the Subscribable protocol as the concept of subscription is different between the two libraries.

The ophyd signature looks like this:
def subscribe(self, callback, event_type=None, run=True) -> int
and the Bluesky protocol looks like this:
def subscribe(self, function: Callback[T]) -> None

They seem to work differently in that Ophyd returns a cid when you subscribe which you can use as a token to unsubscribe. Bluesky doesn't give you a cid and unsubscribing works by passing the callback.

Copy link

Choose a reason for hiding this comment

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

Hmmm I don't know if the cid is useful here?

It's not like it would be accessible from the private _temp_callback_ids.

I'm not really sure what we need event_type for here either:

https://github.com/bluesky/bluesky/blob/5f5d2c1db8ee5c52fbc9a5f9e4fb3513b2d8625e/src/bluesky/suspenders.py#L67-L68

@tacaswell

Copy link
Author

Choose a reason for hiding this comment

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

Now that I think about it, are the concepts even the same between Bluesky and Ophyd? In Ophyd a subscription is for watching for changes in device attributes, and in Bluesky a subscription is for handling events (e.g. with BestEffortCallback)

FWIW I know in our code base we have instances where we use event_type when subscribing to an Ophyd device to filter what events we want to receive and we store the CID to unsubscribe later.

# Placeholder required for devices to conform to the Bluesky Moveable protocol
# The actual implementation will be provided by sub classes
def set(self, *args, **kwargs) -> Any:
return super().set(*args, **kwargs)
Copy link
Author

Choose a reason for hiding this comment

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

These placeholder set() functions that just forward on to super() don't seem to break any tests so they seem OK but might need some extra scrutiny because they change the public API I suppose

Copy link

Choose a reason for hiding this comment

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

Why is this needed? If set is just calling super's set, and it hasn't refined the return type at all, then what is it for? If we leave this out thensuper()'s set would be used instead anyway.

Copy link
Author

Choose a reason for hiding this comment

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

Good point. It made the type checker happy, but now that I look again, I don't think the base classes even have an implementation of set. Component doesn't even have a base class.

I'll have to have another look at this. Maybe adding set() to BlueskyInterface would make more sense.

Copy link

@evvaaaa evvaaaa left a comment

Choose a reason for hiding this comment

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

I'm fairly sure we have to use Dict[] from typing instead of my suggested dict[] in python 3.8.

Also my suggestions on using the protocols themselves are dependent on if we decide to include bluesky as a dependency.

@@ -1,5 +1,3 @@
# type: ignore
Copy link

Choose a reason for hiding this comment

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

Removal of this wouldn't break anything other than type checking, which CI here doesn't do.

The question is whether it breaks type checking anywhere downstream using these methods... which I don't think it would since the methods aren't type hinted 🤔

DEFAULT_CONNECTION_TIMEOUT = object()


class OrderedDictType(Dict[A, B]):
Copy link

Choose a reason for hiding this comment

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

I think we can just replace it with dict by itself? We don't need the OrderedDict since insertion order would work fine.

pass

def read(self) -> OrderedDictType[str, Dict[str, Any]]:
def read(self) -> OrderedDict[str, Any]:
Copy link

Choose a reason for hiding this comment

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

I think we can use the TypedDict bluesky protocols here and for the rest of the Component methods, like ophyd-async is doing.

Copy link
Author

@NoxHarmonium NoxHarmonium May 22, 2025

Choose a reason for hiding this comment

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

It would be great if we could just make bluesky a dependency like in ophyd-async. Then we could just use all the Bluesky types. I was assuming that we were trying to avoid coupling Ophyd to Bluesky but if not I'll re-do this PR. I suppose it might not be a breaking change as long as the version constraints were not too strict.

Copy link

Choose a reason for hiding this comment

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

We can wait and see. I suppose the difference is that bluesky doesn't require ophyd-async 🤔

return res

def describe_configuration(self) -> OrderedDictType[str, Dict[str, Any]]:
def describe_configuration(self) -> OrderedDict[str, Any]:
Copy link

Choose a reason for hiding this comment

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

dict[str, dict[str, Any]] would be preferred, but we can use the DataKey TypedDict directly.

# Placeholder required for devices to conform to the Bluesky Moveable protocol
# The actual implementation will be provided by sub classes
def set(self, *args, **kwargs) -> Any:
return super().set(*args, **kwargs)
Copy link

Choose a reason for hiding this comment

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

Why is this needed? If set is just calling super's set, and it hasn't refined the return type at all, then what is it for? If we leave this out thensuper()'s set would be used instead anyway.



def test_subscribable():
# TODO: Ophyd signature is incompatible with bluesky protocol
Copy link

Choose a reason for hiding this comment

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

Hmmm I don't know if the cid is useful here?

It's not like it would be accessible from the private _temp_callback_ids.

I'm not really sure what we need event_type for here either:

https://github.com/bluesky/bluesky/blob/5f5d2c1db8ee5c52fbc9a5f9e4fb3513b2d8625e/src/bluesky/suspenders.py#L67-L68

@tacaswell

run(cmd + "foo: Subscribable = hw.flyer1")


if __name__ == "__main__":
Copy link

Choose a reason for hiding this comment

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

I think we can remove this if __name__ == "__main__" section.

Comment on lines +131 to +134
# TODO: Ophyd signature is incompatible with bluesky protocol
# (extra parameters, returns int instead of None, different parameter names)
# Pyright is stricter and picks this up. Disabled for now
@pytest.mark.skip()
Copy link

Choose a reason for hiding this comment

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

Suggested change
# TODO: Ophyd signature is incompatible with bluesky protocol
# (extra parameters, returns int instead of None, different parameter names)
# Pyright is stricter and picks this up. Disabled for now
@pytest.mark.skip()
# TODO: ...
@pytest.mark.skip(reason="Ophyd signature is incompatible with bluesky protocol")

from typing import List, Literal, Tuple, TypedDict


class Hints(TypedDict, total=False):
Copy link

Choose a reason for hiding this comment

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

Can we not use bluesky as a dependency and use the protocols like ophyd-async does?

@danielballan thoughts?

@NoxHarmonium
Copy link
Author

NoxHarmonium commented May 22, 2025

I'll wait to hear the verdict on whether we should have bluesky as a runtime dependency (like ophyd-async) before I make any other changes.

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