diff --git a/README.md b/README.md index 4fd87298e..09cd38da1 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ The key features are: + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 7a015e404..943b92adb 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -26,6 +26,9 @@ gold: - url: https://docs.railway.com/guides/fastapi?utm_medium=integration&utm_source=docs&utm_campaign=fastapi title: Deploy enterprise applications at startup speed img: https://fastapi.tiangolo.com/img/sponsors/railway.png + - url: https://serpapi.com/?utm_source=fastapi_website + title: "SerpApi: Web Search API" + img: https://fastapi.tiangolo.com/img/sponsors/serpapi.png silver: - url: https://databento.com/?utm_source=fastapi&utm_medium=sponsor&utm_content=display title: Pay as you go for market data diff --git a/docs/en/docs/img/sponsors/serpapi-banner.png b/docs/en/docs/img/sponsors/serpapi-banner.png new file mode 100644 index 000000000..3c3fd629e Binary files /dev/null and b/docs/en/docs/img/sponsors/serpapi-banner.png differ diff --git a/docs/en/docs/img/sponsors/serpapi.png b/docs/en/docs/img/sponsors/serpapi.png new file mode 100644 index 000000000..d7258ef70 Binary files /dev/null and b/docs/en/docs/img/sponsors/serpapi.png differ diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index fb4c26701..bbcbc74b7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,8 +7,20 @@ hide: ## Latest Changes +### Refactors + +* ♻️ Refactor internals of dependencies, simplify code and remove `get_param_sub_dependant`. PR [#14255](https://github.com/fastapi/fastapi/pull/14255) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Refactor internals of dependencies, simplify using dataclasses. PR [#14254](https://github.com/fastapi/fastapi/pull/14254) by [@tiangolo](https://github.com/tiangolo). + +## 0.120.2 + +### Fixes + +* 🐛 Fix separation of schemas with nested models introduced in 0.119.0. PR [#14246](https://github.com/fastapi/fastapi/pull/14246) by [@tiangolo](https://github.com/tiangolo). + ### Internal +* 🔧 Add sponsor: SerpApi. PR [#14248](https://github.com/fastapi/fastapi/pull/14248) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/download-artifact from 5 to 6. PR [#14236](https://github.com/fastapi/fastapi/pull/14236) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#14237](https://github.com/fastapi/fastapi/pull/14237) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump actions/upload-artifact from 4 to 5. PR [#14235](https://github.com/fastapi/fastapi/pull/14235) by [@dependabot[bot]](https://github.com/apps/dependabot). diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index c7ffaef5d..be31bd75c 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -80,6 +80,12 @@ +
+ + + + +
{% endblock %} diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 787c52dda..a4c17a6bd 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.120.1" +__version__ = "0.120.2" from starlette import status as status diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index fb2c691d8..6a87b9ae9 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -207,11 +207,31 @@ def get_definitions( override_mode: Union[Literal["validation"], None] = ( None if separate_input_output_schemas else "validation" ) - flat_models = get_flat_models_from_fields(fields, known_models=set()) - flat_model_fields = [ - ModelField(field_info=FieldInfo(annotation=model), name=model.__name__) - for model in flat_models + validation_fields = [field for field in fields if field.mode == "validation"] + serialization_fields = [field for field in fields if field.mode == "serialization"] + flat_validation_models = get_flat_models_from_fields( + validation_fields, known_models=set() + ) + flat_serialization_models = get_flat_models_from_fields( + serialization_fields, known_models=set() + ) + flat_validation_model_fields = [ + ModelField( + field_info=FieldInfo(annotation=model), + name=model.__name__, + mode="validation", + ) + for model in flat_validation_models + ] + flat_serialization_model_fields = [ + ModelField( + field_info=FieldInfo(annotation=model), + name=model.__name__, + mode="serialization", + ) + for model in flat_serialization_models ] + flat_model_fields = flat_validation_model_fields + flat_serialization_model_fields input_types = {f.type_ for f in fields} unique_flat_model_fields = { f for f in flat_model_fields if f.type_ not in input_types diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index aa06dd2a9..18f6a234e 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -125,23 +125,6 @@ def ensure_multipart_is_installed() -> None: raise RuntimeError(multipart_not_installed_error) from None -def get_param_sub_dependant( - *, - param_name: str, - depends: params.Depends, - path: str, - security_scopes: Optional[List[str]] = None, -) -> Dependant: - assert depends.dependency - return get_sub_dependant( - depends=depends, - dependency=depends.dependency, - path=path, - name=param_name, - security_scopes=security_scopes, - ) - - def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> Dependant: assert callable(depends.dependency), ( "A parameter-less dependency must have a callable dependency" @@ -160,8 +143,8 @@ def get_sub_dependant( security_requirement = None security_scopes = security_scopes or [] if isinstance(depends, params.Security): - dependency_scopes = depends.scopes - security_scopes.extend(dependency_scopes) + if depends.scopes: + security_scopes.extend(depends.scopes) if isinstance(dependency, SecurityBase): use_scopes: List[str] = [] if isinstance(dependency, (OAuth2, OpenIdConnect)): @@ -282,9 +265,6 @@ def get_dependant( security_scopes: Optional[List[str]] = None, use_cache: bool = True, ) -> Dependant: - path_param_names = get_path_param_names(path) - endpoint_signature = get_typed_signature(call) - signature_params = endpoint_signature.parameters dependant = Dependant( call=call, name=name, @@ -292,6 +272,9 @@ def get_dependant( security_scopes=security_scopes, use_cache=use_cache, ) + path_param_names = get_path_param_names(path) + endpoint_signature = get_typed_signature(call) + signature_params = endpoint_signature.parameters for param_name, param in signature_params.items(): is_path_param = param_name in path_param_names param_details = analyze_param( @@ -301,10 +284,12 @@ def get_dependant( is_path_param=is_path_param, ) if param_details.depends is not None: - sub_dependant = get_param_sub_dependant( - param_name=param_name, + assert param_details.depends.dependency + sub_dependant = get_sub_dependant( depends=param_details.depends, + dependency=param_details.depends.dependency, path=path, + name=param_name, security_scopes=security_scopes, ) dependant.dependencies.append(sub_dependant) diff --git a/fastapi/params.py b/fastapi/params.py index e85375018..2dc04be14 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -1,4 +1,5 @@ import warnings +from dataclasses import dataclass from enum import Enum from typing import Any, Callable, Dict, List, Optional, Sequence, Union @@ -761,26 +762,12 @@ class File(Form): # type: ignore[misc] ) +@dataclass class Depends: - def __init__( - self, dependency: Optional[Callable[..., Any]] = None, *, use_cache: bool = True - ): - self.dependency = dependency - self.use_cache = use_cache - - def __repr__(self) -> str: - attr = getattr(self.dependency, "__name__", type(self.dependency).__name__) - cache = "" if self.use_cache else ", use_cache=False" - return f"{self.__class__.__name__}({attr}{cache})" + dependency: Optional[Callable[..., Any]] = None + use_cache: bool = True +@dataclass class Security(Depends): - def __init__( - self, - dependency: Optional[Callable[..., Any]] = None, - *, - scopes: Optional[Sequence[str]] = None, - use_cache: bool = True, - ): - super().__init__(dependency=dependency, use_cache=use_cache) - self.scopes = scopes or [] + scopes: Optional[Sequence[str]] = None diff --git a/tests/test_no_schema_split.py b/tests/test_no_schema_split.py new file mode 100644 index 000000000..b0b5958c1 --- /dev/null +++ b/tests/test_no_schema_split.py @@ -0,0 +1,203 @@ +# Test with parts from, and to verify the report in: +# https://github.com/fastapi/fastapi/discussions/14177 +# Made an issue in: +# https://github.com/fastapi/fastapi/issues/14247 +from enum import Enum +from typing import List + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from pydantic import BaseModel, Field + +from tests.utils import pydantic_snapshot + + +class MessageEventType(str, Enum): + alpha = "alpha" + beta = "beta" + + +class MessageEvent(BaseModel): + event_type: MessageEventType = Field(default=MessageEventType.alpha) + output: str + + +class MessageOutput(BaseModel): + body: str = "" + events: List[MessageEvent] = [] + + +class Message(BaseModel): + input: str + output: MessageOutput + + +app = FastAPI(title="Minimal FastAPI App", version="1.0.0") + + +@app.post("/messages", response_model=Message) +async def create_message(input_message: str) -> Message: + return Message( + input=input_message, + output=MessageOutput(body=f"Processed: {input_message}"), + ) + + +client = TestClient(app) + + +def test_create_message(): + response = client.post("/messages", params={"input_message": "Hello"}) + assert response.status_code == 200, response.text + assert response.json() == { + "input": "Hello", + "output": {"body": "Processed: Hello", "events": []}, + } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "Minimal FastAPI App", "version": "1.0.0"}, + "paths": { + "/messages": { + "post": { + "summary": "Create Message", + "operationId": "create_message_messages_post", + "parameters": [ + { + "name": "input_message", + "in": "query", + "required": True, + "schema": {"type": "string", "title": "Input Message"}, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Message": { + "properties": { + "input": {"type": "string", "title": "Input"}, + "output": {"$ref": "#/components/schemas/MessageOutput"}, + }, + "type": "object", + "required": ["input", "output"], + "title": "Message", + }, + "MessageEvent": { + "properties": { + "event_type": pydantic_snapshot( + v2=snapshot( + { + "$ref": "#/components/schemas/MessageEventType", + "default": "alpha", + } + ), + v1=snapshot( + { + "allOf": [ + { + "$ref": "#/components/schemas/MessageEventType" + } + ], + "default": "alpha", + } + ), + ), + "output": {"type": "string", "title": "Output"}, + }, + "type": "object", + "required": ["output"], + "title": "MessageEvent", + }, + "MessageEventType": pydantic_snapshot( + v2=snapshot( + { + "type": "string", + "enum": ["alpha", "beta"], + "title": "MessageEventType", + } + ), + v1=snapshot( + { + "type": "string", + "enum": ["alpha", "beta"], + "title": "MessageEventType", + "description": "An enumeration.", + } + ), + ), + "MessageOutput": { + "properties": { + "body": {"type": "string", "title": "Body", "default": ""}, + "events": { + "items": {"$ref": "#/components/schemas/MessageEvent"}, + "type": "array", + "title": "Events", + "default": [], + }, + }, + "type": "object", + "title": "MessageOutput", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_params_repr.py b/tests/test_params_repr.py index bfc7bed09..baa172497 100644 --- a/tests/test_params_repr.py +++ b/tests/test_params_repr.py @@ -1,7 +1,7 @@ from typing import Any, List from dirty_equals import IsOneOf -from fastapi.params import Body, Cookie, Depends, Header, Param, Path, Query +from fastapi.params import Body, Cookie, Header, Param, Path, Query test_data: List[Any] = ["teststr", None, ..., 1, []] @@ -141,12 +141,3 @@ def test_body_repr_number(): def test_body_repr_list(): assert repr(Body([])) == "Body([])" - - -def test_depends_repr(): - assert repr(Depends()) == "Depends(NoneType)" - assert repr(Depends(get_user)) == "Depends(get_user)" - assert repr(Depends(use_cache=False)) == "Depends(NoneType, use_cache=False)" - assert ( - repr(Depends(get_user, use_cache=False)) == "Depends(get_user, use_cache=False)" - ) diff --git a/tests/test_pydantic_v1_v2_multifile/test_multifile.py b/tests/test_pydantic_v1_v2_multifile/test_multifile.py index 4472bd73e..e66d102fb 100644 --- a/tests/test_pydantic_v1_v2_multifile/test_multifile.py +++ b/tests/test_pydantic_v1_v2_multifile/test_multifile.py @@ -1028,17 +1028,6 @@ def test_openapi_schema(): "type": "object", "title": "HTTPValidationError", }, - "SubItem-Output": { - "properties": { - "new_sub_name": { - "type": "string", - "title": "New Sub Name", - } - }, - "type": "object", - "required": ["new_sub_name"], - "title": "SubItem", - }, "ValidationError": { "properties": { "loc": { @@ -1113,11 +1102,11 @@ def test_openapi_schema(): "title": "New Description", }, "new_sub": { - "$ref": "#/components/schemas/SubItem-Output" + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" }, "new_multi": { "items": { - "$ref": "#/components/schemas/SubItem-Output" + "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem" }, "type": "array", "title": "New Multi",