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

⬆️ Upgrade compatibility with Pydantic v2.4, new renamed functions and JSON Schema input/output models with default values #10344

Merged
merged 7 commits into from Sep 28, 2023
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Expand Up @@ -29,7 +29,7 @@ jobs:
id: cache
with:
path: ${{ env.pythonLocation }}
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v05
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v06
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: pip install -r requirements-tests.txt
Expand Down Expand Up @@ -62,7 +62,7 @@ jobs:
id: cache
with:
path: ${{ env.pythonLocation }}
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v05
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v06
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: pip install -r requirements-tests.txt
Expand Down
14 changes: 10 additions & 4 deletions fastapi/_compat.py
Expand Up @@ -58,9 +58,15 @@
from pydantic_core import CoreSchema as CoreSchema
from pydantic_core import PydanticUndefined, PydanticUndefinedType
from pydantic_core import Url as Url
from pydantic_core.core_schema import (
general_plain_validator_function as general_plain_validator_function,
)

try:
from pydantic_core.core_schema import (
with_info_plain_validator_function as with_info_plain_validator_function,
)
except ImportError: # pragma: no cover
from pydantic_core.core_schema import (
general_plain_validator_function as with_info_plain_validator_function, # noqa: F401
)

Required = PydanticUndefined
Undefined = PydanticUndefined
Expand Down Expand Up @@ -345,7 +351,7 @@ class GenerateJsonSchema: # type: ignore[no-redef]
class PydanticSchemaGenerationError(Exception): # type: ignore[no-redef]
pass

