Browse Source

Merge branch 'master' into 14297-followup

pull/14430/head
Sebastián Ramírez 8 months ago
committed by GitHub
parent
commit
1e0c86029d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 8
      docs/en/docs/advanced/behind-a-proxy.md
  2. 19
      docs/en/docs/release-notes.md
  3. 2
      fastapi/__init__.py
  4. 14
      fastapi/_compat/v2.py
  5. 7
      fastapi/applications.py
  6. 22
      fastapi/dependencies/models.py
  7. 8
      fastapi/routing.py
  8. 7
      tests/test_computed_fields.py
  9. 77
      tests/test_dependency_wrapped.py

8
docs/en/docs/advanced/behind-a-proxy.md

@ -443,6 +443,14 @@ The docs UI will interact with the server that you select.
/// ///
/// note | Technical Details
The `servers` property in the OpenAPI specification is optional.
If you don't specify the `servers` parameter and `root_path` is equal to `/`, the `servers` property in the generated OpenAPI schema will be omitted entirely by default, which is the equivalent of a single server with a `url` value of `/`.
///
### Disable automatic server from `root_path` { #disable-automatic-server-from-root-path } ### Disable automatic server from `root_path` { #disable-automatic-server-from-root-path }
If you don't want **FastAPI** to include an automatic server using the `root_path`, you can use the parameter `root_path_in_servers=False`: If you don't want **FastAPI** to include an automatic server using the `root_path`, you can use the parameter `root_path_in_servers=False`:

19
docs/en/docs/release-notes.md

@ -7,6 +7,25 @@ hide:
## Latest Changes ## Latest Changes
### Features
* ✨ Handle wrapped dependencies. PR [#9555](https://github.com/fastapi/fastapi/pull/9555) by [@phy1729](https://github.com/phy1729).
### Refactors
* 🔥 Remove dangling extra condiitonal no longer needed. PR [#14435](https://github.com/fastapi/fastapi/pull/14435) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Refactor internals, update `is_coroutine` check to reuse internal supported variants (unwrap, check class). PR [#14434](https://github.com/fastapi/fastapi/pull/14434) by [@tiangolo](https://github.com/tiangolo).
## 0.123.4
### Fixes
* 🐛 Fix OpenAPI schema support for computed fields when using `separate_input_output_schemas=False`. PR [#13207](https://github.com/fastapi/fastapi/pull/13207) by [@vgrafe](https://github.com/vgrafe).
### Docs
* 📝 Fix docstring of `servers` parameter. PR [#14405](https://github.com/fastapi/fastapi/pull/14405) by [@YuriiMotov](https://github.com/YuriiMotov).
## 0.123.3 ## 0.123.3
### Fixes ### Fixes

2
fastapi/__init__.py

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

14
fastapi/_compat/v2.py

@ -180,8 +180,13 @@ def get_schema_from_model_field(
], ],
separate_input_output_schemas: bool = True, separate_input_output_schemas: bool = True,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
computed_fields = field._type_adapter.core_schema.get("schema", {}).get(
"computed_fields", []
)
override_mode: Union[Literal["validation"], None] = ( override_mode: Union[Literal["validation"], None] = (
None if separate_input_output_schemas else "validation" None
if (separate_input_output_schemas or len(computed_fields) > 0)
else "validation"
) )
# This expects that GenerateJsonSchema was already used to generate the definitions # This expects that GenerateJsonSchema was already used to generate the definitions
json_schema = field_mapping[(field, override_mode or field.mode)] json_schema = field_mapping[(field, override_mode or field.mode)]
@ -203,9 +208,14 @@ def get_definitions(
Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue],
Dict[str, Dict[str, Any]], Dict[str, Dict[str, Any]],
]: ]:
has_computed_fields: bool = any(
field._type_adapter.core_schema.get("schema", {}).get("computed_fields", [])
for field in fields
)
schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE) schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
override_mode: Union[Literal["validation"], None] = ( override_mode: Union[Literal["validation"], None] = (
None if separate_input_output_schemas else "validation" None if (separate_input_output_schemas or has_computed_fields) else "validation"
) )
validation_fields = [field for field in fields if field.mode == "validation"] validation_fields = [field for field in fields if field.mode == "validation"]
serialization_fields = [field for field in fields if field.mode == "serialization"] serialization_fields = [field for field in fields if field.mode == "serialization"]

7
fastapi/applications.py

@ -301,7 +301,12 @@ class FastAPI(Starlette):
browser tabs open). Or if you want to leave fixed the possible URLs. browser tabs open). Or if you want to leave fixed the possible URLs.
If the servers `list` is not provided, or is an empty `list`, the If the servers `list` is not provided, or is an empty `list`, the
default value would be a `dict` with a `url` value of `/`. `servers` property in the generated OpenAPI will be:
* a `dict` with a `url` value of the application's mounting point
(`root_path`) if it's different from `/`.
* otherwise, the `servers` property will be omitted from the OpenAPI
schema.
Each item in the `list` is a `dict` containing: Each item in the `list` is a `dict` containing:

22
fastapi/dependencies/models.py

