Skip to content

Commit

Permalink
refactor: remove support for pydantic v1 (#275)
Browse files Browse the repository at this point in the history
* refactor: remove support for pydantic v1

* minor updates

* remove line

* remove another dict

* more removals

* remove line
tlambert03 authored Jan 3, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent ae694b6 commit e294e4c
Showing 16 changed files with 85 additions and 311 deletions.
37 changes: 0 additions & 37 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -100,43 +100,6 @@ jobs:
with:
run: pytest tests/test_widget.py

test-pydantic:
name: Pydantic compat
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11"]
pydantic: ["v1", "v2", "both"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install
run: |
python -m pip install -U pip
python -m pip install .[test]
env:
PYDANTIC_SUPPORT: ${{ matrix.pydantic }}

- name: Test pydantic1
if: matrix.pydantic == 'v1' || matrix.pydantic == 'both'
run: |
python -m pip install 'pydantic<2'
pytest --cov --cov-report=xml --cov-append
- name: Test pydantic2
if: matrix.pydantic == 'v2' || matrix.pydantic == 'both'
run: |
python -m pip install 'pydantic>=2'
pytest --cov --cov-report=xml --cov-append
- uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}

test-build:
name: Build
runs-on: ubuntu-latest
5 changes: 2 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -10,13 +10,13 @@ repos:
- id: validate-pyproject

- repo: https://github.com/crate-ci/typos
rev: codespell-dict-v0.5.0
rev: v1.29.4
hooks:
- id: typos
args: [--force-exclude] # omit --write-changes

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.4
rev: v0.8.5
hooks:
- id: ruff
args: [--fix, --unsafe-fixes]
@@ -29,7 +29,6 @@ repos:
exclude: ^tests|^docs|_napari_plugin|widgets
additional_dependencies:
- pydantic>=2.10
- pydantic-compat
- xsdata==24.2.1
- Pint
- types-lxml
13 changes: 1 addition & 12 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -28,11 +28,7 @@ classifiers = [
"Programming Language :: Python :: 3.13",
]
dynamic = ["version"]
dependencies = [
"pydantic >=1.10.16, !=2.0, !=2.1, !=2.2, !=2.3",
"pydantic-compat >=0.1.0",
"xsdata >=23.6,<24.4",
]
dependencies = ["pydantic >=2.4", "pydantic_extra_types", "xsdata >=23.6,<24.4"]

[project.urls]
Source = "https://github.com/tlambert03/ome-types"
@@ -188,13 +184,6 @@ module = ['ome_types._autogenerated.ome_2016_06.structured_annotations']
# is incompatible with definition in base class "Sequence"
disable_error_code = "misc"

[[tool.mypy.overrides]]
module = ['ome_types._autogenerated.*']
# FIXME: this is because we use type hints from pydantic2 Field
# (via pydantic_compat ... cause that's what it forwards)
# but we *have* to use pydantic v1 syntax
disable_error_code = "call-arg"

# https://coverage.readthedocs.io/en/6.4/config.html
[tool.coverage.report]
exclude_lines = [
17 changes: 3 additions & 14 deletions src/ome_autogen/main.py
Original file line number Diff line number Diff line change
@@ -10,21 +10,20 @@

from xsdata.codegen.writer import CodeWriter
from xsdata.models import config as cfg
from xsdata.models.config import GeneratorOutput
from xsdata.utils import text

from ome_autogen import _util
from ome_autogen._util import camel_to_snake
from ome_autogen.generator import OmeGenerator
from ome_autogen.overrides import MIXINS
from ome_autogen.transformer import OMETransformer
from xsdata_pydantic_basemodel.config import GeneratorOutput

# these are normally "reserved" names that we want to allow as field names
ALLOW_RESERVED_NAMES = {"type", "Type", "Union"}
# format key used to register our custom OmeGenerator
OME_FORMAT = "OME"

PYDANTIC_SUPPORT = os.getenv("PYDANTIC_SUPPORT", "both")
RUFF_LINE_LENGTH = 88
RUFF_TARGET_VERSION = "py38"
OUTPUT_PACKAGE = "ome_types._autogenerated.ome_2016_06"
@@ -79,8 +78,6 @@ def get_config(
structure_style=cfg.StructureStyle.CLUSTERS,
docstring_style=cfg.DocstringStyle.NUMPY,
compound_fields=cfg.CompoundFields(enabled=compound_fields),
# whether to create models that work for both pydantic 1 and 2
pydantic_support=PYDANTIC_SUPPORT, # type: ignore
),
# Add our mixins
extensions=cfg.GeneratorExtensions(mixins),
@@ -142,8 +139,7 @@ def _fix_formatting(package_dir: str, ruff_ignore: list[str] = RUFF_IGNORE) -> N
def _check_mypy(package_dir: str) -> None:
_print_gray("Running mypy ...")

# FIXME: the call-overload disable is due to Field() in pydantic/pydantic-compat.
mypy = ["mypy", package_dir, "--strict", "--disable-error-code", "call-overload"]
mypy = ["mypy", package_dir, "--strict"]
try:
subprocess.check_output(mypy, stderr=subprocess.STDOUT) # noqa S
except subprocess.CalledProcessError as e: # pragma: no cover
@@ -186,14 +182,7 @@ def _build_typed_dicts(package_dir: str) -> None:
def foo(**kwargs: Unpack[ome.ImageDict]) -> None:
...
"""
# sourcery skip: assign-if-exp, reintroduce-else
try:
from pydantic._internal._repr import display_as_type
except ImportError:
# don't try to do this on pydantic1
return
if PYDANTIC_SUPPORT == "v1":
return
from pydantic._internal._repr import display_as_type

from ome_types import model
from ome_types._mixins._base_type import OMEType
4 changes: 2 additions & 2 deletions src/ome_autogen/overrides.py
Original file line number Diff line number Diff line change
@@ -124,8 +124,8 @@ class Ovr:
"typing": {"ClassVar": [": ClassVar"]},
"ome_types._mixins._util": {"new_uuid": ["default_factory=new_uuid"]},
"datetime": {"datetime": ["datetime"]},
"pydantic": {"validator": ["validator("]},
"pydantic_compat": {
"pydantic": {
"validator": ["validator("],
"model_validator": ["model_validator("],
"field_validator": ["field_validator("],
},
23 changes: 4 additions & 19 deletions src/ome_types/_mixins/_base_type.py
Original file line number Diff line number Diff line change
@@ -3,16 +3,9 @@
from datetime import datetime
from enum import Enum
from textwrap import indent
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Optional,
TypeVar,
cast,
)
from typing import TYPE_CHECKING, Any, ClassVar, Optional, TypeVar, cast

from pydantic_compat import PYDANTIC2, BaseModel, field_validator
from pydantic import BaseModel, field_validator

from ome_types._mixins._ids import validate_id
from ome_types._pydantic_compat import field_type, update_set_fields
@@ -84,10 +77,6 @@ class OMEType(BaseModel):
"coerce_numbers_to_str": True,
}

# allow use with weakref
if not PYDANTIC2:
__slots__: ClassVar[set[str]] = {"__weakref__"} # type: ignore

_vid = field_validator("id", mode="before", check_fields=False)(validate_id)

def __iter__(self) -> Any:
@@ -160,11 +149,7 @@ def __getattr__(self, key: str) -> Any:
stacklevel=2,
)
return getattr(self, new_key)
# pydantic v2+ has __getattr__
if hasattr(BaseModel, "__getattr__"):
return super().__getattr__(key) # type: ignore
else:
return object.__getattribute__(self, key)
return super().__getattr__(key) # type: ignore

def to_xml(self, **kwargs: Any) -> str:
"""Serialize this object to XML.
@@ -195,7 +180,7 @@ def _update_set_fields(self) -> None:
Because pydantic isn't aware of mutations to sequences, it can't tell when
a field has been "set" by mutating a sequence. This method updates the
self.__fields_set__ attribute to reflect that. We assume that if an attribute
`model_fields_set` attribute to reflect that. We assume that if an attribute
is not None, and is not equal to the default value, then it has been set.
"""
update_set_fields(self)
27 changes: 11 additions & 16 deletions src/ome_types/_mixins/_kinded.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import builtins
from typing import Any
from typing import TYPE_CHECKING, Any

from pydantic_compat import BaseModel

try:
from pydantic import model_serializer
except ImportError:
model_serializer = None # type: ignore
from pydantic import BaseModel, model_serializer


class KindMixin(BaseModel):
@@ -20,15 +15,15 @@ def __init__(self, **data: Any) -> None:
data.pop("kind", None)
return super().__init__(**data)

def dict(self, **kwargs: Any) -> dict[str, Any]:
d = super().dict(**kwargs)
d["kind"] = self.__class__.__name__.lower()
return d

if model_serializer is not None:
if not TYPE_CHECKING:

@model_serializer(mode="wrap")
def serialize_root(self, handler, _info) -> builtins.dict: # type: ignore
d = handler(self)
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
d = super().model_dump(**kwargs)
d["kind"] = self.__class__.__name__.lower()
return d

@model_serializer(mode="wrap")
def serialize_root(self, handler, _info) -> builtins.dict: # type: ignore
d = handler(self)
d["kind"] = self.__class__.__name__.lower()
return d
18 changes: 7 additions & 11 deletions src/ome_types/_mixins/_map_mixin.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
from collections.abc import Iterator, MutableMapping
from typing import TYPE_CHECKING, Any, Optional

try:
from pydantic import model_serializer
except ImportError:
model_serializer = None # type: ignore

from pydantic import model_serializer

if TYPE_CHECKING:
from typing import Protocol
@@ -45,11 +41,11 @@ def __setitem__(self: "HasMsProtocol", key: str, value: Optional[str]) -> None:
def _pydict(self: "HasMsProtocol", **kwargs: Any) -> dict[str, str]:
return {m.k: m.value for m in self.ms if m.k is not None}

def dict(self, **kwargs: Any) -> dict[str, Any]:
return self._pydict() # type: ignore

if model_serializer is not None:
if not TYPE_CHECKING:

@model_serializer(mode="wrap")
def serialize_root(self, handler, _info) -> dict: # type: ignore
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
return self._pydict() # type: ignore

@model_serializer(mode="wrap")
def serialize_root(self, handler, _info) -> dict: # type: ignore
return self._pydict() # type: ignore
48 changes: 15 additions & 33 deletions src/ome_types/_pydantic_compat.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from __future__ import annotations

from collections.abc import MutableSequence
from typing import TYPE_CHECKING, Any, cast
from typing import TYPE_CHECKING, Any

import pydantic.version
from pydantic import BaseModel
from pydantic_extra_types.color import Color as Color

if TYPE_CHECKING:
from pydantic.fields import FieldInfo
@@ -14,51 +15,32 @@
)


if pydantic_version >= (2,):
try:
from pydantic_extra_types.color import Color as Color
except ImportError:
from pydantic.color import Color as Color
def field_type(field: FieldInfo) -> Any:
return field.annotation

def field_type(field: FieldInfo) -> Any:
return field.annotation

def field_regex(obj: type[BaseModel], field_name: str) -> str | None:
# typing is incorrect at the moment, but may indicate breakage in pydantic 3
field_info = obj.model_fields[field_name] # type: ignore [index]
meta = field_info.json_schema_extra or {}
# if a "metadata" key exists... use it.
# After pydantic-compat 0.2, this is where it will be.
if "metadata" in meta: # type: ignore
meta = meta["metadata"] # type: ignore
if meta:
return meta.get("pattern") # type: ignore
return None
def field_regex(obj: type[BaseModel], field_name: str) -> str | None:
# typing is incorrect at the moment, but may indicate breakage in pydantic 3
field_info = obj.model_fields[field_name] # type: ignore [index]
meta = field_info.json_schema_extra or {}
if meta:
return meta.get("pattern") # type: ignore
return None # pragma: no cover

kw: dict = {"validated_data": {}} if pydantic_version >= (2, 10) else {}

def get_default(f: FieldInfo) -> Any:
return f.get_default(call_default_factory=True, **kw)
else:
from pydantic.color import Color as Color # type: ignore [no-redef]
kw: dict = {"validated_data": {}} if pydantic_version >= (2, 10) else {}

def field_type(field: Any) -> Any: # type: ignore
return field.type_

def field_regex(obj: type[BaseModel], field_name: str) -> str | None:
field = obj.__fields__[field_name] # type: ignore
return cast(str, field.field_info.regex)

def get_default(f: Any) -> Any: # type: ignore
return f.get_default()
def get_default(f: FieldInfo) -> Any:
return f.get_default(call_default_factory=True, **kw)


def update_set_fields(self: BaseModel) -> None:
"""Update set fields with populated mutable sequences.
Because pydantic isn't aware of mutations to sequences, it can't tell when
a field has been "set" by mutating a sequence. This method updates the
self.__fields_set__ attribute to reflect that. We assume that if an attribute
`model_fields_set` attribute to reflect that. We assume that if an attribute
is not None, and is not equal to the default value, then it has been set.
"""
for field_name, field in self.model_fields.items():
5 changes: 1 addition & 4 deletions src/ome_types/widget.py
Original file line number Diff line number Diff line change
@@ -139,10 +139,7 @@ def update(self, ome: OME | str | None | dict) -> None:
self._current_path = ome
else:
raise TypeError("must be OME object or string")
if hasattr(_ome, "model_dump"):
data = _ome.model_dump(exclude_unset=True)
else:
data = _ome.dict(exclude_unset=True)
data = _ome.model_dump(exclude_unset=True)
self._fill_item(data)

def _fill_item(self, obj: Any, item: QTreeWidgetItem = None) -> None:
56 changes: 22 additions & 34 deletions src/xsdata_pydantic_basemodel/compat.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,28 @@
import dataclasses as dc
from collections.abc import Iterator
from contextlib import suppress
from typing import (
TYPE_CHECKING,
Any,
Callable,
ClassVar,
Generic,
Optional,
TypeVar,
)
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, Optional, TypeVar

try:
from lxml import etree as ET
except ImportError:
import xml.etree.ElementTree as ET # type: ignore

from pydantic import BaseModel, validators
from pydantic_compat import PYDANTIC2, PydanticCompatMixin
from pydantic import BaseModel, Field
from pydantic_core import core_schema as cs
from xsdata.formats.dataclass.compat import Dataclasses, class_types
from xsdata.formats.dataclass.models.elements import XmlType
from xsdata.models.datatype import XmlDate, XmlDateTime, XmlDuration, XmlPeriod, XmlTime

from xsdata_pydantic_basemodel.pydantic_compat import Field, dataclass_fields
from xsdata_pydantic_basemodel.pydantic_compat import dataclass_fields

try:
from lxml import etree as ET
except ImportError:
import xml.etree.ElementTree as ET # type: ignore

T = TypeVar("T", bound=object)

if TYPE_CHECKING:
from pydantic import ConfigDict


class AnyElement(PydanticCompatMixin, BaseModel):
class AnyElement(BaseModel):
"""Generic model to bind xml document data to wildcard fields.
:param qname: The element's qualified name
@@ -45,11 +37,11 @@ class AnyElement(PydanticCompatMixin, BaseModel):
tail: Optional[str] = Field(default=None)
children: list["AnyElement"] = Field(
default_factory=list,
metadata={"type": XmlType.WILDCARD}, # type: ignore [call-arg,call-overload]
json_schema_extra={"type": XmlType.WILDCARD},
)
attributes: dict[str, str] = Field(
default_factory=dict,
metadata={"type": XmlType.ATTRIBUTES}, # type: ignore [call-arg,call-overload]
json_schema_extra={"type": XmlType.ATTRIBUTES},
)

model_config: ClassVar["ConfigDict"] = {"arbitrary_types_allowed": True}
@@ -63,7 +55,7 @@ def to_etree_element(self) -> "ET._Element":
return elem


class DerivedElement(PydanticCompatMixin, BaseModel, Generic[T]):
class DerivedElement(BaseModel, Generic[T]):
"""Generic model wrapper for type substituted elements.
Example: eg. <b xsi:type="a">...</b>
@@ -130,19 +122,15 @@ def validator(value: Any) -> Any:
ET.QName: make_validators(ET.QName, ET.QName),
}

if not PYDANTIC2:
validators._VALIDATORS.extend(list(_validators.items()))
else:
from pydantic import BaseModel
from pydantic_core import core_schema as cs

def _make_get_core_schema(validator: Callable) -> Callable:
def get_core_schema(*args: Any) -> cs.PlainValidatorFunctionSchema:
return cs.general_plain_validator_function(validator)
def _make_get_core_schema(validator: Callable) -> Callable:
def get_core_schema(*args: Any) -> cs.PlainValidatorFunctionSchema:
return cs.with_info_plain_validator_function(validator)

return get_core_schema

return get_core_schema

for type_, val in _validators.items():
get_schema = _make_get_core_schema(val[0])
with suppress(TypeError):
type_.__get_pydantic_core_schema__ = get_schema # type: ignore
for type_, val in _validators.items():
get_schema = _make_get_core_schema(val[0])
with suppress(TypeError):
type_.__get_pydantic_core_schema__ = get_schema # type: ignore
20 changes: 0 additions & 20 deletions src/xsdata_pydantic_basemodel/config.py

This file was deleted.

25 changes: 2 additions & 23 deletions src/xsdata_pydantic_basemodel/generator.py
Original file line number Diff line number Diff line change
@@ -8,8 +8,6 @@
from xsdata.utils import text
from xsdata.utils.collections import unique_sequence

from xsdata_pydantic_basemodel.pydantic_compat import PYDANTIC2

if TYPE_CHECKING:
from xsdata.codegen.models import Attr, Class
from xsdata.models.config import GeneratorConfig, OutputFormat
@@ -28,12 +26,6 @@ def init_filters(cls, config: GeneratorConfig) -> Filters:
class PydanticBaseFilters(Filters):
def __init__(self, config: GeneratorConfig):
super().__init__(config)
self.pydantic_support = getattr(config.output, "pydantic_support", False)
if self.pydantic_support == "both":
self.import_patterns["pydantic"].pop("Field")
self.import_patterns["xsdata_pydantic_basemodel.pydantic_compat"] = {
"Field": {" = Field("}
}

@classmethod
def build_import_patterns(cls) -> dict[str, dict]:
@@ -73,26 +65,13 @@ def move_restrictions_to_pydantic_field(
if "metadata" not in kwargs: # pragma: no cover
return

# The choice to use v1 syntax for cross-compatible mode has to do with
# https://docs.pydantic.dev/usage/schema/#unenforced-field-constraints
# There were more fields in v1 than in v2, so "min_length" is degenerate in v2
# NOTE: ... this might be fixed by using pydantic_compat?
if self.pydantic_support == "v2":
use_v2 = True
elif self.pydantic_support == "auto":
use_v2 = PYDANTIC2
else: # v1 or both
use_v2 = False

restriction_map = V2_RESTRICTION_MAP if use_v2 else V1_RESTRICTION_MAP

metadata: dict = kwargs["metadata"]
getitem = metadata.pop if pop else metadata.get
for from_, to_ in restriction_map.items():
for from_, to_ in V2_RESTRICTION_MAP.items():
if from_ in metadata:
kwargs[to_] = getitem(from_)

if use_v2 and "metadata" in kwargs:
if "metadata" in kwargs:
kwargs["json_schema_extra"] = kwargs.pop("metadata")

# note, this method is the same as the base class implementation before it
42 changes: 14 additions & 28 deletions src/xsdata_pydantic_basemodel/pydantic_compat.py
Original file line number Diff line number Diff line change
@@ -5,41 +5,27 @@
from functools import cache
from typing import TYPE_CHECKING, Any, Callable, TypeVar

from pydantic_compat import PYDANTIC2, Field

__all__ = ["PYDANTIC2", "Field"]
from pydantic_core import PydanticUndefined

if TYPE_CHECKING:
from pydantic import BaseModel
from pydantic.fields import FieldInfo

M = TypeVar("M", bound=BaseModel)
C = TypeVar("C", bound=Callable[..., Any])


if PYDANTIC2:
from pydantic.fields import FieldInfo
from pydantic_core import PydanticUndefined as Undefined

def _get_metadata(pydantic_field: FieldInfo) -> dict[str, Any]:
meta = (
pydantic_field.json_schema_extra
if isinstance(pydantic_field.json_schema_extra, dict)
else {}
)
# if a "metadata" key exists... use it.
# After pydantic-compat 0.2, this is where it will be.
if "metadata" in meta:
meta = meta["metadata"] # type: ignore
return meta

else:
from pydantic.fields import Undefined as Undefined # type: ignore

def _get_metadata(pydantic_field) -> dict: # type: ignore
extra = pydantic_field.field_info.extra
if "json_schema_extra" in extra:
return extra["json_schema_extra"]
return extra.get("metadata", {})
def _get_metadata(pydantic_field: FieldInfo) -> dict[str, Any]:
meta = (
pydantic_field.json_schema_extra
if isinstance(pydantic_field.json_schema_extra, dict)
else {}
)
# if a "metadata" key exists... use it.
# After pydantic-compat 0.2, this is where it will be.
if "metadata" in meta:
meta = meta["metadata"] # type: ignore
return meta


def _get_defaults(pydantic_field: FieldInfo) -> tuple[Any, Any]:
@@ -50,7 +36,7 @@ def _get_defaults(pydantic_field: FieldInfo) -> tuple[Any, Any]:
default_factory = dc.MISSING
default = (
dc.MISSING
if pydantic_field.default in (Undefined, Ellipsis)
if pydantic_field.default in (PydanticUndefined, Ellipsis)
else pydantic_field.default
)
return default_factory, default
4 changes: 1 addition & 3 deletions tests/test_model.py
Original file line number Diff line number Diff line change
@@ -10,7 +10,6 @@

import pytest
from pydantic import ValidationError
from pydantic_compat import PYDANTIC2

from ome_types import from_tiff, from_xml, model, to_xml
from ome_types.model import OME, AnnotationRef, CommentAnnotation, Instrument
@@ -46,7 +45,6 @@ def test_refs() -> None:
assert ome.screens[0].plate_refs[0].ref is ome.plates[0]


@pytest.mark.skipif(not PYDANTIC2, reason="pydantic v1 has poor support for deepcopy")
def test_ref_copy() -> None:
aref = AnnotationRef(id=1)
ome = OME(
@@ -60,7 +58,7 @@ def test_ref_copy() -> None:

ome3 = copy.deepcopy(ome)
assert ome3.instruments[0].annotation_refs[0].ref is not aref.ref
ome4 = OME(**ome.dict())
ome4 = OME(**ome.model_dump())
assert ome4.instruments[0].annotation_refs[0].ref is not aref.ref

del ome, aref
52 changes: 0 additions & 52 deletions tests/test_names.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
from __future__ import annotations

import json
from pathlib import Path
from typing import TYPE_CHECKING, Any

import pytest
from pydantic import BaseModel, version

import ome_types
from ome_types import model

if TYPE_CHECKING:
from collections.abc import Sequence

PYDANTIC2 = version.VERSION.startswith("2")
TESTS = Path(__file__).parent
KNOWN_CHANGES: dict[str, list[tuple[str, str | None]]] = {
"OME.datasets": [
@@ -88,51 +81,6 @@
}


def _assert_names_match(
old: dict[str, Any], new: dict[str, Any], path: Sequence[str] = ()
) -> None:
"""Make sure every key in old is in new, or that it's in KNOWN_CHANGES."""
for old_key, value in old.items():
new_key = old_key
if old_key not in new:
_path = ".".join(path)
if _path in KNOWN_CHANGES:
for from_, new_key in KNOWN_CHANGES[_path]: # type: ignore
if old_key == from_ and (new_key in new or new_key is None):
break
else:
raise AssertionError(
f"Key {old_key!r} not in new model at {_path}: {list(new)}"
)
else:
raise AssertionError(f"{_path!r} not in KNOWN_CHANGES")

if isinstance(value, dict) and new_key in new:
_assert_names_match(value, new[new_key], (*path, old_key))


def _get_fields(cls: type[BaseModel]) -> dict[str, Any]:
from pydantic.typing import display_as_type

fields = {}
for name, field in cls.__fields__.items():
if name.startswith("_"):
continue
if isinstance(field.type_, type) and issubclass(field.type_, BaseModel):
fields[name] = _get_fields(field.type_)
else:
fields[name] = display_as_type(field.outer_type_) # type: ignore
return fields


@pytest.mark.skipif(PYDANTIC2, reason="no need to check pydantic 2")
def test_names() -> None:
with (TESTS / "data" / "old_model.json").open() as f:
old_names = json.load(f)
new_names = _get_fields(ome_types.model.OME)
_assert_names_match(old_names, new_names, ("OME",))


V1_EXPORTS = [
("affine_transform", "AffineTransform"),
("annotation", "Annotation"),

0 comments on commit e294e4c

Please sign in to comment.