def general_plain_validator_function( # type: ignore[misc]
def with_info_plain_validator_function( # type: ignore[misc]
function: Callable[..., Any],
*,
ref: Union[str, None] = None,
Expand Down
4 changes: 2 additions & 2 deletions fastapi/datastructures.py
Expand Up @@ -5,7 +5,7 @@
CoreSchema,
GetJsonSchemaHandler,
JsonSchemaValue,
general_plain_validator_function,
with_info_plain_validator_function,
)
from starlette.datastructures import URL as URL # noqa: F401
from starlette.datastructures import Address as Address # noqa: F401
Expand Down Expand Up @@ -49,7 +49,7 @@ def __get_pydantic_json_schema__(
def __get_pydantic_core_schema__(
cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
) -> CoreSchema:
return general_plain_validator_function(cls._validate)
return with_info_plain_validator_function(cls._validate)


class DefaultPlaceholder:
Expand Down
4 changes: 2 additions & 2 deletions fastapi/openapi/models.py
Expand Up @@ -7,7 +7,7 @@
GetJsonSchemaHandler,
JsonSchemaValue,
_model_rebuild,
general_plain_validator_function,
with_info_plain_validator_function,
)
from fastapi.logger import logger
from pydantic import AnyUrl, BaseModel, Field
Expand Down Expand Up @@ -52,7 +52,7 @@ def __get_pydantic_json_schema__(
def __get_pydantic_core_schema__(
cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
) -> CoreSchema:
return general_plain_validator_function(cls._validate)
return with_info_plain_validator_function(cls._validate)


class Contact(BaseModel):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_compat.py
Expand Up @@ -24,7 +24,7 @@ def test_model_field_default_required():


@needs_pydanticv1
def test_upload_file_dummy_general_plain_validator_function():
def test_upload_file_dummy_with_info_plain_validator_function():
# For coverage
assert UploadFile.__get_pydantic_core_schema__(str, lambda x: None) == {}

Expand Down
4 changes: 2 additions & 2 deletions tests/test_filter_pydantic_sub_model_pv2.py
Expand Up @@ -12,7 +12,7 @@

@pytest.fixture(name="client")
def get_client():
from pydantic import BaseModel, FieldValidationInfo, field_validator
from pydantic import BaseModel, ValidationInfo, field_validator

app = FastAPI()

Expand All @@ -28,7 +28,7 @@ class ModelA(BaseModel):
foo: ModelB

@field_validator("name")
def lower_username(cls, name: str, info: FieldValidationInfo):
def lower_username(cls, name: str, info: ValidationInfo):
if not name.endswith("A"):
raise ValueError("name must end in A")
return name
Expand Down
6 changes: 5 additions & 1 deletion tests/test_openapi_separate_input_output_schemas.py
Expand Up @@ -4,19 +4,23 @@
from fastapi.testclient import TestClient
from pydantic import BaseModel

from .utils import needs_pydanticv2
from .utils import PYDANTIC_V2, needs_pydanticv2


class SubItem(BaseModel):
subname: str
sub_description: Optional[str] = None
tags: List[str] = []
if PYDANTIC_V2:
model_config = {"json_schema_serialization_defaults_required": True}


class Item(BaseModel):
name: str
description: Optional[str] = None
sub: Optional[SubItem] = None
if PYDANTIC_V2:
model_config = {"json_schema_serialization_defaults_required": True}


def get_app_client(separate_input_output_schemas: bool = True) -> TestClient:
Expand Down
38 changes: 4 additions & 34 deletions tests/test_tutorial/test_body_updates/test_tutorial001.py
Expand Up @@ -52,9 +52,7 @@ def test_openapi_schema(client: TestClient):
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Item-Output"
}
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
Expand Down Expand Up @@ -86,9 +84,7 @@ def test_openapi_schema(client: TestClient):
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Item-Output"
}
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
Expand Down Expand Up @@ -116,7 +112,7 @@ def test_openapi_schema(client: TestClient):
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item-Input"}
"schema": {"$ref": "#/components/schemas/Item"}
}
},
"required": True,
Expand All @@ -126,35 +122,9 @@ def test_openapi_schema(client: TestClient):
},
"components": {
"schemas": {
"Item-Input": {
"title": "Item",
"Item": {
"type": "object",
"properties": {
"name": {
"title": "Name",
"anyOf": [{"type": "string"}, {"type": "null"}],
},
"description": {
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
},
"price": {
"title": "Price",
"anyOf": [{"type": "number"}, {"type": "null"}],
},
"tax": {"title": "Tax", "type": "number", "default": 10.5},
"tags": {
"title": "Tags",
"type": "array",
"items": {"type": "string"},
"default": [],
},
},
},
"Item-Output": {
"title": "Item",
"type": "object",
"required": ["name", "description", "price", "tax", "tags"],
"properties": {
"name": {
"anyOf": [{"type": "string"}, {"type": "null"}],
Expand Down
38 changes: 4 additions & 34 deletions tests/test_tutorial/test_body_updates/test_tutorial001_py310.py
Expand Up @@ -55,9 +55,7 @@ def test_openapi_schema(client: TestClient):
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Item-Output"
}
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
Expand Down Expand Up @@ -89,9 +87,7 @@ def test_openapi_schema(client: TestClient):
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Item-Output"
}
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
Expand Down Expand Up @@ -119,7 +115,7 @@ def test_openapi_schema(client: TestClient):
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item-Input"}
"schema": {"$ref": "#/components/schemas/Item"}
}
},
"required": True,
Expand All @@ -129,35 +125,9 @@ def test_openapi_schema(client: TestClient):
},
"components": {
"schemas": {
"Item-Input": {
"title": "Item",
"Item": {
"type": "object",
"properties": {
"name": {
"title": "Name",
"anyOf": [{"type": "string"}, {"type": "null"}],
},
"description": {
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
},
"price": {
"title": "Price",
"anyOf": [{"type": "number"}, {"type": "null"}],
},
"tax": {"title": "Tax", "type": "number", "default": 10.5},
"tags": {
"title": "Tags",
"type": "array",
"items": {"type": "string"},
"default": [],
},
},
},
"Item-Output": {
"title": "Item",
"type": "object",
"required": ["name", "description", "price", "tax", "tags"],
"properties": {
"name": {
"anyOf": [{"type": "string"}, {"type": "null"}],
Expand Down
38 changes: 4 additions & 34 deletions tests/test_tutorial/test_body_updates/test_tutorial001_py39.py
Expand Up @@ -55,9 +55,7 @@ def test_openapi_schema(client: TestClient):
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Item-Output"
}
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
Expand Down Expand Up @@ -89,9 +87,7 @@ def test_openapi_schema(client: TestClient):
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Item-Output"
}
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
Expand Down Expand Up @@ -119,7 +115,7 @@ def test_openapi_schema(client: TestClient):
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item-Input"}
"schema": {"$ref": "#/components/schemas/Item"}
}
},
"required": True,
Expand All @@ -129,35 +125,9 @@ def test_openapi_schema(client: TestClient):
},
"components": {
"schemas": {
"Item-Input": {
"title": "Item",
"Item": {
"type": "object",
"properties": {
"name": {
"title": "Name",
"anyOf": [{"type": "string"}, {"type": "null"}],
},
"description": {
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
},
"price": {
"title": "Price",
"anyOf": [{"type": "number"}, {"type": "null"}],
},
"tax": {"title": "Tax", "type": "number", "default": 10.5},
"tags": {
"title": "Tags",
"type": "array",
"items": {"type": "string"},
"default": [],
},
},
},
"Item-Output": {
"title": "Item",
"type": "object",
"required": ["name", "description", "price", "tax", "tags"],
"properties": {
"name": {
"anyOf": [{"type": "string"}, {"type": "null"}],
Expand Down
22 changes: 4 additions & 18 deletions tests/test_tutorial/test_dataclasses/test_tutorial003.py
Expand Up @@ -79,9 +79,7 @@ def test_openapi_schema():
"schema": {
"title": "Items",
"type": "array",
"items": {
"$ref": "#/components/schemas/Item-Input"
},
"items": {"$ref": "#/components/schemas/Item"},
}
}
},
Expand Down Expand Up @@ -136,14 +134,14 @@ def test_openapi_schema():
"schemas": {
"Author": {
"title": "Author",
"required": ["name", "items"],
"required": ["name"],
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"items": {
"title": "Items",
"type": "array",
"items": {"$ref": "#/components/schemas/Item-Output"},
"items": {"$ref": "#/components/schemas/Item"},
},
},
},
Expand All @@ -158,27 +156,15 @@ def test_openapi_schema():
}
},
},
"Item-Input": {
"Item": {
"title": "Item",
"required": ["name"],
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"description": {
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
},
},
},
"Item-Output": {
"title": "Item",
"required": ["name", "description"],
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"description": {
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
},
},
},
Expand Down