Skip to content

Make loading settings robust to subscripted generics.#269

Open
julianstirling wants to merge 10 commits intomainfrom
generic-type-settings
Open

Make loading settings robust to subscripted generics.#269
julianstirling wants to merge 10 commits intomainfrom
generic-type-settings

Conversation

@julianstirling
Copy link
Contributor

@julianstirling julianstirling commented Feb 26, 2026

PropertyInfo.model_to_value used isinstance with value_type as the second argument, which doesn't work for subscripted generics. This is the bug that yanked v0.0.15. Subscripted generics may sound esoteric, but tuple[int, int] is a subscripted generic, so it's pretty standard.

I've refactored, so we no longer need model_to_value. Instead, PropertyInfo exposes a validate() method, which provides the functionality of model_validate using the property's model. This is easier to type, much more robust, and (I think) rather clearer. As a nice bonus, it accepts models as input, so it replaces model_to_value.

There are only two cases to consider:

  1. The property's type is a BaseModel subclass (including subscripted generics). In this case, we can simply return value_type.model_validate() and know it's correctly typed.
  2. The property's type is not a BaseModel, so we wrap it in a RootModel and use that wrapper (model) to validate. We then need to return its root field, not the whole model instance.

I've added test cases for a bunch of different types, but I think the new logic is solid. For both cases, we use exactly one call to model_validate so we're always sure we have the right type at the end.

@rwb27
Copy link
Collaborator

rwb27 commented Feb 26, 2026

Thanks for this. I'd pretty well implemented the tuple setting bit of this, but wasn't checking the logs in the test so missed the error. For testing it would be really good to raise these errors rather than just log them - I might add in a switch somewhere to turn logging errors into hard errors, I think that would be very helpful particularly in testing.

rwb27
rwb27 previously approved these changes Feb 26, 2026
Copy link
Collaborator

@rwb27 rwb27 left a comment

Choose a reason for hiding this comment

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

This adds a very useful test case. There may well be other places where it would be useful to add generic types.

@rwb27 rwb27 dismissed their stale review February 26, 2026 13:27

I'm happy with the test, but Julian is working on a fix so I'm going to leave this for now and re-approve once that's pushed.

julianstirling and others added 2 commits February 26, 2026 13:35
Rather than separately load a model, then set the property from a model (which requires some checks that are a bit fragile), I've refactored to add a `validate` method to `PropertyInfo`.

This creates and then (if needed) unpacks a model in one function, removing the need to check the model is the correct model.

I've added a few extra tests to make sure `validate` works for the cases I missed when releasing v0.0.15.

I think the logic in `validate` is much simpler and more robust than the isinstance checks that caused the problem.
@barecheck
Copy link

barecheck bot commented Feb 27, 2026

Barecheck - Code coverage report

Total: 96.52%

Your code coverage diff: 0.04% ▴

Uncovered files and lines
FileLines
src/labthings_fastapi/properties.py699, 703, 726-729, 801, 820, 1047
src/labthings_fastapi/thing.py262, 282, 334-336, 368, 456
src/labthings_fastapi/utilities/__init__.py138, 158

Just to make sure, I'm checking a generic model and a BaseModel too.
@rwb27 rwb27 changed the title Failing test for generics in setting types when loading Make loading settings robust to subscripted generics. Feb 27, 2026
Copy link
Collaborator

@rwb27 rwb27 left a comment

Choose a reason for hiding this comment

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

I was about to approve this and ask @julianstirling for his blessing but I'm now not so sure. I think we are swallowing too many errors in load_settings. I can live with consigning ValidationError and KeyError to the log, but TypeError indicates something is broken, and should not be swallowed. That requires doing something different if we load an non-dictionary object from JSON.

@rwb27
Copy link
Collaborator

rwb27 commented Feb 27, 2026

I'm realising that adding LabThingsRootModelWrapper could simplify code in quite a few places. That will be a future PR but I think it will be nice.

rwb27 and others added 3 commits February 27, 2026 12:49
Swap the order of the two cases for clarity.

Co-authored-by: Julian Stirling <julian@julianstirling.co.uk>
As discussed in the PR thread, this now hard errors if the settings file is not a valid dictionary, or if TypeErrors occur when loading settings. The latter probably indicates LabThings or Thing code is broken, it should not result from an incorrect settings file.

I also fixed a line that was too long.
I've added an extra try: block enclosing just the part that loads the JSON file. I think this neatly separates errors loading the file from errors applying the settings it contains.

I have reverted to the old behaviour, of warning for corrupt files and overwriting them. We can add options to make these hard errors in the future.
@rwb27
Copy link
Collaborator

rwb27 commented Feb 27, 2026

I added a separate try: block around the part that loads the JSON file. That neatly separates errors loading the file from errors applying its contents. I have, for now, left the current behaviour (a warning in the log if the file is broken, or if any keys can't be applied), but I think the way it is now makes it easier to add hard errors in the future.

It's also much less likely to swallow errors we weren't intending to suppress, which should reduce the likelihood of summarily overwriting the settings file.

@rwb27
Copy link
Collaborator

rwb27 commented Feb 27, 2026

@julianstirling I think this is ready now and I'll approve it. I will leave it for you to merge, as I feel you probably need to approve it as well.

Copy link
Collaborator

@rwb27 rwb27 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 happy with this, bar a docstring that I've just pushed an update to.

I'm approving it but will let @julianstirling merge so someone else has seen my changes.

The nested exception logic was getting confusing I worried the
cognative complexity and the length of the function was too high.
There is no reason to be in disable_saving_settings mode when
loading the file. As such all of the file loading can be
done before this starts.

Moving it to it's own function provides simplifies it significantly.
@julianstirling
Copy link
Contributor Author

Right this MR has got pretty bike sheddy. Sorry about that, also ambiguous whose branch it is.

I was worried that the second try block nested in the first made the function excessively complicated and only sat within that overall try block because we needed to do the finally to re-enable saving settings.

If loading the data happens is fully handled separately from setting the settings. Then there is no reason for it to happen within this block. Moving it to its own private method cleanly separates the file handling logic from the validation and setting* logic.

*Setting the verb not setting the noun. This is a confusing sentence to write/read.

@julianstirling
Copy link
Contributor Author

@rwb27 I added an extra test. Now this passes. I'm happy, but I'll ping pong it back to you for merge as I committed.

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