Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1934 +/- ##
==========================================
+ Coverage 99.05% 99.08% +0.02%
==========================================
Files 314 314
Lines 11779 11802 +23
==========================================
+ Hits 11668 11694 +26
+ Misses 111 108 -3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
There isn't really anything wrong with the implementation but couldn't the same thing be done with a local function? def load_data(file: str = DEFAULT) -> MyModel:
return load_json_file_to_class(MyModel, file)
bc.plans.my_plan(det1, load_data())
bc.plans.my_plan(det1, load_data("path/to/data.json"))As models are going to be serialized back to JSON and then reconstructed and validated on the server, it might be easier to read straight to JSON and pass it directly. At the moment the data is going def load_data(file: str = DEFAULT):
with open(file) as src:
return json.load(src)I might have missed something though, is there a use case within plans where this would offer something more? |
Yes, I could do this def p60_load_sequence(file: str) -> P60Sequence:
return load_json_file_to_class(P60Sequence, file)
...
def i09_load_sequence(file: str) -> I09Sequence:
return load_json_file_to_class(I09Sequence, file)
...
def i09_1_load_sequence(file: str) -> I091Sequence:
return load_json_file_to_class(I091Sequence, file)
...
def b07_load_sequence(file: str) -> B07BSequence:
return load_json_file_to_class(B07BSequence, file)
...
def b07_1_load_sequence(file: str) -> B07CSequence:
return load_json_file_to_class(B07CSequence, file)for each specific beamline. However, I wanted a standard way to do this via configuration rather defining a new function in the environment every time. This also means it is fully tested, has documentation, and can also reused with existing tests within dodal and plan repos rather than redefining a new loader function for each test.
So I know the type checking isn't added yet, but when it is added surely this will provide the type checking for the model? E.g the model on client side and server side have to match? def my_plan(model: MyModel1, ...):
...
load_model1 = JsonModelLoader[MyModel1](MyModel1)
load_model2 = JsonModelLoader[MyModel2](MyModel2)
...
bc.plans.my_plan(load_model1("/my/file")) # Okay
bc.plans.my_plan(load_model2("/my/file")) # Static type checker should complain as it isn't using the correct model? |
Unfortunately not. The client is working entirely from the API schema provided by the server. For a plan that accepts You can see the spec the client has available by running |
It's not really any different but if you want to avoid writing a function you could do def json_model_loader(model: type[TBaseModel], default_file: str|None=None):
def load_json(file: str| None = default_file) -> TBaseModel:
if file is None:
raise ValueError("No filename given")
return load_json_file_to_class(bm, file)
return load_jsonand then your example becomes p60_load_sequence = json_model_loader(P60Sequence, file)
i09_load_sequence = json_model_loader(I09Sequence, file) which isn't really that different to (and if anything is clearer than) p60_load_sequence = JsonModelLoader[P60Sequence](P60Sequence, "my/default/file.json")
i09_load_sequence = JsonModelLoader[I09Sequence](I09Sequence, "my/default/file.json") |
With a small tweak, I do like how simple your function is def json_model_loader(model: type[TBaseModel], default_file: str | None = None):
def load_json(file: str | None = None) -> TBaseModel:
if file is None and default_file is not None:
return load_json_file_to_class(model, default_file)
elif file is not None:
return load_json_file_to_class(model, file)
raise RuntimeError(
f"{model.__name__} loader has no default file configured and no file was"
"provided when trying to load in a model. "
)
return load_jsonHowever, I am not sure it is correct approach because it is quite limiting. For example, I cannot view or update the default file at a later date, I have to rerun the factory function again and reassign to the variable for my function which I'm not sure I like |
|
When would you need to check or modify the default value? Isn't the point that you can override it if you want a different value? Also, I posted my example function without checking it properly, I think if you use def json_model_loader(model: type[TBaseModel], default_file: str|None=None):
def load_json(file: str| None = default_file) -> TBaseModel:
if file is None:
raise ValueError("No filename given")
return load_json_file_to_class(bm, file)
return load_jsonyou get the default file behaviour you're after. |
I was thinking of just trying to make it flexible enough. However, if we need it we can cross that bridge when we get there. I have update it to use your implementation so ready for review |
|
I think I stand by my original comment
I'm happy to let others chime in but until there's a clear need for updating defaults, this seems like a lot of complexity to avoid writing a one line function. |
|
So I have added more optional configuration. You can optionally provide configuration providing a default file and default path, or provide just a default file and construct the path from the file without overloading the method with too many combinations but using a helper dataclass instead. The use case is for electron analysers, they ususally do the following command The default path is based on the comissioning directoy, so if this ever changes we now can update it dynamically. So now in BlueapiClient, we can do this instead plans.analyserscan(detector, load_data(), <scan_args>) # Load default
plans.analyserscan(detector, load_data("new_file"), <scan_args>) # Load using relative path
plans.analyserscan(detector, load_data("/path/to/new_file"), <scan_args>) # Load using abs pathSo I am trying to replicating this behaviour while giving flexibility so others may not need any default path or default file. |
Villtord
left a comment
There was a problem hiding this comment.
I agree that it does look quite complex but also it makes util more flexible and adds good tests for functions, so would approve it.
Create a
json_model_loaderfactory function. Wrapsload_json_file_to_classbut allows for use of configuring defaults. Will be used with plans and BlueapiClient.Instructions to reviewer on how to test:
Checks for reviewer
dodal connect ${BEAMLINE}