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

feature/pydantic 1 and 2 #3426

Merged
merged 12 commits into from
Mar 30, 2024
6 changes: 6 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Release type: minor

This release adds support for using both Pydantic v1 and v2, when importing from
`pydantic.v1`.

This is automatically detected and the correct version is used.
2 changes: 1 addition & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def tests_integrations(session: Session, integration: str) -> None:
session.run("pytest", *COMMON_PYTEST_OPTIONS, "-m", integration)


@session(python=PYTHON_VERSIONS, name="Pydantic tests", tags=["tests"])
@session(python=PYTHON_VERSIONS, name="Pydantic tests", tags=["tests", "pydantic"])
@nox.parametrize("pydantic", ["1.10", "2.0.3"])
def test_pydantic(session: Session, pydantic: str) -> None:
session.run_always("poetry", "install", external=True)
Expand Down
2 changes: 1 addition & 1 deletion strawberry/experimental/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
try:
from . import pydantic
except ImportError:
except ModuleNotFoundError:
pass
else:
__all__ = ["pydantic"]
239 changes: 211 additions & 28 deletions strawberry/experimental/pydantic/_compat.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import dataclasses
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Type
from decimal import Decimal
from functools import cached_property
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Type
from uuid import UUID

Check warning on line 6 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L3-L6

Added lines #L3 - L6 were not covered by tests

import pydantic

Check warning on line 8 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L8

Added line #L8 was not covered by tests
from pydantic import BaseModel
from pydantic.version import VERSION as PYDANTIC_VERSION

from strawberry.experimental.pydantic.exceptions import UnsupportedTypeError

Check warning on line 12 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L12

Added line #L12 was not covered by tests

if TYPE_CHECKING:
from pydantic.fields import FieldInfo

Expand All @@ -24,21 +30,101 @@
allow_none: bool
has_alias: bool
description: Optional[str]
_missing_type: Any
is_v1: bool

Check warning on line 34 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L33-L34

Added lines #L33 - L34 were not covered by tests
Comment on lines +33 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code_clarification): Consider documenting the purpose of _missing_type and is_v1 attributes.


@property

Check warning on line 36 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L36

Added line #L36 was not covered by tests
def has_default_factory(self) -> bool:
return self.default_factory is not self._missing_type

Check warning on line 38 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L38

Added line #L38 was not covered by tests

if IS_PYDANTIC_V2:
from typing_extensions import get_args, get_origin
@property

Check warning on line 40 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L40

Added line #L40 was not covered by tests
def has_default(self) -> bool:
return self.default is not self._missing_type

Check warning on line 42 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L42

Added line #L42 was not covered by tests

from pydantic._internal._typing_extra import is_new_type
from pydantic._internal._utils import lenient_issubclass, smart_deepcopy
from pydantic_core import PydanticUndefined

PYDANTIC_MISSING_TYPE = PydanticUndefined
ATTR_TO_TYPE_MAP = {

Check warning on line 45 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L45

Added line #L45 was not covered by tests
"NoneStr": Optional[str],
"NoneBytes": Optional[bytes],
"StrBytes": None,
"NoneStrBytes": None,
"StrictStr": str,
"ConstrainedBytes": bytes,
"conbytes": bytes,
"ConstrainedStr": str,
"constr": str,
"EmailStr": str,
"PyObject": None,
"ConstrainedInt": int,
"conint": int,
"PositiveInt": int,
"NegativeInt": int,
"ConstrainedFloat": float,
"confloat": float,
"PositiveFloat": float,
"NegativeFloat": float,
"ConstrainedDecimal": Decimal,
"condecimal": Decimal,
"UUID1": UUID,
"UUID3": UUID,
"UUID4": UUID,
"UUID5": UUID,
"FilePath": None,
"DirectoryPath": None,
"Json": None,
"JsonWrapper": None,
"SecretStr": str,
"SecretBytes": bytes,
"StrictBool": bool,
"StrictInt": int,
"StrictFloat": float,
"PaymentCardNumber": None,
"ByteSize": None,
"AnyUrl": str,
"AnyHttpUrl": str,
"HttpUrl": str,
"PostgresDsn": str,
"RedisDsn": str,
}

ATTR_TO_TYPE_MAP_Pydantic_V2 = {

Check warning on line 89 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L89

Added line #L89 was not covered by tests
"EmailStr": str,
"SecretStr": str,
"SecretBytes": bytes,
"AnyUrl": str,
}

ATTR_TO_TYPE_MAP_Pydantic_Core_V2 = {

Check warning on line 96 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L96

Added line #L96 was not covered by tests
"MultiHostUrl": str,
}

def new_type_supertype(type_: Any) -> Any:
return type_.__supertype__

def get_model_fields(model: Type[BaseModel]) -> Dict[str, CompatModelField]:
def get_fields_map_for_v2() -> Dict[Any, Any]:
import pydantic_core

Check warning on line 102 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L101-L102

Added lines #L101 - L102 were not covered by tests

