diff --git a/.github/workflows/ci-pydantic-latest.yaml b/.github/workflows/ci-pydantic-latest.yaml new file mode 100644 index 000000000..a26a40bbb --- /dev/null +++ b/.github/workflows/ci-pydantic-latest.yaml @@ -0,0 +1,44 @@ +name: ci-pydantic-latest + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +defaults: + run: + shell: pixi run -e py312amber bash -e {0} + +jobs: + test: + name: Test with latest pydantic on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + + steps: + - uses: actions/checkout@v6 + + - name: Set up virtual environment + uses: prefix-dev/setup-pixi@v0.9.5 + with: + pixi-version: v0.41.4 + environments: py312amber + frozen: true + + - name: Upgrade pydantic to latest + run: pip install "pydantic>=2.12" + + - name: Run mypy + run: pixi run -e py312amber run_mypy + + - name: Run tests + run: pixi run -e py312amber run_tests diff --git a/devtools/conda-envs/docs_env.yaml b/devtools/conda-envs/docs_env.yaml index 14304d999..5515e3f61 100644 --- a/devtools/conda-envs/docs_env.yaml +++ b/devtools/conda-envs/docs_env.yaml @@ -8,7 +8,7 @@ dependencies: - setuptools !=76.0.0 - pip - numpy =2 - - pydantic >=2,<2.12 + - pydantic >=2 - openff-toolkit-base =0.17 - openmm - mbuild-base =1 diff --git a/openff/interchange/components/potentials.py b/openff/interchange/components/potentials.py index ce173b0b5..8d117909e 100644 --- a/openff/interchange/components/potentials.py +++ b/openff/interchange/components/potentials.py @@ -439,7 +439,20 @@ def validate_collections( raise ValueError(f"Validation mode {info.mode} not implemented.") +def serialize_collections(v: Any, handler: Any, info: Any) -> dict: + """Serialize collections using each collection's actual type schema. + + Without this, pydantic uses the declared Collection base class schema and + drops subclass-specific fields (e.g. scale_14, cutoff, periodic_potential). + """ + if info.mode == "json": + return {name: json.loads(collection.model_dump_json()) for name, collection in v.items()} + else: + raise NotImplementedError(f"Serialization mode {info.mode} not implemented.") + + _AnnotatedCollections = Annotated[ dict[str, Collection], WrapValidator(validate_collections), + WrapSerializer(serialize_collections), ] diff --git a/openff/interchange/pydantic.py b/openff/interchange/pydantic.py index d81ebf7bd..8b1a0112e 100644 --- a/openff/interchange/pydantic.py +++ b/openff/interchange/pydantic.py @@ -17,4 +17,7 @@ def model_dump(self, **kwargs) -> dict[str, Any]: return super().model_dump(serialize_as_any=True, **kwargs) def model_dump_json(self, **kwargs) -> str: - return super().model_dump_json(serialize_as_any=True, **kwargs) + # serialize_as_any=True breaks pint.Quantity serialization in pydantic >=2.12 + # (pydantic/pydantic#12348); the Annotated WrapSerializer on _Quantity handles + # JSON serialization correctly without it + return super().model_dump_json(**kwargs)