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
  • Loading branch information
tlambert03 authored Jan 3, 2025
1 parent ae694b6 commit e294e4c
Show file tree
Hide file tree
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
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
Expand Up @@ -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"
Expand Down Expand Up @@ -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 = [
Expand Down
17 changes: 3 additions & 14 deletions src/ome_autogen/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/ome_autogen/overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("],
},
Expand Down
23 changes: 4 additions & 19 deletions src/ome_types/_mixins/_base_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
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):
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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():
Expand Down
5 changes: 1 addition & 4 deletions src/ome_types/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading

0 comments on commit e294e4c

Please sign in to comment.