Merge branch 'master' into feat/query-method

This commit is contained in:
Sofie Van Landeghem 2025-10-20 16:16:53 +02:00 committed by GitHub
commit 0f7863627d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 432 additions and 103 deletions

View File

@ -0,0 +1,133 @@
# Von Pydantic v1 zu Pydantic v2 migrieren { #migrate-from-pydantic-v1-to-pydantic-v2 }
Wenn Sie eine ältere FastAPI-App haben, nutzen Sie möglicherweise Pydantic Version 1.
FastAPI unterstützt seit Version 0.100.0 sowohl Pydantic v1 als auch v2.
Wenn Sie Pydantic v2 installiert hatten, wurde dieses verwendet. Wenn stattdessen Pydantic v1 installiert war, wurde jenes verwendet.
Pydantic v1 ist jetzt deprecatet und die Unterstützung dafür wird in den nächsten Versionen von FastAPI entfernt, Sie sollten also zu **Pydantic v2 migrieren**. Auf diese Weise erhalten Sie die neuesten Features, Verbesserungen und Fixes.
/// warning | Achtung
Außerdem hat das Pydantic-Team die Unterstützung für Pydantic v1 in den neuesten Python-Versionen eingestellt, beginnend mit **Python 3.14**.
Wenn Sie die neuesten Features von Python nutzen möchten, müssen Sie sicherstellen, dass Sie Pydantic v2 verwenden.
///
Wenn Sie eine ältere FastAPI-App mit Pydantic v1 haben, zeige ich Ihnen hier, wie Sie sie zu Pydantic v2 migrieren, und die **neuen Features in FastAPI 0.119.0**, die Ihnen bei einer schrittweisen Migration helfen.
## Offizieller Leitfaden { #official-guide }
Pydantic hat einen offiziellen <a href="https://docs.pydantic.dev/latest/migration/" class="external-link" target="_blank">Migrationsleitfaden</a> von v1 zu v2.
Er enthält auch, was sich geändert hat, wie Validierungen nun korrekter und strikter sind, mögliche Stolpersteine, usw.
Sie können ihn lesen, um besser zu verstehen, was sich geändert hat.
## Tests { #tests }
Stellen Sie sicher, dass Sie [Tests](../tutorial/testing.md){.internal-link target=_blank} für Ihre App haben und diese in Continuous Integration (CI) ausführen.
Auf diese Weise können Sie das Update durchführen und sicherstellen, dass weiterhin alles wie erwartet funktioniert.
## `bump-pydantic` { #bump-pydantic }
In vielen Fällen, wenn Sie reguläre Pydantic-Modelle ohne Anpassungen verwenden, können Sie den Großteil des Prozesses der Migration von Pydantic v1 auf Pydantic v2 automatisieren.
Sie können <a href="https://github.com/pydantic/bump-pydantic" class="external-link" target="_blank">`bump-pydantic`</a> vom selben Pydantic-Team verwenden.
Dieses Tool hilft Ihnen, den Großteil des zu ändernden Codes automatisch anzupassen.
Danach können Sie die Tests ausführen und prüfen, ob alles funktioniert. Falls ja, sind Sie fertig. 😎
## Pydantic v1 in v2 { #pydantic-v1-in-v2 }
Pydantic v2 enthält alles aus Pydantic v1 als Untermodul `pydantic.v1`.
Das bedeutet, Sie können die neueste Version von Pydantic v2 installieren und die alten Pydanticv1Komponenten aus diesem Untermodul importieren und verwenden, als hätten Sie das alte Pydantic v1 installiert.
{* ../../docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py hl[1,4] *}
### FastAPI-Unterstützung für Pydantic v1 in v2 { #fastapi-support-for-pydantic-v1-in-v2 }
Seit FastAPI 0.119.0 gibt es außerdem eine teilweise Unterstützung für Pydantic v1 innerhalb von Pydantic v2, um die Migration auf v2 zu erleichtern.
Sie könnten also Pydantic auf die neueste Version 2 aktualisieren und die Importe so ändern, dass das Untermodul `pydantic.v1` verwendet wird, und in vielen Fällen würde es einfach funktionieren.
{* ../../docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py hl[2,5,15] *}
/// warning | Achtung
Beachten Sie, dass, da das PydanticTeam Pydantic v1 in neueren PythonVersionen nicht mehr unterstützt, beginnend mit Python 3.14, auch die Verwendung von `pydantic.v1` unter Python 3.14 und höher nicht unterstützt wird.
///
### Pydantic v1 und v2 in derselben App { #pydantic-v1-and-v2-on-the-same-app }
Es wird von Pydantic **nicht unterstützt**, dass ein Pydanticv2Modell Felder hat, die als Pydanticv1Modelle definiert sind, und umgekehrt.
```mermaid
graph TB
subgraph "❌ Nicht unterstützt"
direction TB
subgraph V2["Pydantic-v2-Modell"]
V1Field["Pydantic-v1-Modell"]
end
subgraph V1["Pydantic-v1-Modell"]
V2Field["Pydantic-v2-Modell"]
end
end
style V2 fill:#f9fff3
style V1 fill:#fff6f0
style V1Field fill:#fff6f0
style V2Field fill:#f9fff3
```
... aber Sie können getrennte Modelle, die Pydantic v1 bzw. v2 nutzen, in derselben App verwenden.
```mermaid
graph TB
subgraph "✅ Unterstützt"
direction TB
subgraph V2["Pydantic-v2-Modell"]
V2Field["Pydantic-v2-Modell"]
end
subgraph V1["Pydantic-v1-Modell"]
V1Field["Pydantic-v1-Modell"]
end
end
style V2 fill:#f9fff3
style V1 fill:#fff6f0
style V1Field fill:#fff6f0
style V2Field fill:#f9fff3
```
In einigen Fällen ist es sogar möglich, sowohl Pydanticv1 als auch Pydanticv2Modelle in derselben **Pfadoperation** Ihrer FastAPIApp zu verwenden:
{* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *}
Im obigen Beispiel ist das Eingabemodell ein Pydanticv1Modell, und das Ausgabemodell (definiert in `response_model=ItemV2`) ist ein Pydanticv2Modell.
### Pydantic v1 Parameter { #pydantic-v1-parameters }
Wenn Sie einige der FastAPI-spezifischen Tools für Parameter wie `Body`, `Query`, `Form`, usw. zusammen mit Pydanticv1Modellen verwenden müssen, können Sie die aus `fastapi.temp_pydantic_v1_params` importieren, während Sie die Migration zu Pydantic v2 abschließen:
{* ../../docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py hl[4,18] *}
### In Schritten migrieren { #migrate-in-steps }
/// tip | Tipp
Probieren Sie zuerst `bump-pydantic` aus. Wenn Ihre Tests erfolgreich sind und das funktioniert, sind Sie mit einem einzigen Befehl fertig. ✨
///
Wenn `bump-pydantic` für Ihren Anwendungsfall nicht funktioniert, können Sie die Unterstützung für Pydanticv1 und Pydanticv2Modelle in derselben App nutzen, um die Migration zu Pydantic v2 schrittweise durchzuführen.
Sie könnten zuerst Pydantic auf die neueste Version 2 aktualisieren und die Importe so ändern, dass für all Ihre Modelle `pydantic.v1` verwendet wird.
Anschließend können Sie beginnen, Ihre Modelle gruppenweise von Pydantic v1 auf v2 zu migrieren in kleinen, schrittweisen Etappen. 🚶

