From 58e1a00615d121cdc8418ffbae5468fb85eaab06 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 3 Jan 2025 09:55:23 -0500 Subject: [PATCH 1/6] refactor: remove support for pydantic v1 --- .github/workflows/test.yml | 37 -------------- pyproject.toml | 13 +---- src/ome_autogen/main.py | 10 +--- src/ome_autogen/overrides.py | 4 +- src/ome_types/_mixins/_base_type.py | 15 +----- src/ome_types/_mixins/_kinded.py | 2 +- src/ome_types/_pydantic_compat.py | 50 +++++++------------ src/xsdata_pydantic_basemodel/compat.py | 46 ++++++++--------- src/xsdata_pydantic_basemodel/config.py | 20 -------- src/xsdata_pydantic_basemodel/generator.py | 25 +--------- .../pydantic_compat.py | 42 ++++++---------- tests/test_model.py | 4 +- 12 files changed, 64 insertions(+), 204 deletions(-) delete mode 100644 src/xsdata_pydantic_basemodel/config.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9b5c4f58..ff8d51a5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 716af931..ccfc37fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [ diff --git a/src/ome_autogen/main.py b/src/ome_autogen/main.py index b57b8735..26a03cab 100644 --- a/src/ome_autogen/main.py +++ b/src/ome_autogen/main.py @@ -10,6 +10,7 @@ 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 @@ -17,14 +18,12 @@ 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 @@ -192,8 +188,6 @@ def foo(**kwargs: Unpack[ome.ImageDict]) -> None: except ImportError: # don't try to do this on pydantic1 return - if PYDANTIC_SUPPORT == "v1": - return from ome_types import model from ome_types._mixins._base_type import OMEType diff --git a/src/ome_autogen/overrides.py b/src/ome_autogen/overrides.py index a178f1f1..ffa44607 100644 --- a/src/ome_autogen/overrides.py +++ b/src/ome_autogen/overrides.py @@ -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("], }, diff --git a/src/ome_types/_mixins/_base_type.py b/src/ome_types/_mixins/_base_type.py index 636f96b3..b12d7e33 100644 --- a/src/ome_types/_mixins/_base_type.py +++ b/src/ome_types/_mixins/_base_type.py @@ -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: diff --git a/src/ome_types/_mixins/_kinded.py b/src/ome_types/_mixins/_kinded.py index 5c281f10..645b836c 100644 --- a/src/ome_types/_mixins/_kinded.py +++ b/src/ome_types/_mixins/_kinded.py @@ -1,7 +1,7 @@ import builtins from typing import Any -from pydantic_compat import BaseModel +from pydantic import BaseModel try: from pydantic import model_serializer diff --git a/src/ome_types/_pydantic_compat.py b/src/ome_types/_pydantic_compat.py index 04b9dda0..e7047338 100644 --- a/src/ome_types/_pydantic_compat.py +++ b/src/ome_types/_pydantic_compat.py @@ -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,43 +15,28 @@ ) -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 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 - 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: diff --git a/src/xsdata_pydantic_basemodel/compat.py b/src/xsdata_pydantic_basemodel/compat.py index c17abc86..30c181e0 100644 --- a/src/xsdata_pydantic_basemodel/compat.py +++ b/src/xsdata_pydantic_basemodel/compat.py @@ -11,18 +11,18 @@ 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) @@ -30,7 +30,7 @@ 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 +45,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 +63,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. ... @@ -130,19 +130,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.general_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 diff --git a/src/xsdata_pydantic_basemodel/config.py b/src/xsdata_pydantic_basemodel/config.py deleted file mode 100644 index 1d940fda..00000000 --- a/src/xsdata_pydantic_basemodel/config.py +++ /dev/null @@ -1,20 +0,0 @@ -from dataclasses import dataclass -from typing import Literal - -from xsdata.models import config - - -@dataclass -class GeneratorOutput(config.GeneratorOutput): - # v1 will only support pydantic<2 - # v2 will only support pydantic>=2 - # auto will only support whatever pydantic is installed at codegen time - # both will support both pydantic versions - pydantic_support: Literal["v1", "v2", "auto", "both"] = "auto" - - def __post_init__(self) -> None: - if self.pydantic_support not in ("v1", "v2", "auto", "both"): - raise ValueError( - "pydantic_support must be one of 'v1', 'v2', 'auto', 'both', not " - f"{self.pydantic_support!r}" - ) diff --git a/src/xsdata_pydantic_basemodel/generator.py b/src/xsdata_pydantic_basemodel/generator.py index 6a327dd5..b73733d9 100644 --- a/src/xsdata_pydantic_basemodel/generator.py +++ b/src/xsdata_pydantic_basemodel/generator.py @@ -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 diff --git a/src/xsdata_pydantic_basemodel/pydantic_compat.py b/src/xsdata_pydantic_basemodel/pydantic_compat.py index ef8c0ee7..c1293a54 100644 --- a/src/xsdata_pydantic_basemodel/pydantic_compat.py +++ b/src/xsdata_pydantic_basemodel/pydantic_compat.py @@ -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 diff --git a/tests/test_model.py b/tests/test_model.py index 59378617..b988accf 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -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 From e6b08bf93f8fab6843d8b404645a9e6adcf72178 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 3 Jan 2025 10:03:41 -0500 Subject: [PATCH 2/6] minor updates --- .pre-commit-config.yaml | 5 ++--- src/xsdata_pydantic_basemodel/compat.py | 12 ++---------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4305abbf..8837766e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/src/xsdata_pydantic_basemodel/compat.py b/src/xsdata_pydantic_basemodel/compat.py index 30c181e0..6856156d 100644 --- a/src/xsdata_pydantic_basemodel/compat.py +++ b/src/xsdata_pydantic_basemodel/compat.py @@ -1,15 +1,7 @@ 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 from pydantic import BaseModel, Field from pydantic_core import core_schema as cs @@ -133,7 +125,7 @@ def validator(value: Any) -> Any: def _make_get_core_schema(validator: Callable) -> Callable: def get_core_schema(*args: Any) -> cs.PlainValidatorFunctionSchema: - return cs.general_plain_validator_function(validator) + return cs.with_info_plain_validator_function(validator) return get_core_schema From d5b514bf3bea47904160fb893decaeddda543216 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 3 Jan 2025 10:12:46 -0500 Subject: [PATCH 3/6] remove line --- src/ome_autogen/main.py | 7 +------ src/ome_types/_pydantic_compat.py | 6 +----- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/ome_autogen/main.py b/src/ome_autogen/main.py index 26a03cab..9a0e0441 100644 --- a/src/ome_autogen/main.py +++ b/src/ome_autogen/main.py @@ -182,12 +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 + from pydantic._internal._repr import display_as_type from ome_types import model from ome_types._mixins._base_type import OMEType diff --git a/src/ome_types/_pydantic_compat.py b/src/ome_types/_pydantic_compat.py index e7047338..6f731df2 100644 --- a/src/ome_types/_pydantic_compat.py +++ b/src/ome_types/_pydantic_compat.py @@ -23,13 +23,9 @@ 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 + return None # pragma: no cover kw: dict = {"validated_data": {}} if pydantic_version >= (2, 10) else {} From a1896fcf338f3e034353f06b392682b94c1c11fc Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 3 Jan 2025 10:15:09 -0500 Subject: [PATCH 4/6] remove another dict --- src/ome_types/_mixins/_kinded.py | 27 +++++++++++---------------- src/ome_types/_mixins/_map_mixin.py | 18 +++++++----------- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/ome_types/_mixins/_kinded.py b/src/ome_types/_mixins/_kinded.py index 645b836c..d1751945 100644 --- a/src/ome_types/_mixins/_kinded.py +++ b/src/ome_types/_mixins/_kinded.py @@ -1,12 +1,7 @@ import builtins -from typing import Any +from typing import TYPE_CHECKING, Any -from pydantic 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 diff --git a/src/ome_types/_mixins/_map_mixin.py b/src/ome_types/_mixins/_map_mixin.py index 6e45ff87..8b94431a 100644 --- a/src/ome_types/_mixins/_map_mixin.py +++ b/src/ome_types/_mixins/_map_mixin.py @@ -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 From 18814454d3145fcf905a16ac0a8160e2282bce7a Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 3 Jan 2025 10:18:45 -0500 Subject: [PATCH 5/6] more removals --- src/ome_types/_mixins/_base_type.py | 2 +- src/ome_types/_pydantic_compat.py | 2 +- src/ome_types/widget.py | 5 +-- tests/test_names.py | 52 ----------------------------- 4 files changed, 3 insertions(+), 58 deletions(-) diff --git a/src/ome_types/_mixins/_base_type.py b/src/ome_types/_mixins/_base_type.py index b12d7e33..3bcbf70f 100644 --- a/src/ome_types/_mixins/_base_type.py +++ b/src/ome_types/_mixins/_base_type.py @@ -184,7 +184,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) diff --git a/src/ome_types/_pydantic_compat.py b/src/ome_types/_pydantic_compat.py index 6f731df2..527aaca0 100644 --- a/src/ome_types/_pydantic_compat.py +++ b/src/ome_types/_pydantic_compat.py @@ -40,7 +40,7 @@ def update_set_fields(self: BaseModel) -> 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. """ for field_name, field in self.model_fields.items(): diff --git a/src/ome_types/widget.py b/src/ome_types/widget.py index 297c4a57..af4d00e6 100644 --- a/src/ome_types/widget.py +++ b/src/ome_types/widget.py @@ -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: diff --git a/tests/test_names.py b/tests/test_names.py index 8fd33c65..04a6e753 100644 --- a/tests/test_names.py +++ b/tests/test_names.py @@ -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"), From aba74e359e0dfc65007caefc1229369fa303e7af Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 3 Jan 2025 15:01:43 -0500 Subject: [PATCH 6/6] remove line --- src/ome_types/_mixins/_base_type.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ome_types/_mixins/_base_type.py b/src/ome_types/_mixins/_base_type.py index 3bcbf70f..fb8aaf10 100644 --- a/src/ome_types/_mixins/_base_type.py +++ b/src/ome_types/_mixins/_base_type.py @@ -149,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.