fields_map = {

Check warning on line 104 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L104

Added line #L104 was not covered by tests
getattr(pydantic, field_name): type
for field_name, type in ATTR_TO_TYPE_MAP_Pydantic_V2.items()
if hasattr(pydantic, field_name)
}
fields_map.update(

Check warning on line 109 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L109

Added line #L109 was not covered by tests
{
getattr(pydantic_core, field_name): type
for field_name, type in ATTR_TO_TYPE_MAP_Pydantic_Core_V2.items()
if hasattr(pydantic_core, field_name)
}
)

patrick91 marked this conversation as resolved.
Show resolved Hide resolved
return fields_map

Check warning on line 117 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L117

Added line #L117 was not covered by tests


class PydanticV2Compat:
@property

Check warning on line 121 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L120-L121

Added lines #L120 - L121 were not covered by tests
def PYDANTIC_MISSING_TYPE(self) -> Any:
from pydantic_core import PydanticUndefined

Check warning on line 123 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L123

Added line #L123 was not covered by tests

return PydanticUndefined

Check warning on line 125 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L125

Added line #L125 was not covered by tests

def get_model_fields(self, model: Type[BaseModel]) -> Dict[str, CompatModelField]:

Check warning on line 127 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L127

Added line #L127 was not covered by tests
field_info: dict[str, FieldInfo] = model.model_fields
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): We've found these issues:

new_fields = {}
# Convert it into CompatModelField
Expand All @@ -55,24 +141,34 @@
allow_none=False,
has_alias=field is not None,
description=field.description,
_missing_type=self.PYDANTIC_MISSING_TYPE,
is_v1=False,
)
return new_fields

else:
from pydantic.typing import ( # type: ignore[no-redef]
get_args,
get_origin,
is_new_type,
new_type_supertype,
)
from pydantic.utils import ( # type: ignore[no-redef]
lenient_issubclass,
smart_deepcopy,
)
@cached_property

Check warning on line 149 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L149

Added line #L149 was not covered by tests
def fields_map(self) -> Dict[Any, Any]:
return get_fields_map_for_v2()

Check warning on line 151 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L151

Added line #L151 was not covered by tests

PYDANTIC_MISSING_TYPE = dataclasses.MISSING # type: ignore[assignment]
def get_basic_type(self, type_: Any) -> Type[Any]:

Check warning on line 153 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L153

Added line #L153 was not covered by tests
if type_ in self.fields_map:
type_ = self.fields_map[type_]

Check warning on line 155 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L155

Added line #L155 was not covered by tests

def get_model_fields(model: Type[BaseModel]) -> Dict[str, CompatModelField]:
if type_ is None:

Check warning on line 157 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L157

Added line #L157 was not covered by tests
raise UnsupportedTypeError()

if is_new_type(type_):
return new_type_supertype(type_)

Check warning on line 161 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L161

Added line #L161 was not covered by tests

return type_

Check warning on line 163 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L163

Added line #L163 was not covered by tests
Comment on lines +160 to +163
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): We've found these issues:

Suggested change
if is_new_type(type_):
return new_type_supertype(type_)
return type_
return new_type_supertype(type_) if is_new_type(type_) else type_



class PydanticV1Compat:
@property

Check warning on line 167 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L166-L167

Added lines #L166 - L167 were not covered by tests
def PYDANTIC_MISSING_TYPE(self) -> Any:
return dataclasses.MISSING

Check warning on line 169 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L169

Added line #L169 was not covered by tests

def get_model_fields(self, model: Type[BaseModel]) -> Dict[str, CompatModelField]:

Check warning on line 171 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L171

Added line #L171 was not covered by tests
new_fields = {}
patrick91 marked this conversation as resolved.
Show resolved Hide resolved
# Convert it into CompatModelField
for name, field in model.__fields__.items(): # type: ignore[attr-defined]
Expand All @@ -87,17 +183,104 @@
allow_none=field.allow_none,
has_alias=field.has_alias,
description=field.field_info.description,
_missing_type=self.PYDANTIC_MISSING_TYPE,
is_v1=True,
)
return new_fields

@cached_property

Check warning on line 191 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L191

Added line #L191 was not covered by tests
def fields_map(self) -> Dict[Any, Any]:
if IS_PYDANTIC_V2:
return {

Check warning on line 194 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L194

Added line #L194 was not covered by tests
getattr(pydantic.v1, field_name): type
for field_name, type in ATTR_TO_TYPE_MAP.items()
if hasattr(pydantic.v1, field_name)
}

return {

Check warning on line 200 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L200

Added line #L200 was not covered by tests
getattr(pydantic, field_name): type
for field_name, type in ATTR_TO_TYPE_MAP.items()
if hasattr(pydantic, field_name)
}

def get_basic_type(self, type_: Any) -> Type[Any]:

Check warning on line 206 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L206

Added line #L206 was not covered by tests
if IS_PYDANTIC_V1:
ConstrainedInt = pydantic.ConstrainedInt
ConstrainedFloat = pydantic.ConstrainedFloat
ConstrainedStr = pydantic.ConstrainedStr
ConstrainedList = pydantic.ConstrainedList

