Feat model validation#14
Conversation
…revamp Feat pyproject build revamp
| jobs: | ||
| # Job 1: Linters and Type Checkers | ||
| tests-lint-and-type-check: | ||
| runs-on: self-hosted-good |
There was a problem hiding this comment.
I don't think public repos can run on self-hosted runners. Also we don't actually need any features of the self-hosted (like being in the network), so I think it's probably best to use a gh-provided one like windows-latest
Also then you don't need to do the env stuff to point to AIBS_MPE
There was a problem hiding this comment.
This is 100% cruft from a very old template starting point. If you have recs on an updated version, I'll gladly take it!
There was a problem hiding this comment.
I like to keep my actual tool invocations in a makefile, then just call them from the CI like so:
name: Lint and run tests
on:
pull_request:
branches:
- main
jobs:
test-lint-type:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14' ]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install UV - python version set to ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
- name: Lint
run: make lint
- name: Test
run: make testMakefile:
lint:
uvx ruff check
lint-fix:
uvx ruff check --fix
test:
uv run pytest
That way its easy to locally run the exact same commands as the CI, and you can just update the makefile rather than having to hunt in the CI.
| self.devices = {} | ||
| self.instance_names = set() | ||
| self.devices: dict[str, object] = {} | ||
| self.instance_names: set = set() |
There was a problem hiding this comment.
These mutable default arguments are gonna cause some problems:
https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments
https://florimond.dev/en/posts/2018/08/python-mutable-defaults-are-the-source-of-all-evil
Tangentially though, you can provide mutable defaults to pydantic models! https://pydantic.dev/docs/validation/latest/concepts/fields/#mutable-default-values
There was a problem hiding this comment.
I would totally agree except these aren't input arguments. ;) They're entirely constructed internally.
There was a problem hiding this comment.
Oh whoops didn't read closely, sorry!
| from typing import Optional, Any, Self | ||
| import importlib | ||
|
|
||
| class DeviceSpec(BaseModel): |
There was a problem hiding this comment.
Maybe add a docstring about the module_name/class_name/factory options. Also might be a good use case for some doctests!
I turned your unit tests into doctests, maybe it's a bit much to have all the error cases in a docstring, but I think it's definitely handy to have an example or two of how I'm supposed to use it, especially since there's special validation logic.
"""
Examples
--------
>>> data = {'module': 'builtins', 'class': 'dict', 'kwds': {'key0': 'MyVal'}}
>>> DeviceSpec.model_validate(data).model_dump(exclude_none=True)
{'module_name': 'builtins', 'class_name': 'dict', 'kwds': {'key0': 'MyVal'}}
>>> data = {'module': '', 'class': 'dict', 'kwds': {'key0': 'MyVal'}}
>>> DeviceSpec.model_validate(data) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
pydantic_core._pydantic_core.ValidationError: ...
>>> data = {'class': 'dict', 'kwds': {'key0': 'MyVal'}}
>>> DeviceSpec.model_validate(data) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
pydantic_core._pydantic_core.ValidationError: ...
>>> data = {'class': 'builtins.dict', 'factory': 'builtins.dict', 'kwds': {'key0': 'MyVal'}}
>>> DeviceSpec.model_validate(data) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
pydantic_core._pydantic_core.ValidationError: ...
>>> data = {'kwds': {'key0': 'MyVal'}}
>>> DeviceSpec.model_validate(data) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
pydantic_core._pydantic_core.ValidationError: ...
"""
Also, you can configure pytest in the pyproject.toml to run doctests
[tool.pytest.ini_options]
addopts = "--doctest-modules"
There was a problem hiding this comment.
I like it! This looks handy from the point of view of generating docs, but pytest is useful from the point of view of being able to run one function at at time, so I'll probs keep both.
It also looks like doctest autoruns the examples folder, so I cleaned those up and made an omission where needed.
There was a problem hiding this comment.
Yeah, there's definitely a balance to be found with doctests, they're definitely not like, the place to put all your unit tests, but they're very handy to have a few examples and basic usage tests.
| @@ -0,0 +1,71 @@ | |||
| from multiprocessing import Value | |||
There was a problem hiding this comment.
Pretty sure this import is a tab-complete-gone-wrong situation
There was a problem hiding this comment.
oof; my editor taking my tab too far.
| # TODO: Validate signature. | ||
|
|
||
|
|
||
| class DeviceTrees(RootModel[dict[str, DeviceSpec]]): |
There was a problem hiding this comment.
This is cool, I haven't used this before!
There was a problem hiding this comment.
yeah; I thought that was cool too for an anonymous-ish field!
| pass | ||
|
|
||
| def create_devices_from_specs(self, spec_trees: dict): | ||
| def create_devices_from_specs(self, spec_trees: dict, |
There was a problem hiding this comment.
Should this spec_trees be typed as a DeviceTrees? Or if it's easier to keep it a dict, would this function be a good place to run the validation?
DeviceTrees.model_validate(**spec_trees)There was a problem hiding this comment.
This is actually the resulting dict of instances that device-spinner will buid, not the input. But you're right that I should totally let the create_devices_from_specs take a DeviceTrees as input or do a model validate.
Basic validation for a device spec entry.
Let's merge: #10 first.