-
Notifications
You must be signed in to change notification settings - Fork 87
Correct return types to match current bluesky protocols #1248
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Also add pyright as a type checker to flush out any differences between pyright/mypy
| @@ -1,5 +1,3 @@ | |||
| # type: ignore | |||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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]): |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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(): |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
evvaaaa
left a comment
There was a problem hiding this 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 | |||
There was a problem hiding this comment.
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]): |
There was a problem hiding this comment.
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]: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]: |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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:
| run(cmd + "foo: Subscribable = hw.flyer1") | ||
|
|
||
|
|
||
| if __name__ == "__main__": |
There was a problem hiding this comment.
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.
| # 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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| # 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): |
There was a problem hiding this comment.
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?
|
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. |
As per #1232 we have noticed that a lot of Ophyd objects such as
Signalflag 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
Anyso 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:


After: