From 65d79118c0e8d1ca4622b41c837a1ebb8e54a0cd Mon Sep 17 00:00:00 2001 From: Peter Bull Date: Sun, 9 Jul 2023 17:38:58 -0700 Subject: [PATCH] pydantic v2 compatibility --- HISTORY.md | 5 +++-- cloudpathlib/__init__.py | 7 ++++--- cloudpathlib/anypath.py | 31 ++++++++++++++++++++++--------- cloudpathlib/cloudpath.py | 20 ++++++++++++++------ docs/docs/integrations.md | 14 ++++++++------ pyproject.toml | 2 +- requirements-dev.txt | 2 +- 7 files changed, 53 insertions(+), 28 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 83b02e51..11ea056a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,9 +1,10 @@ # cloudpathlib Changelog +## v0.15.2 (2023-07-09) + - Compatibility with pydantic >= 2.0.0. + ## v0.15.1 (2023-06-16) - Warn if Pydantic >= 2.0.0 since we are not yet compatible. - - ## v0.15.0 (2023-06-16) - Changed return type for `CloudPathMeta.__call__` to fix problems with pyright/pylance ([PR #330](https://github.com/drivendataorg/cloudpathlib/pull/330)) diff --git a/cloudpathlib/__init__.py b/cloudpathlib/__init__.py index 68408987..65001423 100644 --- a/cloudpathlib/__init__.py +++ b/cloudpathlib/__init__.py @@ -35,9 +35,10 @@ class PydanticVersionWarning(UserWarning): message = ( - "This version of cloudpathlib is only compatible with pydantic<2.0.0. " + "This version of cloudpathlib is only compatible with pydantic>=2.0.0. " "You can ignore this warning if none of your pydantic models are " - "annotated with cloudpathlib types." + "annotated with cloudpathlib types. cloudpathlib=0.15.1 is the last " + "version that supports pydantic<2.0.0." ) @@ -45,7 +46,7 @@ class PydanticVersionWarning(UserWarning): import pydantic from packaging.version import parse - if parse(pydantic.__version__) >= parse("2.0.0"): + if parse(pydantic.__version__) < parse("2.0.0"): warn(PydanticVersionWarning(PydanticVersionWarning.message)) except ImportError: diff --git a/cloudpathlib/anypath.py b/cloudpathlib/anypath.py index 197dbe65..e04befcc 100644 --- a/cloudpathlib/anypath.py +++ b/cloudpathlib/anypath.py @@ -1,7 +1,7 @@ import os from abc import ABC from pathlib import Path -from typing import Union +from typing import Any, Union from .cloudpath import InvalidPrefixError, CloudPath from .exceptions import AnyPathTypeError @@ -30,18 +30,31 @@ def __new__(cls, *args, **kwargs) -> Union[CloudPath, Path]: # type: ignore f"Path exception: {repr(path_exception)}" ) + # =========== pydantic integration special methods =============== @classmethod - def __get_validators__(cls): + def __get_pydantic_core_schema__(cls, _source_type: Any, _handler): """Pydantic special method. See - https://pydantic-docs.helpmanual.io/usage/types/#custom-data-types""" - yield cls._validate + https://docs.pydantic.dev/2.0/usage/types/custom/""" + try: + from pydantic_core import core_schema + + return core_schema.no_info_after_validator_function( + cls.validate, + core_schema.any_schema(), + ) + except ImportError: + return None @classmethod - def _validate(cls, value) -> Union[CloudPath, Path]: - """Used as a Pydantic validator. See - https://pydantic-docs.helpmanual.io/usage/types/#custom-data-types""" - # Note __new__ is static method and not a class method - return cls.__new__(cls, value) + def validate(cls, v: str) -> Union[CloudPath, Path]: + """Pydantic special method. See + https://docs.pydantic.dev/2.0/usage/types/custom/""" + try: + return cls.__new__(cls, v) + except AnyPathTypeError as e: + # type errors no longer converted to validation errors + # https://docs.pydantic.dev/2.0/migration/#typeerror-is-no-longer-converted-to-validationerror-in-validators + raise ValueError(e) AnyPath.register(CloudPath) # type: ignore diff --git a/cloudpathlib/cloudpath.py b/cloudpathlib/cloudpath.py index 42b43de3..7b08e08c 100644 --- a/cloudpathlib/cloudpath.py +++ b/cloudpathlib/cloudpath.py @@ -1113,16 +1113,24 @@ def _upload_file_to_cloud( # =========== pydantic integration special methods =============== @classmethod - def __get_validators__(cls) -> Generator[Callable[[Any], Self], None, None]: + def __get_pydantic_core_schema__(cls, _source_type: Any, _handler): """Pydantic special method. See - https://pydantic-docs.helpmanual.io/usage/types/#custom-data-types""" - yield cls._validate + https://docs.pydantic.dev/2.0/usage/types/custom/""" + try: + from pydantic_core import core_schema + + return core_schema.no_info_after_validator_function( + cls.validate, + core_schema.any_schema(), + ) + except ImportError: + return None @classmethod - def _validate(cls, value: Any) -> Self: + def validate(cls, v: str) -> Self: """Used as a Pydantic validator. See - https://pydantic-docs.helpmanual.io/usage/types/#custom-data-types""" - return cls(value) + https://docs.pydantic.dev/2.0/usage/types/custom/""" + return cls(v) # The function resolve is not available on Pure paths because it removes relative diff --git a/docs/docs/integrations.md b/docs/docs/integrations.md index 456612ae..25f46582 100644 --- a/docs/docs/integrations.md +++ b/docs/docs/integrations.md @@ -2,14 +2,16 @@ ## Pydantic -!!! warning +`cloudpathlib` integrates with [Pydantic](https://pydantic-docs.helpmanual.io/)'s data validation. You can declare fields with cloud path classes, and Pydantic's validation mechanisms will run inputs through the cloud path's constructor. - As of version `0.15.1`, `cloudpathlib` is not compatible with - version `2.0.0` or greater of Pydantic. If you want to annotate - your pydantic models with cloudpathlib types, you need to use - `pydantic<2`. +### Compatibility table -`cloudpathlib` integrates with [Pydantic](https://pydantic-docs.helpmanual.io/)'s data validation. You can declare fields with cloud path classes, and Pydantic's validation mechanisms will run inputs through the cloud path's constructor. +| `pydantic` version | `cloudpathlib` version | +| --- | --- | +| `>=2.0.0` | `>=0.15.2` | +| `<2.0.0` | `<0.15.2` | + +### Example ```python from cloudpathlib import S3Path diff --git a/pyproject.toml b/pyproject.toml index 998de781..2acea0b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "cloudpathlib" -version = "0.15.1" +version = "0.15.2" description = "pathlib-style classes for cloud storage services." readme = "README.md" authors = [{ name = "DrivenData", email = "info@drivendata.org" }] diff --git a/requirements-dev.txt b/requirements-dev.txt index c936b3d2..3acae4f4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -18,7 +18,7 @@ nbautoexport pandas pillow psutil -pydantic<2 +pydantic pytest pytest-cases pytest-cov