Browse Source

♻️ Handle Pydantic v1 imports for checks conditionally, filtering warnings

pull/14609/head
Sebastián Ramírez 7 months ago
parent
commit
b32e65f5af
  1. 2
      fastapi/_compat/__init__.py
  2. 3
      fastapi/_compat/may_v1.py
  3. 20
      fastapi/_compat/shared.py
  4. 9
      fastapi/encoders.py
  5. 8
      tests/test_jsonable_encoder.py
  6. 6
      tests/test_pydantic_v1_error.py

2
fastapi/_compat/__init__.py

@ -31,6 +31,8 @@ from .shared import PYDANTIC_V2 as PYDANTIC_V2
from .shared import PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE from .shared import PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE
from .shared import annotation_is_pydantic_v1 as annotation_is_pydantic_v1 from .shared import annotation_is_pydantic_v1 as annotation_is_pydantic_v1
from .shared import field_annotation_is_scalar as field_annotation_is_scalar from .shared import field_annotation_is_scalar as field_annotation_is_scalar
from .shared import is_pydantic_v1_model_class as is_pydantic_v1_model_class
from .shared import is_pydantic_v1_model_instance as is_pydantic_v1_model_instance
from .shared import ( from .shared import (
is_uploadfile_or_nonable_uploadfile_annotation as is_uploadfile_or_nonable_uploadfile_annotation, is_uploadfile_or_nonable_uploadfile_annotation as is_uploadfile_or_nonable_uploadfile_annotation,
) )

3
fastapi/_compat/may_v1.py

@ -1,9 +1,6 @@
from collections.abc import Sequence from collections.abc import Sequence
from typing import Any, Union from typing import Any, Union
from pydantic.v1 import BaseModel as BaseModel
from pydantic.v1.fields import ModelField as ModelField
def _regenerate_error_with_loc( def _regenerate_error_with_loc(
*, errors: Sequence[Any], loc_prefix: tuple[Union[str, int], ...] *, errors: Sequence[Any], loc_prefix: tuple[Union[str, int], ...]

20
fastapi/_compat/shared.py

@ -1,6 +1,7 @@
import sys import sys
import types import types
import typing import typing
import warnings
from collections import deque from collections import deque
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from dataclasses import is_dataclass from dataclasses import is_dataclass
@ -10,7 +11,6 @@ from typing import (
Union, Union,
) )
from fastapi._compat import may_v1
from fastapi.types import UnionType from fastapi.types import UnionType
from pydantic import BaseModel from pydantic import BaseModel
from pydantic.version import VERSION as PYDANTIC_VERSION from pydantic.version import VERSION as PYDANTIC_VERSION
@ -177,13 +177,27 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool:
) )
def is_pydantic_v1_model_instance(obj: Any) -> bool:
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
from pydantic import v1
return isinstance(obj, v1.BaseModel)
def is_pydantic_v1_model_class(cls: Any) -> bool:
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
from pydantic import v1
return lenient_issubclass(cls, v1.BaseModel)
def annotation_is_pydantic_v1(annotation: Any) -> bool: def annotation_is_pydantic_v1(annotation: Any) -> bool:
if lenient_issubclass(annotation, may_v1.BaseModel): if is_pydantic_v1_model_class(annotation):
return True return True
origin = get_origin(annotation) origin = get_origin(annotation)
if origin is Union or origin is UnionType: if origin is Union or origin is UnionType:
for arg in get_args(annotation): for arg in get_args(annotation):
if lenient_issubclass(arg, may_v1.BaseModel): if is_pydantic_v1_model_class(arg):
return True return True
if field_annotation_is_sequence(annotation): if field_annotation_is_sequence(annotation):
for sub_annotation in get_args(annotation): for sub_annotation in get_args(annotation):

9
fastapi/encoders.py

@ -25,7 +25,12 @@ from pydantic.color import Color
from pydantic.networks import AnyUrl, NameEmail from pydantic.networks import AnyUrl, NameEmail
from pydantic.types import SecretBytes, SecretStr from pydantic.types import SecretBytes, SecretStr
from ._compat import Url, _is_undefined, _model_dump, may_v1 from ._compat import (
Url,
_is_undefined,
_model_dump,
is_pydantic_v1_model_instance,
)
# Taken from Pydantic v1 as is # Taken from Pydantic v1 as is
@ -218,7 +223,7 @@ def jsonable_encoder(
include = set(include) include = set(include)
if exclude is not None and not isinstance(exclude, (set, dict)): if exclude is not None and not isinstance(exclude, (set, dict)):
exclude = set(exclude) exclude = set(exclude)
if isinstance(obj, may_v1.BaseModel): if is_pydantic_v1_model_instance(obj):
raise PydanticV1NotSupportedError( raise PydanticV1NotSupportedError(
"pydantic.v1 models are no longer supported by FastAPI." "pydantic.v1 models are no longer supported by FastAPI."
f" Please update the model {obj!r}." f" Please update the model {obj!r}."

8
tests/test_jsonable_encoder.py

@ -1,3 +1,4 @@
import warnings
from collections import deque from collections import deque
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timezone from datetime import datetime, timezone
@ -13,8 +14,6 @@ from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import PydanticV1NotSupportedError from fastapi.exceptions import PydanticV1NotSupportedError
from pydantic import BaseModel, Field, ValidationError from pydantic import BaseModel, Field, ValidationError
from .utils import needs_pydanticv1
class Person: class Person:
def __init__(self, name: str): def __init__(self, name: str):
@ -157,9 +156,10 @@ def test_encode_custom_json_encoders_model_pydanticv2():
assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
@needs_pydanticv1
def test_json_encoder_error_with_pydanticv1(): def test_json_encoder_error_with_pydanticv1():
from pydantic import v1 with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
from pydantic import v1
class ModelV1(v1.BaseModel): class ModelV1(v1.BaseModel):
name: str name: str

6
tests/test_pydantic_v1_error.py

@ -1,4 +1,5 @@
import sys import sys
import warnings
import pytest import pytest
@ -8,9 +9,12 @@ if sys.version_info >= (3, 14):
skip_module_if_py_gte_314() skip_module_if_py_gte_314()
from fastapi import FastAPI from fastapi import FastAPI
from fastapi._compat.may_v1 import BaseModel
from fastapi.exceptions import PydanticV1NotSupportedError from fastapi.exceptions import PydanticV1NotSupportedError
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
from pydantic.v1 import BaseModel
def test_raises_pydantic_v1_model_in_endpoint_param() -> None: def test_raises_pydantic_v1_model_in_endpoint_param() -> None:
class ParamModelV1(BaseModel): class ParamModelV1(BaseModel):

Loading…
Cancel
Save