-
-
Notifications
You must be signed in to change notification settings - Fork 544
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
feature/pydantic 1 and 2 #3426
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
e72db0d
Remove unused if statement
patrick91 5cc04bf
Initial stub at supporting both pydantic v1 and v2
patrick91 df9812a
Better catch
patrick91 a9d9c89
Fix import
patrick91 9bf8752
Reduce number of places that need to know about pydantic v2/v1
patrick91 56e77cc
Use compat to get type
patrick91 2f02448
Move fields map inside v1/v2 compat
patrick91 a03fc94
V1 and V2 supposedly working
patrick91 2a4739d
Fix mypy
patrick91 fd7e83a
Add release file
patrick91 1bc7055
Fix subclasses check
patrick91 4a1a498
Unfail test
patrick91 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||||||||||||
|
||||||||||||
import pydantic | ||||||||||||
from pydantic import BaseModel | ||||||||||||
from pydantic.version import VERSION as PYDANTIC_VERSION | ||||||||||||
|
||||||||||||
from strawberry.experimental.pydantic.exceptions import UnsupportedTypeError | ||||||||||||
|
||||||||||||
if TYPE_CHECKING: | ||||||||||||
from pydantic.fields import FieldInfo | ||||||||||||
|
||||||||||||
|
@@ -24,21 +30,101 @@ | |||||||||||
allow_none: bool | ||||||||||||
has_alias: bool | ||||||||||||
description: Optional[str] | ||||||||||||
_missing_type: Any | ||||||||||||
is_v1: bool | ||||||||||||
|
||||||||||||
@property | ||||||||||||
def has_default_factory(self) -> bool: | ||||||||||||
return self.default_factory is not self._missing_type | ||||||||||||
|
||||||||||||
if IS_PYDANTIC_V2: | ||||||||||||
from typing_extensions import get_args, get_origin | ||||||||||||
@property | ||||||||||||
def has_default(self) -> bool: | ||||||||||||
return self.default is not self._missing_type | ||||||||||||
|
||||||||||||
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 = { | ||||||||||||
"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 = { | ||||||||||||
"EmailStr": str, | ||||||||||||
"SecretStr": str, | ||||||||||||
"SecretBytes": bytes, | ||||||||||||
"AnyUrl": str, | ||||||||||||
} | ||||||||||||
|
||||||||||||
ATTR_TO_TYPE_MAP_Pydantic_Core_V2 = { | ||||||||||||
"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 | ||||||||||||
|
||||||||||||
fields_map = { | ||||||||||||
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( | ||||||||||||
{ | ||||||||||||
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 | ||||||||||||
|
||||||||||||
|
||||||||||||
class PydanticV2Compat: | ||||||||||||
@property | ||||||||||||
def PYDANTIC_MISSING_TYPE(self) -> Any: | ||||||||||||
from pydantic_core import PydanticUndefined | ||||||||||||
|
||||||||||||
return PydanticUndefined | ||||||||||||
|
||||||||||||
def get_model_fields(self, model: Type[BaseModel]) -> Dict[str, CompatModelField]: | ||||||||||||
field_info: dict[str, FieldInfo] = model.model_fields | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||||||||
|
@@ -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 | ||||||||||||
def fields_map(self) -> Dict[Any, Any]: | ||||||||||||
return get_fields_map_for_v2() | ||||||||||||
|
||||||||||||
PYDANTIC_MISSING_TYPE = dataclasses.MISSING # type: ignore[assignment] | ||||||||||||
def get_basic_type(self, type_: Any) -> Type[Any]: | ||||||||||||
if type_ in self.fields_map: | ||||||||||||
type_ = self.fields_map[type_] | ||||||||||||
|
||||||||||||
def get_model_fields(model: Type[BaseModel]) -> Dict[str, CompatModelField]: | ||||||||||||
if type_ is None: | ||||||||||||
raise UnsupportedTypeError() | ||||||||||||
|
||||||||||||
if is_new_type(type_): | ||||||||||||
return new_type_supertype(type_) | ||||||||||||
|
||||||||||||
return type_ | ||||||||||||
Comment on lines
+160
to
+163
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (code-quality): We've found these issues:
Suggested change
|
||||||||||||
|
||||||||||||
|
||||||||||||
class PydanticV1Compat: | ||||||||||||
@property | ||||||||||||
def PYDANTIC_MISSING_TYPE(self) -> Any: | ||||||||||||
return dataclasses.MISSING | ||||||||||||
|
||||||||||||
def get_model_fields(self, model: Type[BaseModel]) -> Dict[str, CompatModelField]: | ||||||||||||
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] | ||||||||||||
|
@@ -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 | ||||||||||||
def fields_map(self) -> Dict[Any, Any]: | ||||||||||||
if IS_PYDANTIC_V2: | ||||||||||||
return { | ||||||||||||
getattr(pydantic.v1, field_name): type | ||||||||||||
for field_name, type in ATTR_TO_TYPE_MAP.items() | ||||||||||||
if hasattr(pydantic.v1, field_name) | ||||||||||||
} | ||||||||||||
|
||||||||||||
return { | ||||||||||||
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]: | ||||||||||||
if IS_PYDANTIC_V1: | ||||||||||||
ConstrainedInt = pydantic.ConstrainedInt | ||||||||||||
ConstrainedFloat = pydantic.ConstrainedFloat | ||||||||||||
ConstrainedStr = pydantic.ConstrainedStr | ||||||||||||
ConstrainedList = pydantic.ConstrainedList | ||||||||||||
else: | ||||||||||||
ConstrainedInt = pydantic.v1.ConstrainedInt | ||||||||||||
ConstrainedFloat = pydantic.v1.ConstrainedFloat | ||||||||||||
ConstrainedStr = pydantic.v1.ConstrainedStr | ||||||||||||
ConstrainedList = pydantic.v1.ConstrainedList | ||||||||||||
|
||||||||||||
if lenient_issubclass(type_, ConstrainedInt): | ||||||||||||
return int | ||||||||||||
if lenient_issubclass(type_, ConstrainedFloat): | ||||||||||||
return float | ||||||||||||
if lenient_issubclass(type_, ConstrainedStr): | ||||||||||||
return str | ||||||||||||
if lenient_issubclass(type_, ConstrainedList): | ||||||||||||
return List[self.get_basic_type(type_.item_type)] # type: ignore | ||||||||||||
|
||||||||||||
if type_ in self.fields_map: | ||||||||||||
type_ = self.fields_map[type_] | ||||||||||||
|
||||||||||||
if type_ is None: | ||||||||||||
raise UnsupportedTypeError() | ||||||||||||
|
||||||||||||
if is_new_type(type_): | ||||||||||||
return new_type_supertype(type_) | ||||||||||||
|
||||||||||||
return type_ | ||||||||||||
patrick91 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
|
||||||||||||
|
||||||||||||
class PydanticCompat: | ||||||||||||
def __init__(self, is_v2: bool): | ||||||||||||
if is_v2: | ||||||||||||
self._compat = PydanticV2Compat() | ||||||||||||
else: | ||||||||||||
self._compat = PydanticV1Compat() # type: ignore[assignment] | ||||||||||||
patrick91 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
|
||||||||||||
@classmethod | ||||||||||||
def from_model(cls, model: Type[BaseModel]) -> "PydanticCompat": | ||||||||||||
if hasattr(model, "model_fields"): | ||||||||||||
return cls(is_v2=True) | ||||||||||||
|
||||||||||||
return cls(is_v2=False) | ||||||||||||
patrick91 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
|
||||||||||||
def __getattr__(self, name: str) -> Any: | ||||||||||||
return getattr(self._compat, name) | ||||||||||||
|
||||||||||||
|
||||||||||||
if IS_PYDANTIC_V2: | ||||||||||||
from typing_extensions import get_args, get_origin | ||||||||||||
|
||||||||||||
from pydantic._internal._typing_extra import is_new_type | ||||||||||||
from pydantic._internal._utils import lenient_issubclass, smart_deepcopy | ||||||||||||
|
||||||||||||
def new_type_supertype(type_: Any) -> Any: | ||||||||||||
return type_.__supertype__ | ||||||||||||
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, | ||||||||||||
) | ||||||||||||
|
||||||||||||
|
||||||||||||
__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", | ||||||||||||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
andis_v1
attributes.