Skip to content

Feat model validation#14

Merged
Poofjunior merged 15 commits into
mainfrom
feat-model-validation
Jun 10, 2026
Merged

Feat model validation#14
Poofjunior merged 15 commits into
mainfrom
feat-model-validation

Conversation

@Poofjunior

@Poofjunior Poofjunior commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Basic validation for a device spec entry.
Let's merge: #10 first.

@Poofjunior Poofjunior requested a review from patricklatimer June 9, 2026 22:50
Comment thread .github/workflows/test_lint_type.yml Outdated
jobs:
# Job 1: Linters and Type Checkers
tests-lint-and-type-check:
runs-on: self-hosted-good

@patricklatimer patricklatimer Jun 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is 100% cruft from a very old template starting point. If you have recs on an updated version, I'll gladly take it!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 test

Makefile:

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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I would totally agree except these aren't input arguments. ;) They're entirely constructed internally.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Oh whoops didn't read closely, sorry!

from typing import Optional, Any, Self
import importlib

class DeviceSpec(BaseModel):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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"

https://docs.pytest.org/en/7.1.x/how-to/doctest.html

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment thread src/device_spinner/models.py Outdated
@@ -0,0 +1,71 @@
from multiprocessing import Value

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pretty sure this import is a tab-complete-gone-wrong situation

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

oof; my editor taking my tab too far.

# TODO: Validate signature.


class DeviceTrees(RootModel[dict[str, DeviceSpec]]):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is cool, I haven't used this before!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

@Poofjunior Poofjunior merged commit 9d13a39 into main Jun 10, 2026
@Poofjunior Poofjunior deleted the feat-model-validation branch June 10, 2026 18:24
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