Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: remove support for pydantic v1 #275

Merged
merged 6 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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)

Check warning on line 21 in src/ome_types/_mixins/_kinded.py

View check run for this annotation

Codecov / codecov/patch

src/ome_types/_mixins/_kinded.py#L21

Added line #L21 was not covered by tests
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 @@
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)

Check warning on line 142 in src/ome_types/widget.py

View check run for this annotation

Codecov / codecov/patch

src/ome_types/widget.py#L142

Added line #L142 was not covered by tests
self._fill_item(data)

def _fill_item(self, obj: Any, item: QTreeWidgetItem = None) -> None:
Expand Down
Loading
Loading