Check warning on line 211 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L208-L211

Added lines #L208 - L211 were not covered by tests
else:
ConstrainedInt = pydantic.v1.ConstrainedInt
ConstrainedFloat = pydantic.v1.ConstrainedFloat
ConstrainedStr = pydantic.v1.ConstrainedStr
ConstrainedList = pydantic.v1.ConstrainedList

Check warning on line 216 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L213-L216

Added lines #L213 - L216 were not covered by tests

if lenient_issubclass(type_, ConstrainedInt):
return int

Check warning on line 219 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L219

Added line #L219 was not covered by tests
if lenient_issubclass(type_, ConstrainedFloat):
return float

Check warning on line 221 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L221

Added line #L221 was not covered by tests
if lenient_issubclass(type_, ConstrainedStr):
return str

Check warning on line 223 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L223

Added line #L223 was not covered by tests
if lenient_issubclass(type_, ConstrainedList):
return List[self.get_basic_type(type_.item_type)] # type: ignore

Check warning on line 225 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L225

Added line #L225 was not covered by tests

if type_ in self.fields_map:
type_ = self.fields_map[type_]

Check warning on line 228 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L228

Added line #L228 was not covered by tests

if type_ is None:

Check warning on line 230 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L230

Added line #L230 was not covered by tests
raise UnsupportedTypeError()

if is_new_type(type_):
return new_type_supertype(type_)

Check warning on line 234 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L234

Added line #L234 was not covered by tests

return type_

Check warning on line 236 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L236

Added line #L236 was not covered by tests
patrick91 marked this conversation as resolved.
Show resolved Hide resolved


class PydanticCompat:
def __init__(self, is_v2: bool):

Check warning on line 240 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L239-L240

Added lines #L239 - L240 were not covered by tests
if is_v2:
self._compat = PydanticV2Compat()

Check warning on line 242 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L242

Added line #L242 was not covered by tests
else:
self._compat = PydanticV1Compat() # type: ignore[assignment]

Check warning on line 244 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L244

Added line #L244 was not covered by tests
patrick91 marked this conversation as resolved.
Show resolved Hide resolved

@classmethod

Check warning on line 246 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L246

Added line #L246 was not covered by tests
def from_model(cls, model: Type[BaseModel]) -> "PydanticCompat":
if hasattr(model, "model_fields"):
return cls(is_v2=True)

Check warning on line 249 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L249

Added line #L249 was not covered by tests

return cls(is_v2=False)

Check warning on line 251 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L251

Added line #L251 was not covered by tests
patrick91 marked this conversation as resolved.
Show resolved Hide resolved

def __getattr__(self, name: str) -> Any:
return getattr(self._compat, name)

Check warning on line 254 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L253-L254

Added lines #L253 - L254 were not covered by tests


if IS_PYDANTIC_V2:
from typing_extensions import get_args, get_origin

Check warning on line 258 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L258

Added line #L258 was not covered by tests

from pydantic._internal._typing_extra import is_new_type
from pydantic._internal._utils import lenient_issubclass, smart_deepcopy

Check warning on line 261 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L260-L261

Added lines #L260 - L261 were not covered by tests

def new_type_supertype(type_: Any) -> Any:
return type_.__supertype__

Check warning on line 264 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L263-L264

Added lines #L263 - L264 were not covered by tests
else:
from pydantic.typing import ( # type: ignore[no-redef]

Check warning on line 266 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L266

Added line #L266 was not covered by tests
get_args,
get_origin,
is_new_type,
new_type_supertype,
)
from pydantic.utils import ( # type: ignore[no-redef]

Check warning on line 272 in strawberry/experimental/pydantic/_compat.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/_compat.py#L272

Added line #L272 was not covered by tests
lenient_issubclass,
smart_deepcopy,
)


__all__ = [
"smart_deepcopy",
"PydanticCompat",
"is_new_type",
"lenient_issubclass",
"get_args",
"get_origin",
"is_new_type",
"get_args",
"new_type_supertype",
"get_model_fields",
"PYDANTIC_MISSING_TYPE",
"smart_deepcopy",
]
5 changes: 3 additions & 2 deletions strawberry/experimental/pydantic/error_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from strawberry.auto import StrawberryAuto
from strawberry.experimental.pydantic._compat import (
CompatModelField,
get_model_fields,
PydanticCompat,
lenient_issubclass,
)
from strawberry.experimental.pydantic.utils import (
Expand Down Expand Up @@ -72,7 +72,8 @@
all_fields: bool = False,
) -> Callable[..., Type]:
def wrap(cls: Type) -> Type:
model_fields = get_model_fields(model)
compat = PydanticCompat.from_model(model)
model_fields = compat.get_model_fields(model)

Check warning on line 76 in strawberry/experimental/pydantic/error_type.py

View check run for this annotation

Codecov / codecov/patch

strawberry/experimental/pydantic/error_type.py#L75-L76

Added lines #L75 - L76 were not covered by tests
fields_set = set(fields) if fields else set()

if fields:
Expand Down
Loading
Loading