View File

@ -58,3 +58,6 @@ bronze:
- url: https://lambdatest.com/?utm_source=fastapi&utm_medium=partner&utm_campaign=sponsor&utm_term=opensource&utm_content=webpage
title: LambdaTest, AI-Powered Cloud-based Test Orchestration Platform
img: https://fastapi.tiangolo.com/img/sponsors/lambdatest.png
- url: https://requestly.com/fastapi
title: All-in-one platform to Test, Mock and Intercept APIs. Built for speed, privacy and offline support.
img: https://fastapi.tiangolo.com/img/sponsors/requestly.png

View File

@ -46,3 +46,4 @@ logins:
- madisonredtfeldt
- railwayapp
- subtotal
- requestly

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -7,12 +7,23 @@ hide:
## Latest Changes
### Translations
* 🌐 Sync German docs. PR [#14188](https://github.com/fastapi/fastapi/pull/14188) by [@nilslindemann](https://github.com/nilslindemann).
## 0.119.1
### Fixes
* 🐛 Fix internal Pydantic v1 compatibility (warnings) for Python 3.14 and Pydantic 2.12.1. PR [#14186](https://github.com/fastapi/fastapi/pull/14186) by [@svlandeg](https://github.com/svlandeg).
### Docs
* 📝 Replace `starlette.io` by `starlette.dev` and `uvicorn.org` by `uvicorn.dev`. PR [#14176](https://github.com/fastapi/fastapi/pull/14176) by [@Kludex](https://github.com/Kludex).
### Internal
* 🔧 Add sponsor Requestly. PR [#14205](https://github.com/fastapi/fastapi/pull/14205) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Configure reminder for `waiting` label in `issue-manager`. PR [#14156](https://github.com/fastapi/fastapi/pull/14156) by [@YuriiMotov](https://github.com/YuriiMotov).
## 0.119.0

View File

@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.119.0"
__version__ = "0.119.1"
from starlette import status as status

View File

@ -30,6 +30,10 @@ from .main import serialize_sequence_value as serialize_sequence_value
from .main import (
with_info_plain_validator_function as with_info_plain_validator_function,
)
from .may_v1 import CoreSchema as CoreSchema
from .may_v1 import GetJsonSchemaHandler as GetJsonSchemaHandler
from .may_v1 import JsonSchemaValue as JsonSchemaValue
from .may_v1 import _normalize_errors as _normalize_errors
from .model_field import ModelField as ModelField
from .shared import PYDANTIC_V2 as PYDANTIC_V2
from .shared import PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE
@ -44,7 +48,3 @@ from .shared import (
from .shared import lenient_issubclass as lenient_issubclass
from .shared import sequence_types as sequence_types
from .shared import value_is_sequence as value_is_sequence
from .v1 import CoreSchema as CoreSchema
from .v1 import GetJsonSchemaHandler as GetJsonSchemaHandler
from .v1 import JsonSchemaValue as JsonSchemaValue
from .v1 import _normalize_errors as _normalize_errors

View File

@ -1,3 +1,4 @@
import sys
from functools import lru_cache
from typing import (
Any,
@ -8,7 +9,7 @@ from typing import (
Type,
)
from fastapi._compat import v1
from fastapi._compat import may_v1
from fastapi._compat.shared import PYDANTIC_V2, lenient_issubclass
from fastapi.types import ModelNameMap
from pydantic import BaseModel
@ -50,7 +51,9 @@ else:
@lru_cache
def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]:
if lenient_issubclass(model, v1.BaseModel):
if lenient_issubclass(model, may_v1.BaseModel):
from fastapi._compat import v1
return v1.get_model_fields(model)
else:
from . import v2
@ -59,7 +62,7 @@ def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]:
def _is_undefined(value: object) -> bool:
if isinstance(value, v1.UndefinedType):
if isinstance(value, may_v1.UndefinedType):
return True
elif PYDANTIC_V2:
from . import v2
@ -69,7 +72,9 @@ def _is_undefined(value: object) -> bool:
def _get_model_config(model: BaseModel) -> Any:
if isinstance(model, v1.BaseModel):
if isinstance(model, may_v1.BaseModel):
from fastapi._compat import v1
return v1._get_model_config(model)
elif PYDANTIC_V2:
from . import v2
@ -80,7 +85,9 @@ def _get_model_config(model: BaseModel) -> Any:
def _model_dump(
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
) -> Any:
if isinstance(model, v1.BaseModel):
if isinstance(model, may_v1.BaseModel):
from fastapi._compat import v1
return v1._model_dump(model, mode=mode, **kwargs)
elif PYDANTIC_V2:
from . import v2
@ -89,7 +96,7 @@ def _model_dump(
def _is_error_wrapper(exc: Exception) -> bool:
if isinstance(exc, v1.ErrorWrapper):
if isinstance(exc, may_v1.ErrorWrapper):
return True
elif PYDANTIC_V2:
from . import v2
@ -99,7 +106,9 @@ def _is_error_wrapper(exc: Exception) -> bool:
def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
if isinstance(field_info, v1.FieldInfo):
if isinstance(field_info, may_v1.FieldInfo):
from fastapi._compat import v1
return v1.copy_field_info(field_info=field_info, annotation=annotation)
else:
assert PYDANTIC_V2
@ -111,7 +120,9 @@ def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
def create_body_model(
*, fields: Sequence[ModelField], model_name: str
) -> Type[BaseModel]:
if fields and isinstance(fields[0], v1.ModelField):
if fields and isinstance(fields[0], may_v1.ModelField):
from fastapi._compat import v1
return v1.create_body_model(fields=fields, model_name=model_name)
else:
assert PYDANTIC_V2
@ -123,7 +134,9 @@ def create_body_model(
def get_annotation_from_field_info(
annotation: Any, field_info: FieldInfo, field_name: str
) -> Any:
if isinstance(field_info, v1.FieldInfo):
if isinstance(field_info, may_v1.FieldInfo):
from fastapi._compat import v1
return v1.get_annotation_from_field_info(
annotation=annotation, field_info=field_info, field_name=field_name
)
@ -137,7 +150,9 @@ def get_annotation_from_field_info(
def is_bytes_field(field: ModelField) -> bool:
if isinstance(field, v1.ModelField):
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.is_bytes_field(field)
else:
assert PYDANTIC_V2
@ -147,7 +162,9 @@ def is_bytes_field(field: ModelField) -> bool:
def is_bytes_sequence_field(field: ModelField) -> bool:
if isinstance(field, v1.ModelField):
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.is_bytes_sequence_field(field)
else:
assert PYDANTIC_V2
@ -157,7 +174,9 @@ def is_bytes_sequence_field(field: ModelField) -> bool:
def is_scalar_field(field: ModelField) -> bool:
if isinstance(field, v1.ModelField):
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.is_scalar_field(field)
else:
assert PYDANTIC_V2
@ -167,7 +186,9 @@ def is_scalar_field(field: ModelField) -> bool:
def is_scalar_sequence_field(field: ModelField) -> bool:
if isinstance(field, v1.ModelField):
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.is_scalar_sequence_field(field)
else:
assert PYDANTIC_V2
@ -177,7 +198,9 @@ def is_scalar_sequence_field(field: ModelField) -> bool:
def is_sequence_field(field: ModelField) -> bool:
if isinstance(field, v1.ModelField):
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.is_sequence_field(field)
else:
assert PYDANTIC_V2
@ -187,7 +210,9 @@ def is_sequence_field(field: ModelField) -> bool:
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
if isinstance(field, v1.ModelField):
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.serialize_sequence_value(field=field, value=value)
else:
assert PYDANTIC_V2
@ -197,7 +222,9 @@ def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
def _model_rebuild(model: Type[BaseModel]) -> None:
if lenient_issubclass(model, v1.BaseModel):
if lenient_issubclass(model, may_v1.BaseModel):
from fastapi._compat import v1
v1._model_rebuild(model)
elif PYDANTIC_V2:
from . import v2
@ -206,9 +233,18 @@ def _model_rebuild(model: Type[BaseModel]) -> None:
def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
v1_model_fields = [field for field in fields if isinstance(field, v1.ModelField)]
v1_flat_models = v1.get_flat_models_from_fields(v1_model_fields, known_models=set()) # type: ignore[attr-defined]
all_flat_models = v1_flat_models
v1_model_fields = [
field for field in fields if isinstance(field, may_v1.ModelField)
]
if v1_model_fields:
from fastapi._compat import v1
v1_flat_models = v1.get_flat_models_from_fields(
v1_model_fields, known_models=set()
)
all_flat_models = v1_flat_models
else:
all_flat_models = set()
if PYDANTIC_V2:
from . import v2
@ -222,6 +258,8 @@ def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
model_name_map = v2.get_model_name_map(all_flat_models)
return model_name_map
from fastapi._compat import v1
model_name_map = v1.get_model_name_map(all_flat_models)
return model_name_map
@ -232,17 +270,35 @@ def get_definitions(
model_name_map: ModelNameMap,
separate_input_output_schemas: bool = True,
) -> Tuple[
Dict[Tuple[ModelField, Literal["validation", "serialization"]], v1.JsonSchemaValue],
Dict[
Tuple[ModelField, Literal["validation", "serialization"]],
may_v1.JsonSchemaValue,
],
Dict[str, Dict[str, Any]],
]:
v1_fields = [field for field in fields if isinstance(field, v1.ModelField)]
v1_field_maps, v1_definitions = v1.get_definitions(
fields=v1_fields,
model_name_map=model_name_map,
separate_input_output_schemas=separate_input_output_schemas,
)
if not PYDANTIC_V2:
return v1_field_maps, v1_definitions
if sys.version_info < (3, 14):
v1_fields = [field for field in fields if isinstance(field, may_v1.ModelField)]
v1_field_maps, v1_definitions = may_v1.get_definitions(
fields=v1_fields,
model_name_map=model_name_map,
separate_input_output_schemas=separate_input_output_schemas,
)
if not PYDANTIC_V2:
return v1_field_maps, v1_definitions
else:
from . import v2
v2_fields = [field for field in fields if isinstance(field, v2.ModelField)]
v2_field_maps, v2_definitions = v2.get_definitions(
fields=v2_fields,
model_name_map=model_name_map,
separate_input_output_schemas=separate_input_output_schemas,
)
all_definitions = {**v1_definitions, **v2_definitions}
all_field_maps = {**v1_field_maps, **v2_field_maps}
return all_field_maps, all_definitions
# Pydantic v1 is not supported since Python 3.14
else:
from . import v2
@ -252,9 +308,7 @@ def get_definitions(
model_name_map=model_name_map,
separate_input_output_schemas=separate_input_output_schemas,
)
all_definitions = {**v1_definitions, **v2_definitions}
all_field_maps = {**v1_field_maps, **v2_field_maps}
return all_field_maps, all_definitions
return v2_field_maps, v2_definitions
def get_schema_from_model_field(
@ -262,11 +316,14 @@ def get_schema_from_model_field(
field: ModelField,
model_name_map: ModelNameMap,
field_mapping: Dict[
Tuple[ModelField, Literal["validation", "serialization"]], v1.JsonSchemaValue
Tuple[ModelField, Literal["validation", "serialization"]],
may_v1.JsonSchemaValue,
],
separate_input_output_schemas: bool = True,
) -> Dict[str, Any]:
if isinstance(field, v1.ModelField):
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
return v1.get_schema_from_model_field(
field=field,
model_name_map=model_name_map,
@ -286,7 +343,7 @@ def get_schema_from_model_field(
def _is_model_field(value: Any) -> bool:
if isinstance(value, v1.ModelField):
if isinstance(value, may_v1.ModelField):
return True
elif PYDANTIC_V2:
from . import v2
@ -296,7 +353,7 @@ def _is_model_field(value: Any) -> bool:
def _is_model_class(value: Any) -> bool:
if lenient_issubclass(value, v1.BaseModel):
if lenient_issubclass(value, may_v1.BaseModel):
return True
elif PYDANTIC_V2:
from . import v2

123
fastapi/_compat/may_v1.py Normal file
View File

@ -0,0 +1,123 @@
import sys
from typing import Any, Dict, List, Literal, Sequence, Tuple, Type, Union
from fastapi.types import ModelNameMap
if sys.version_info >= (3, 14):
class AnyUrl:
pass
class BaseConfig:
pass
class BaseModel:
pass
class Color:
pass
class CoreSchema:
pass
class ErrorWrapper:
pass
class FieldInfo:
pass
class GetJsonSchemaHandler:
pass
class JsonSchemaValue:
pass
class ModelField:
pass
class NameEmail:
pass
class RequiredParam:
pass
class SecretBytes:
pass
class SecretStr:
pass
class Undefined:
pass
class UndefinedType:
pass
class Url:
pass
from .v2 import ValidationError, create_model
def get_definitions(
*,
fields: List[ModelField],
model_name_map: ModelNameMap,
separate_input_output_schemas: bool = True,
) -> Tuple[
Dict[
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
],
Dict[str, Dict[str, Any]],
]:
return {}, {} # pragma: no cover
else:
from .v1 import AnyUrl as AnyUrl
from .v1 import BaseConfig as BaseConfig
from .v1 import BaseModel as BaseModel
from .v1 import Color as Color
from .v1 import CoreSchema as CoreSchema
from .v1 import ErrorWrapper as ErrorWrapper
from .v1 import FieldInfo as FieldInfo
from .v1 import GetJsonSchemaHandler as GetJsonSchemaHandler
from .v1 import JsonSchemaValue as JsonSchemaValue
from .v1 import ModelField as ModelField
from .v1 import NameEmail as NameEmail
from .v1 import RequiredParam as RequiredParam
from .v1 import SecretBytes as SecretBytes
from .v1 import SecretStr as SecretStr
from .v1 import Undefined as Undefined
from .v1 import UndefinedType as UndefinedType
from .v1 import Url as Url
from .v1 import ValidationError, create_model
from .v1 import get_definitions as get_definitions
RequestErrorModel: Type[BaseModel] = create_model("Request")
def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
use_errors: List[Any] = []
for error in errors:
if isinstance(error, ErrorWrapper):
new_errors = ValidationError( # type: ignore[call-arg]
errors=[error], model=RequestErrorModel
).errors()
use_errors.extend(new_errors)
elif isinstance(error, list):
use_errors.extend(_normalize_errors(error))
else:
use_errors.append(error)
return use_errors
def _regenerate_error_with_loc(
*, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...]
) -> List[Dict[str, Any]]:
updated_loc_errors: List[Any] = [
{**err, "loc": loc_prefix + err.get("loc", ())}
for err in _normalize_errors(errors)
]
return updated_loc_errors

View File

@ -16,7 +16,7 @@ from typing import (
Union,
)
from fastapi._compat import v1
from fastapi._compat import may_v1
from fastapi.types import UnionType
from pydantic import BaseModel
from pydantic.version import VERSION as PYDANTIC_VERSION
@ -98,7 +98,9 @@ def value_is_sequence(value: Any) -> bool:
def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
return (
lenient_issubclass(annotation, (BaseModel, v1.BaseModel, Mapping, UploadFile))
lenient_issubclass(
annotation, (BaseModel, may_v1.BaseModel, Mapping, UploadFile)
)
or _annotation_is_sequence(annotation)
or is_dataclass(annotation)
)
@ -195,12 +197,12 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool:
def annotation_is_pydantic_v1(annotation: Any) -> bool:
if lenient_issubclass(annotation, v1.BaseModel):
if lenient_issubclass(annotation, may_v1.BaseModel):
return True
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
for arg in get_args(annotation):
if lenient_issubclass(arg, v1.BaseModel):
if lenient_issubclass(arg, may_v1.BaseModel):
return True
if field_annotation_is_sequence(annotation):
for sub_annotation in get_args(annotation):

View File

@ -54,13 +54,15 @@ if not PYDANTIC_V2:
from pydantic.schema import TypeModelSet as TypeModelSet
from pydantic.schema import (
field_schema,
get_flat_models_from_fields,
model_process_schema,
)
from pydantic.schema import (
get_annotation_from_field_info as get_annotation_from_field_info,
)
from pydantic.schema import get_flat_models_from_field as get_flat_models_from_field
from pydantic.schema import (
get_flat_models_from_fields as get_flat_models_from_fields,
)
from pydantic.schema import get_model_name_map as get_model_name_map
from pydantic.types import SecretBytes as SecretBytes
from pydantic.types import SecretStr as SecretStr
@ -99,7 +101,6 @@ else:
from pydantic.v1.schema import TypeModelSet as TypeModelSet
from pydantic.v1.schema import (
field_schema,
get_flat_models_from_fields,
model_process_schema,
)
from pydantic.v1.schema import (
@ -108,6 +109,9 @@ else:
from pydantic.v1.schema import (
get_flat_models_from_field as get_flat_models_from_field,
)
from pydantic.v1.schema import (
get_flat_models_from_fields as get_flat_models_from_fields,
)
from pydantic.v1.schema import get_model_name_map as get_model_name_map
from pydantic.v1.types import ( # type: ignore[assignment]
SecretBytes as SecretBytes,
@ -215,32 +219,6 @@ def is_pv1_scalar_sequence_field(field: ModelField) -> bool:
return False
def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
use_errors: List[Any] = []
for error in errors:
if isinstance(error, ErrorWrapper):
new_errors = ValidationError( # type: ignore[call-arg]
errors=[error], model=RequestErrorModel
).errors()
use_errors.extend(new_errors)
elif isinstance(error, list):
use_errors.extend(_normalize_errors(error))
else:
use_errors.append(error)
return use_errors
def _regenerate_error_with_loc(
*, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...]
) -> List[Dict[str, Any]]:
updated_loc_errors: List[Any] = [
{**err, "loc": loc_prefix + err.get("loc", ())}
for err in _normalize_errors(errors)
]
return updated_loc_errors
def _model_rebuild(model: Type[BaseModel]) -> None:
model.update_forward_refs()

View File

@ -15,7 +15,7 @@ from typing import (
cast,
)
from fastapi._compat import shared, v1
from fastapi._compat import may_v1, shared
from fastapi.openapi.constants import REF_TEMPLATE
from fastapi.types import IncEx, ModelNameMap
from pydantic import BaseModel, TypeAdapter, create_model
@ -116,7 +116,7 @@ class ModelField:
None,
)
except ValidationError as exc:
return None, v1._regenerate_error_with_loc(
return None, may_v1._regenerate_error_with_loc(
errors=exc.errors(include_url=False), loc_prefix=loc
)

View File

@ -43,9 +43,9 @@ from fastapi._compat import (
is_uploadfile_or_nonable_uploadfile_annotation,
is_uploadfile_sequence_annotation,
lenient_issubclass,
may_v1,
sequence_types,
serialize_sequence_value,
v1,
value_is_sequence,
)
from fastapi._compat.shared import annotation_is_pydantic_v1
@ -380,7 +380,7 @@ def analyze_param(
fastapi_annotations = [
arg
for arg in annotated_args[1:]
if isinstance(arg, (FieldInfo, v1.FieldInfo, params.Depends))
if isinstance(arg, (FieldInfo, may_v1.FieldInfo, params.Depends))
]
fastapi_specific_annotations = [
arg
@ -397,21 +397,21 @@ def analyze_param(
)
]
if fastapi_specific_annotations:
fastapi_annotation: Union[FieldInfo, v1.FieldInfo, params.Depends, None] = (
fastapi_specific_annotations[-1]
)
fastapi_annotation: Union[
FieldInfo, may_v1.FieldInfo, params.Depends, None
] = fastapi_specific_annotations[-1]
else:
fastapi_annotation = None
# Set default for Annotated FieldInfo
if isinstance(fastapi_annotation, (FieldInfo, v1.FieldInfo)):
if isinstance(fastapi_annotation, (FieldInfo, may_v1.FieldInfo)):
# Copy `field_info` because we mutate `field_info.default` below.
field_info = copy_field_info(
field_info=fastapi_annotation, annotation=use_annotation
)
assert field_info.default in {
Undefined,
v1.Undefined,
} or field_info.default in {RequiredParam, v1.RequiredParam}, (
may_v1.Undefined,
} or field_info.default in {RequiredParam, may_v1.RequiredParam}, (
f"`{field_info.__class__.__name__}` default value cannot be set in"
f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
)
@ -435,7 +435,7 @@ def analyze_param(
)
depends = value
# Get FieldInfo from default value
elif isinstance(value, (FieldInfo, v1.FieldInfo)):
elif isinstance(value, (FieldInfo, may_v1.FieldInfo)):
assert field_info is None, (
"Cannot specify FastAPI annotations in `Annotated` and default value"
f" together for {param_name!r}"
@ -524,7 +524,8 @@ def analyze_param(
type_=use_annotation_from_field_info,
default=field_info.default,
alias=alias,
required=field_info.default in (RequiredParam, v1.RequiredParam, Undefined),
required=field_info.default
in (RequiredParam, may_v1.RequiredParam, Undefined),
field_info=field_info,
)
if is_path_param:
@ -741,7 +742,7 @@ def _validate_value_with_model_field(
if _is_error_wrapper(errors_): # type: ignore[arg-type]
return None, [errors_]
elif isinstance(errors_, list):
new_errors = v1._regenerate_error_with_loc(errors=errors_, loc_prefix=())
new_errors = may_v1._regenerate_error_with_loc(errors=errors_, loc_prefix=())
return None, new_errors
else:
return v_, []

View File

@ -17,7 +17,7 @@ from types import GeneratorType
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
from uuid import UUID
from fastapi._compat import v1
from fastapi._compat import may_v1
from fastapi.types import IncEx
from pydantic import BaseModel
from pydantic.color import Color
@ -59,7 +59,7 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
bytes: lambda o: o.decode(),
Color: str,
v1.Color: str,
may_v1.Color: str,
datetime.date: isoformat,
datetime.datetime: isoformat,
datetime.time: isoformat,
@ -76,19 +76,19 @@ ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
IPv6Interface: str,
IPv6Network: str,
NameEmail: str,
v1.NameEmail: str,
may_v1.NameEmail: str,
Path: str,
Pattern: lambda o: o.pattern,
SecretBytes: str,
v1.SecretBytes: str,
may_v1.SecretBytes: str,
SecretStr: str,
v1.SecretStr: str,
may_v1.SecretStr: str,
set: list,
UUID: str,
Url: str,
v1.Url: str,
may_v1.Url: str,
AnyUrl: str,
v1.AnyUrl: str,
may_v1.AnyUrl: str,
}
@ -220,10 +220,10 @@ def jsonable_encoder(
include = set(include)
if exclude is not None and not isinstance(exclude, (set, dict)):
exclude = set(exclude)
if isinstance(obj, (BaseModel, v1.BaseModel)):
if isinstance(obj, (BaseModel, may_v1.BaseModel)):
# TODO: remove when deprecating Pydantic v1
encoders: Dict[Any, Any] = {}
if isinstance(obj, v1.BaseModel):
if isinstance(obj, may_v1.BaseModel):
encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined]
if custom_encoder:
encoders = {**encoders, **custom_encoder}

View File

@ -5,8 +5,8 @@ from fastapi.openapi.models import Example
from fastapi.params import ParamTypes
from typing_extensions import Annotated, deprecated
from ._compat.may_v1 import FieldInfo, Undefined
from ._compat.shared import PYDANTIC_VERSION_MINOR_TUPLE
from ._compat.v1 import FieldInfo, Undefined
_Unset: Any = Undefined

View File

@ -25,7 +25,7 @@ from fastapi._compat import (
Validator,
annotation_is_pydantic_v1,
lenient_issubclass,
v1,
may_v1,
)
from fastapi.datastructures import DefaultPlaceholder, DefaultType
from pydantic import BaseModel
@ -87,8 +87,8 @@ def create_model_field(
) -> ModelField:
class_validators = class_validators or {}
v1_model_config = v1.BaseConfig
v1_field_info = field_info or v1.FieldInfo()
v1_model_config = may_v1.BaseConfig
v1_field_info = field_info or may_v1.FieldInfo()
v1_kwargs = {
"name": name,
"field_info": v1_field_info,
@ -102,9 +102,11 @@ def create_model_field(
if (
annotation_is_pydantic_v1(type_)
or isinstance(field_info, v1.FieldInfo)
or isinstance(field_info, may_v1.FieldInfo)
or version == "1"
):
from fastapi._compat import v1
try:
return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return]
except RuntimeError:
@ -122,6 +124,8 @@ def create_model_field(
raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None
# Pydantic v2 is not installed, but it's not a Pydantic v1 ModelField, it could be
# a Pydantic v1 type, like a constrained int
from fastapi._compat import v1
try:
return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return]
except RuntimeError:
@ -138,6 +142,9 @@ def create_cloned_field(
if isinstance(field, v2.ModelField):
return field
from fastapi._compat import v1
# cloned_types caches already cloned types to support recursive models and improve
# performance by avoiding unnecessary cloning
if cloned_types is None:

View File

@ -7,7 +7,7 @@ from fastapi._compat import (
get_cached_model_fields,
is_scalar_field,
is_uploadfile_sequence_annotation,
v1,
may_v1,
)
from fastapi._compat.shared import is_bytes_sequence_annotation
from fastapi.testclient import TestClient
@ -27,7 +27,10 @@ def test_model_field_default_required():
assert field.default is Undefined
@needs_py_lt_314
def test_v1_plain_validator_function():
from fastapi._compat import v1
# For coverage
def func(v): # pragma: no cover
return v
@ -135,6 +138,8 @@ def test_is_uploadfile_sequence_annotation():
@needs_py_lt_314
def test_is_pv1_scalar_field():
from fastapi._compat import v1
# For coverage
class Model(v1.BaseModel):
foo: Union[str, Dict[str, Any]]
@ -143,8 +148,11 @@ def test_is_pv1_scalar_field():
assert not is_scalar_field(fields[0])
@needs_py_lt_314
def test_get_model_fields_cached():
class Model(v1.BaseModel):
from fastapi._compat import v1
class Model(may_v1.BaseModel):
foo: str
non_cached_fields = v1.get_model_fields(Model)

View File

@ -5,7 +5,6 @@ import fastapi.openapi.utils
import pydantic.schema
import pytest
from fastapi import FastAPI
from fastapi._compat import v1
from pydantic import BaseModel
from starlette.testclient import TestClient
@ -165,6 +164,8 @@ def test_model_description_escaped_with_formfeed(sort_reversed: bool):
Test `get_model_definitions` with models passed in different order.
"""
from fastapi._compat import v1
all_fields = fastapi.openapi.utils.get_fields_from_routes(app.routes)
flat_models = v1.get_flat_models_from_fields(all_fields, known_models=set())

View File

@ -2,12 +2,13 @@ from typing import List, Union
import pytest
from fastapi import FastAPI
from fastapi._compat import v1
from fastapi.exceptions import FastAPIError, ResponseValidationError
from fastapi.responses import JSONResponse, Response
from fastapi.testclient import TestClient
from pydantic import BaseModel
from tests.utils import needs_pydanticv1
class BaseUser(BaseModel):
name: str
@ -511,7 +512,10 @@ def test_invalid_response_model_field():
# TODO: remove when dropping Pydantic v1 support
@needs_pydanticv1
def test_invalid_response_model_field_pv1():
from fastapi._compat import v1
app = FastAPI()
class Model(v1.BaseModel):