From 5b63cb355561108158fd3332faf65f0f5e6f1353 Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Wed, 11 Sep 2024 16:52:16 +0200 Subject: [PATCH 1/3] Run bump-pydantic tool --- conda_lock/lockfile/v1/models.py | 4 ++++ conda_lock/models/channel.py | 12 ++++++------ conda_lock/models/lock_spec.py | 17 ++++++++++------- conda_lock/virtual_package.py | 9 ++++----- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/conda_lock/lockfile/v1/models.py b/conda_lock/lockfile/v1/models.py index e431025a..8eb5db89 100644 --- a/conda_lock/lockfile/v1/models.py +++ b/conda_lock/lockfile/v1/models.py @@ -61,6 +61,8 @@ class BaseLockedDependency(StrictModel): def key(self) -> LockKey: return LockKey(self.manager, self.name, self.platform) + # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually. + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information. @validator("hash") def validate_hash(cls, v: HashModel, values: Dict[str, typing.Any]) -> HashModel: if (values["manager"] == "conda") and (v.md5 is None): @@ -282,6 +284,8 @@ def __or__(self, other: "LockMeta") -> "LockMeta": custom_metadata=new_custom_metadata, ) + # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually. + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information. @validator("channels", pre=True, always=True) def ensure_channels(cls, v: List[Union[str, Channel]]) -> List[Channel]: res: List[Channel] = [] diff --git a/conda_lock/models/channel.py b/conda_lock/models/channel.py index aa296c17..7103a456 100644 --- a/conda_lock/models/channel.py +++ b/conda_lock/models/channel.py @@ -57,14 +57,14 @@ class CondaUrl(BaseModel): raw_url: str env_var_url: str - token: Optional[str] - token_env_var: Optional[str] + token: Optional[str] = None + token_env_var: Optional[str] = None - user: Optional[str] - user_env_var: Optional[str] + user: Optional[str] = None + user_env_var: Optional[str] = None - password: Optional[str] - password_env_var: Optional[str] + password: Optional[str] = None + password_env_var: Optional[str] = None @classmethod def from_string(cls, value: str) -> "CondaUrl": diff --git a/conda_lock/models/lock_spec.py b/conda_lock/models/lock_spec.py index bcba92c5..6074eb41 100644 --- a/conda_lock/models/lock_spec.py +++ b/conda_lock/models/lock_spec.py @@ -5,7 +5,7 @@ from typing import Dict, List, Optional, Union -from pydantic import BaseModel, Field, validator +from pydantic import field_validator, BaseModel, Field from typing_extensions import Literal from conda_lock.models import StrictModel @@ -21,7 +21,8 @@ class _BaseDependency(StrictModel): extras: List[str] = [] markers: Optional[str] = None - @validator("extras") + @field_validator("extras") + @classmethod def sorted_extras(cls, v: List[str]) -> List[str]: return sorted(v) @@ -53,11 +54,11 @@ class Package(StrictModel): class PoetryMappedDependencySpec(StrictModel): - url: Optional[str] + url: Optional[str] = None manager: Literal["conda", "pip"] extras: List - markers: Optional[str] - poetry_version_spec: Optional[str] + markers: Optional[str] = None + poetry_version_spec: Optional[str] = None class LockSpecification(BaseModel): @@ -104,7 +105,8 @@ def content_hash_for_platform( env_spec = json.dumps(data, sort_keys=True) return hashlib.sha256(env_spec.encode("utf-8")).hexdigest() - @validator("channels", pre=True) + @field_validator("channels", mode="before") + @classmethod def validate_channels(cls, v: List[Union[Channel, str]]) -> List[Channel]: for i, e in enumerate(v): if isinstance(e, str): @@ -114,7 +116,8 @@ def validate_channels(cls, v: List[Union[Channel, str]]) -> List[Channel]: raise ValueError("nodefaults channel is not allowed, ref #418") return typing.cast(List[Channel], v) - @validator("pip_repositories", pre=True) + @field_validator("pip_repositories", mode="before") + @classmethod def validate_pip_repositories( cls, value: List[Union[PipRepository, str]] ) -> List[PipRepository]: diff --git a/conda_lock/virtual_package.py b/conda_lock/virtual_package.py index b9b9b4d6..4d1913a7 100644 --- a/conda_lock/virtual_package.py +++ b/conda_lock/virtual_package.py @@ -8,7 +8,7 @@ from types import TracebackType from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Type -from pydantic import BaseModel, Field, validator +from pydantic import field_validator, ConfigDict, BaseModel, Field from conda_lock.interfaces.vendored_conda import MatchSpec from conda_lock.models.channel import Channel @@ -22,9 +22,7 @@ class FakePackage(BaseModel): """A minimal representation of the required metadata for a conda package""" - - class Config: - frozen = True + model_config = ConfigDict(frozen=True) name: str version: str = "1.0" @@ -236,7 +234,8 @@ def default_virtual_package_repodata(cuda_version: str = "11.4") -> FakeRepoData class VirtualPackageSpecSubdir(BaseModel): packages: Dict[str, str] - @validator("packages") + @field_validator("packages") + @classmethod def validate_packages(cls, v: Dict[str, str]) -> Dict[str, str]: for package_name in v: if not package_name.startswith("__"): From aca2ce5557358b40f1e3677ebfd475acb852992f Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Wed, 11 Sep 2024 17:12:57 +0200 Subject: [PATCH 2/3] Manual pydantic migration --- conda_lock/lockfile/v1/models.py | 25 ++++++++++++------------- conda_lock/models/lock_spec.py | 10 ++++++---- conda_lock/virtual_package.py | 5 +++-- tests/test_conda_lock.py | 4 +++- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/conda_lock/lockfile/v1/models.py b/conda_lock/lockfile/v1/models.py index 8eb5db89..a160c7f8 100644 --- a/conda_lock/lockfile/v1/models.py +++ b/conda_lock/lockfile/v1/models.py @@ -4,7 +4,6 @@ import json import logging import pathlib -import typing from collections import namedtuple from typing import ( @@ -22,7 +21,7 @@ if TYPE_CHECKING: from hashlib import _Hash -from pydantic import Field, validator +from pydantic import Field, ValidationInfo, field_validator from typing_extensions import Literal from conda_lock.common import ordered_union, relative_path @@ -61,11 +60,10 @@ class BaseLockedDependency(StrictModel): def key(self) -> LockKey: return LockKey(self.manager, self.name, self.platform) - # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually. - # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information. - @validator("hash") - def validate_hash(cls, v: HashModel, values: Dict[str, typing.Any]) -> HashModel: - if (values["manager"] == "conda") and (v.md5 is None): + @field_validator("hash") + @classmethod + def validate_hash(cls, v: HashModel, info: ValidationInfo) -> HashModel: + if (info.data["manager"] == "conda") and (v.md5 is None): raise ValueError("conda package hashes must use MD5") return v @@ -219,7 +217,7 @@ class LockMeta(StrictModel): ..., description="Hash of dependencies for each target platform" ) channels: List[Channel] = Field( - ..., description="Channels used to resolve dependencies" + ..., description="Channels used to resolve dependencies", validate_default=True ) platforms: List[str] = Field(..., description="Target platforms") sources: List[str] = Field( @@ -284,9 +282,8 @@ def __or__(self, other: "LockMeta") -> "LockMeta": custom_metadata=new_custom_metadata, ) - # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually. - # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information. - @validator("channels", pre=True, always=True) + @field_validator("channels", mode="before") + @classmethod def ensure_channels(cls, v: List[Union[str, Channel]]) -> List[Channel]: res: List[Channel] = [] for e in v: @@ -308,10 +305,12 @@ def dict_for_output(self) -> Dict[str, Any]: return { "version": Lockfile.version, "metadata": json.loads( - self.metadata.json(by_alias=True, exclude_unset=True, exclude_none=True) + self.metadata.model_dump_json( + by_alias=True, exclude_unset=True, exclude_none=True + ) ), "package": [ - package.dict(by_alias=True, exclude_unset=True, exclude_none=True) + package.model_dump(by_alias=True, exclude_unset=True, exclude_none=True) for package in self.package ], } diff --git a/conda_lock/models/lock_spec.py b/conda_lock/models/lock_spec.py index 6074eb41..daba4c46 100644 --- a/conda_lock/models/lock_spec.py +++ b/conda_lock/models/lock_spec.py @@ -5,7 +5,7 @@ from typing import Dict, List, Optional, Union -from pydantic import field_validator, BaseModel, Field +from pydantic import BaseModel, Field, field_validator from typing_extensions import Literal from conda_lock.models import StrictModel @@ -85,16 +85,18 @@ def content_hash_for_platform( self, platform: str, virtual_package_repo: Optional[FakeRepoData] ) -> str: data = { - "channels": [c.json() for c in self.channels], + "channels": [c.model_dump_json() for c in self.channels], "specs": [ - p.dict() + p.model_dump() for p in sorted( self.dependencies[platform], key=lambda p: (p.manager, p.name) ) ], } if self.pip_repositories: - data["pip_repositories"] = [repo.json() for repo in self.pip_repositories] + data["pip_repositories"] = [ + repo.model_dump_json() for repo in self.pip_repositories + ] if virtual_package_repo is not None: vpr_data = virtual_package_repo.all_repodata data["virtual_package_hash"] = { diff --git a/conda_lock/virtual_package.py b/conda_lock/virtual_package.py index 4d1913a7..7f372da8 100644 --- a/conda_lock/virtual_package.py +++ b/conda_lock/virtual_package.py @@ -8,7 +8,7 @@ from types import TracebackType from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Type -from pydantic import field_validator, ConfigDict, BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field, field_validator from conda_lock.interfaces.vendored_conda import MatchSpec from conda_lock.models.channel import Channel @@ -22,6 +22,7 @@ class FakePackage(BaseModel): """A minimal representation of the required metadata for a conda package""" + model_config = ConfigDict(frozen=True) name: str @@ -34,7 +35,7 @@ class FakePackage(BaseModel): package_type: Optional[str] = "virtual_system" def to_repodata_entry(self) -> Tuple[str, Dict[str, Any]]: - out = self.dict() + out = self.model_dump() if self.build_string: build = f"{self.build_string}_{self.build_number}" else: diff --git a/tests/test_conda_lock.py b/tests/test_conda_lock.py index 2f999fa5..0d3467b9 100644 --- a/tests/test_conda_lock.py +++ b/tests/test_conda_lock.py @@ -1727,7 +1727,9 @@ def test_aggregate_lock_specs(): ], sources=[], ) - assert actual.dict(exclude={"sources"}) == expected.dict(exclude={"sources"}) + assert actual.model_dump(exclude={"sources"}) == expected.model_dump( + exclude={"sources"} + ) assert actual.content_hash(None) == expected.content_hash(None) From 345fbcb8b1c4fc687e56d5ca378ff18fb0b93554 Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Wed, 11 Sep 2024 17:27:41 +0200 Subject: [PATCH 3/3] Disallow Pydantic v1.10, require >=2 This is based on the experiment carried out in #693. In particular, ValidationInfo can't be imported from Pydantic v1. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 49e7e191..9d7c85f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "ensureconda >=1.4.4", "gitpython >=3.1.30", "jinja2", - "pydantic >=1.10", + "pydantic >=2", "pyyaml >= 5.1", # constraint on version comes from poetry "requests >=2.26,<3.0",