-
Notifications
You must be signed in to change notification settings - Fork 6
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
Handle parameterised generics #598
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #598 +/- ##
==========================================
+ Coverage 92.97% 92.99% +0.01%
==========================================
Files 40 40
Lines 1823 1826 +3
==========================================
+ Hits 1695 1698 +3
Misses 128 128 ☔ View full report in Codecov by Sentry. |
c01b2e0
to
7a78f59
Compare
Nice @DiamondJoseph , I wanted to know if we can test the BlueAPI endpoints as part of this PR or should they be separate...It is so that we can have more confidence in our code that it is working |
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.
no idea what is going on
src/blueapi/service/runner.py
Outdated
def is_valid_return(value: Any, expected_type: type[T]) -> bool: | ||
if expected_type is Any: | ||
return True |
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's not documented. when would we need this?
why do we use this 'rpc'?
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 have 2 processes in blueapi: 1 exposes the REST API, and the other holds the RunEngine. The _rpc
method in this file passes requests from the first to the second. As part of the validation and type hinting, the expected return type of the function is passed, just in case this has changed in the subprocess holding the RunEngine.
If the function that is to be run in the subprocess has a return annotation of Any
, then whatever the value that is returned by running the function in the subprocess is, it is valid. If the return annotation is None
, then the returned value is only valid if it is None.
The linked issue was exposed when returning a subscripted list, e.g. list[DeviceModel]
, as isinstance(foo, list[DeviceModel])
throws an exception. So this new code is a kind of isinstance
that recursively finds the origin (list
in the example) and args (DeviceModel
in the example) and checks that the returned value is valid for both.
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.
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 just have no subprocesses?
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 PR makes #504 easier by making the subprocess work more like a separate RPC service...
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.
504 isn't difficult in the first place imo, and this feels like reinventing the wheel
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.
You may be right, but this is the smallest amount of work required to unblock people here and now
The endpoints themselves have tests (
When refactoring in this area it took me a very long time to fix up the tests as they were all mocking various things across many levels of indirection including private components. The tests are now closer to actual unit tests. What blueapi needs now is probably some form of system tests to confirm end to end functionality and results. |
@DiamondJoseph Have written and pushed some test cases, all but one pass |
src/blueapi/service/runner.py
Outdated
def is_valid_return(value: Any, expected_type: type[T]) -> bool: | ||
if expected_type is Any: | ||
return True | ||
if expected_type is type(None): | ||
return value is None | ||
origin = get_origin(expected_type) | ||
if origin is None or origin is Union: # works for Python >= 3.10 | ||
return isinstance(value, expected_type) | ||
args = get_args(expected_type) | ||
if issubclass(origin, Mapping): | ||
return isinstance(value, Mapping) and all( | ||
is_valid_return(k, args[0]) and is_valid_return(v, args[1]) | ||
for k, v in value.items() | ||
) | ||
if origin is tuple: # handle ellipses specially | ||
assert isinstance(value, tuple) | ||
if args[1] is Ellipsis: | ||
args = (args[0],) * len(value) | ||
else: | ||
return all( | ||
is_valid_return(inner, arg) | ||
for inner, arg in zip(value, args, strict=True) | ||
) | ||
if issubclass(origin, Collection): # list, set, etc. | ||
return isinstance(value, Collection) and all( | ||
is_valid_return(inner, args[0]) for inner in value | ||
) | ||
raise TypeError(f"Unknown origin for generic type {expected_type}") |
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.
Could pydantic.TypeAdapter
do all of this?
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.
If it can, I can't test until #449
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.
@callumforrester with Pydantic.TypeAdapter it's still got an issue with the same single case. My handling is equally as good ;)
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.
Shall we go with TypeAdapter
and not support that case then? I don't see a use case for it currently.
d969200
to
9ff47c8
Compare
7f1445e
to
e1024e9
Compare
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.
LGTM!
src/blueapi/service/runner.py
Outdated
f"{function_name} returned value of type {type(value)}" | ||
+ f" which is incompatible with expected {expected_type}" | ||
) | ||
return TypeAdapter(expected_type).validate_python(value) |
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.
Nit: Can this go in an else
clause?
I think pydantic/pydantic#6870 is the cause of our failing test case. In the debug console: from typing import Generic, TypeVar
from pydantic import BaseModel, TypeAdapter
T = TypeVar("T")
class GenericModel(BaseModel, Generic[T]):
t: T
g = GenericModel[int](t=1)
h = GenericModel(t=2)
ta = TypeAdapter(GenericModel)
ta2 = TypeAdapter(GenericModel[int])
ta.validate_python(g)
ta.validate_python(h)
ta2.validate_python(g)
# fails
ta2.validate_python(h) As the return annotation of the function is ~GenericModel[int]. |
Closes #597 --------- Co-authored-by: Callum Forrester <callum.forrester@diamond.ac.uk>
Closes #597