@ -75,27 +75,33 @@ class Dependant:
return True return True
return False return False
@cached_property
def _unwrapped_call(self) -> Any:
if self.call is None:
return self.call # pragma: no cover
return inspect.unwrap(self.call)
@cached_property @cached_property
def is_gen_callable(self) -> bool: def is_gen_callable(self) -> bool:
if inspect.isgeneratorfunction(self.call): if inspect.isgeneratorfunction(self._unwrapped_call):
return True return True
dunder_call = getattr(self.call, "__call__", None) # noqa: B004 dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004
return inspect.isgeneratorfunction(dunder_call) return inspect.isgeneratorfunction(dunder_call)
@cached_property @cached_property
def is_async_gen_callable(self) -> bool: def is_async_gen_callable(self) -> bool:
if inspect.isasyncgenfunction(self.call): if inspect.isasyncgenfunction(self._unwrapped_call):
return True return True
dunder_call = getattr(self.call, "__call__", None) # noqa: B004 dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004
return inspect.isasyncgenfunction(dunder_call) return inspect.isasyncgenfunction(dunder_call)
@cached_property @cached_property
def is_coroutine_callable(self) -> bool: def is_coroutine_callable(self) -> bool:
if inspect.isroutine(self.call): if inspect.isroutine(self._unwrapped_call):
return iscoroutinefunction(self.call) return iscoroutinefunction(self._unwrapped_call)
if inspect.isclass(self.call): if inspect.isclass(self._unwrapped_call):
return False return False
dunder_call = getattr(self.call, "__call__", None) # noqa: B004 dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004
return iscoroutinefunction(dunder_call) return iscoroutinefunction(dunder_call)
@cached_property @cached_property

8
fastapi/routing.py

@ -3,7 +3,6 @@ import email.message
import functools import functools
import inspect import inspect
import json import json
import sys
from contextlib import AsyncExitStack, asynccontextmanager from contextlib import AsyncExitStack, asynccontextmanager
from enum import Enum, IntEnum from enum import Enum, IntEnum
from typing import ( from typing import (
@ -79,11 +78,6 @@ from starlette.types import AppType, ASGIApp, Lifespan, Receive, Scope, Send
from starlette.websockets import WebSocket from starlette.websockets import WebSocket
from typing_extensions import Annotated, deprecated from typing_extensions import Annotated, deprecated
if sys.version_info >= (3, 13): # pragma: no cover
from inspect import iscoroutinefunction
else: # pragma: no cover
from asyncio import iscoroutinefunction
# Copy of starlette.routing.request_response modified to include the # Copy of starlette.routing.request_response modified to include the
# dependencies' AsyncExitStack # dependencies' AsyncExitStack
@ -308,7 +302,7 @@ def get_request_handler(
embed_body_fields: bool = False, embed_body_fields: bool = False,
) -> Callable[[Request], Coroutine[Any, Any, Response]]: ) -> Callable[[Request], Coroutine[Any, Any, Response]]:
assert dependant.call is not None, "dependant.call must be a function" assert dependant.call is not None, "dependant.call must be a function"
is_coroutine = iscoroutinefunction(dependant.call) is_coroutine = dependant.is_coroutine_callable
is_body_form = body_field and isinstance( is_body_form = body_field and isinstance(
body_field.field_info, (params.Form, temp_pydantic_v1_params.Form) body_field.field_info, (params.Form, temp_pydantic_v1_params.Form)
) )

7
tests/test_computed_fields.py

@ -6,8 +6,9 @@ from .utils import needs_pydanticv2
@pytest.fixture(name="client") @pytest.fixture(name="client")
def get_client(): def get_client(request):
app = FastAPI() separate_input_output_schemas = request.param
app = FastAPI(separate_input_output_schemas=separate_input_output_schemas)
from pydantic import BaseModel, computed_field from pydantic import BaseModel, computed_field
@ -32,6 +33,7 @@ def get_client():
return client return client
@pytest.mark.parametrize("client", [True, False], indirect=True)
@pytest.mark.parametrize("path", ["/", "/responses"]) @pytest.mark.parametrize("path", ["/", "/responses"])
@needs_pydanticv2 @needs_pydanticv2
def test_get(client: TestClient, path: str): def test_get(client: TestClient, path: str):
@ -40,6 +42,7 @@ def test_get(client: TestClient, path: str):
assert response.json() == {"width": 3, "length": 4, "area": 12} assert response.json() == {"width": 3, "length": 4, "area": 12}
@pytest.mark.parametrize("client", [True, False], indirect=True)
@needs_pydanticv2 @needs_pydanticv2
def test_openapi_schema(client: TestClient): def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json") response = client.get("/openapi.json")

77
tests/test_dependency_wrapped.py

@ -0,0 +1,77 @@
from functools import wraps
from typing import AsyncGenerator, Generator
import pytest
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
def noop_wrap(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
app = FastAPI()
@noop_wrap
def wrapped_dependency() -> bool:
return True
@noop_wrap
def wrapped_gen_dependency() -> Generator[bool, None, None]:
yield True
@noop_wrap
async def async_wrapped_dependency() -> bool:
return True
@noop_wrap
async def async_wrapped_gen_dependency() -> AsyncGenerator[bool, None]:
yield True
@app.get("/wrapped-dependency/")
async def get_wrapped_dependency(value: bool = Depends(wrapped_dependency)):
return value
@app.get("/wrapped-gen-dependency/")
async def get_wrapped_gen_dependency(value: bool = Depends(wrapped_gen_dependency)):
return value
@app.get("/async-wrapped-dependency/")
async def get_async_wrapped_dependency(value: bool = Depends(async_wrapped_dependency)):
return value
@app.get("/async-wrapped-gen-dependency/")
async def get_async_wrapped_gen_dependency(
value: bool = Depends(async_wrapped_gen_dependency),
):
return value
client = TestClient(app)
@pytest.mark.parametrize(
"route",
[
"/wrapped-dependency",
"/wrapped-gen-dependency",
"/async-wrapped-dependency",
"/async-wrapped-gen-dependency",
],
)
def test_class_dependency(route):
response = client.get(route)
assert response.status_code == 200, response.text
assert response.json() is True
Loading…
Cancel
Save