Browse Source

Merge 0226b66f38 into 313723494b

pull/13767/merge
JustKiddingCode 2 days ago
committed by GitHub
parent
commit
6077dbb9eb
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 38
      fastapi/applications.py
  2. 30
      fastapi/openapi/utils.py
  3. 47
      fastapi/routing.py
  4. 1
      pyproject.toml

38
fastapi/applications.py

@ -141,7 +141,7 @@ class FastAPI(Starlette):
), ),
] = None, ] = None,
description: Annotated[ description: Annotated[
str, Optional[str],
Doc( Doc(
''' '''
A description of the API. Supports Markdown (using A description of the API. Supports Markdown (using
@ -1064,7 +1064,7 @@ class FastAPI(Starlette):
dependencies: Optional[Sequence[Depends]] = None, dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
description: Optional[str] = None, description: Optional[str] = None,
response_description: str = "Successful Response", response_description: Optional[str] = None,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None, methods: Optional[List[str]] = None,
@ -1122,7 +1122,7 @@ class FastAPI(Starlette):
dependencies: Optional[Sequence[Depends]] = None, dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
description: Optional[str] = None, description: Optional[str] = None,
response_description: str = "Successful Response", response_description: Optional[str] = None,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None, methods: Optional[List[str]] = None,
@ -1574,7 +1574,7 @@ class FastAPI(Starlette):
), ),
] = None, ] = None,
response_description: Annotated[ response_description: Annotated[
str, Optional[str],
Doc( Doc(
""" """
The description for the default response. The description for the default response.
@ -1582,7 +1582,7 @@ class FastAPI(Starlette):
It will be added to the generated OpenAPI (e.g. visible at `/docs`). It will be added to the generated OpenAPI (e.g. visible at `/docs`).
""" """
), ),
] = "Successful Response", ] = None,
responses: Annotated[ responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]], Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc( Doc(
@ -1947,7 +1947,7 @@ class FastAPI(Starlette):
), ),
] = None, ] = None,
response_description: Annotated[ response_description: Annotated[
str, Optional[str],
Doc( Doc(
""" """
The description for the default response. The description for the default response.
@ -1955,7 +1955,7 @@ class FastAPI(Starlette):
It will be added to the generated OpenAPI (e.g. visible at `/docs`). It will be added to the generated OpenAPI (e.g. visible at `/docs`).
""" """
), ),
] = "Successful Response", ] = None,
responses: Annotated[ responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]], Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc( Doc(
@ -2325,7 +2325,7 @@ class FastAPI(Starlette):
), ),
] = None, ] = None,
response_description: Annotated[ response_description: Annotated[
str, Optional[str],
Doc( Doc(
""" """
The description for the default response. The description for the default response.
@ -2333,7 +2333,7 @@ class FastAPI(Starlette):
It will be added to the generated OpenAPI (e.g. visible at `/docs`). It will be added to the generated OpenAPI (e.g. visible at `/docs`).
""" """
), ),
] = "Successful Response", ] = None,
responses: Annotated[ responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]], Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc( Doc(
@ -2703,7 +2703,7 @@ class FastAPI(Starlette):
), ),
] = None, ] = None,
response_description: Annotated[ response_description: Annotated[
str, Optional[str],
Doc( Doc(
""" """
The description for the default response. The description for the default response.
@ -2711,7 +2711,7 @@ class FastAPI(Starlette):
It will be added to the generated OpenAPI (e.g. visible at `/docs`). It will be added to the generated OpenAPI (e.g. visible at `/docs`).
""" """
), ),
] = "Successful Response", ] = None,
responses: Annotated[ responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]], Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc( Doc(
@ -3076,7 +3076,7 @@ class FastAPI(Starlette):
), ),
] = None, ] = None,
response_description: Annotated[ response_description: Annotated[
str, Optional[str],
Doc( Doc(
""" """
The description for the default response. The description for the default response.
@ -3084,7 +3084,7 @@ class FastAPI(Starlette):
It will be added to the generated OpenAPI (e.g. visible at `/docs`). It will be added to the generated OpenAPI (e.g. visible at `/docs`).
""" """
), ),
] = "Successful Response", ] = None,
responses: Annotated[ responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]], Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc( Doc(
@ -3449,7 +3449,7 @@ class FastAPI(Starlette):
), ),
] = None, ] = None,
response_description: Annotated[ response_description: Annotated[
str, Optional[str],
Doc( Doc(
""" """
The description for the default response. The description for the default response.
@ -3457,7 +3457,7 @@ class FastAPI(Starlette):
It will be added to the generated OpenAPI (e.g. visible at `/docs`). It will be added to the generated OpenAPI (e.g. visible at `/docs`).
""" """
), ),
] = "Successful Response", ] = None,
responses: Annotated[ responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]], Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc( Doc(
@ -3822,7 +3822,7 @@ class FastAPI(Starlette):
), ),
] = None, ] = None,
response_description: Annotated[ response_description: Annotated[
str, Optional[str],
Doc( Doc(
""" """
The description for the default response. The description for the default response.
@ -3830,7 +3830,7 @@ class FastAPI(Starlette):
It will be added to the generated OpenAPI (e.g. visible at `/docs`). It will be added to the generated OpenAPI (e.g. visible at `/docs`).
""" """
), ),
] = "Successful Response", ] = None,
responses: Annotated[ responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]], Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc( Doc(
@ -4200,7 +4200,7 @@ class FastAPI(Starlette):
), ),
] = None, ] = None,
response_description: Annotated[ response_description: Annotated[
str, Optional[str],
Doc( Doc(
""" """
The description for the default response. The description for the default response.
@ -4208,7 +4208,7 @@ class FastAPI(Starlette):
It will be added to the generated OpenAPI (e.g. visible at `/docs`). It will be added to the generated OpenAPI (e.g. visible at `/docs`).
""" """
), ),
] = "Successful Response", ] = None,
responses: Annotated[ responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]], Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc( Doc(

30
fastapi/openapi/utils.py

@ -100,6 +100,7 @@ def _get_openapi_operation_parameters(
field_mapping: Dict[ field_mapping: Dict[
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
], ],
field_docstring: Optional[Dict[str, str]] = None,
separate_input_output_schemas: bool = True, separate_input_output_schemas: bool = True,
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
parameters = [] parameters = []
@ -115,6 +116,7 @@ def _get_openapi_operation_parameters(
(ParamTypes.cookie, cookie_params), (ParamTypes.cookie, cookie_params),
] ]
default_convert_underscores = True default_convert_underscores = True
field_docstring = field_docstring or {}
if len(flat_dependant.header_params) == 1: if len(flat_dependant.header_params) == 1:
first_field = flat_dependant.header_params[0] first_field = flat_dependant.header_params[0]
if lenient_issubclass(first_field.type_, BaseModel): if lenient_issubclass(first_field.type_, BaseModel):
@ -155,6 +157,8 @@ def _get_openapi_operation_parameters(
} }
if field_info.description: if field_info.description:
parameter["description"] = field_info.description parameter["description"] = field_info.description
elif param.name in field_docstring:
parameter["description"] = field_docstring[param.name]
openapi_examples = getattr(field_info, "openapi_examples", None) openapi_examples = getattr(field_info, "openapi_examples", None)
example = getattr(field_info, "example", None) example = getattr(field_info, "example", None)
if openapi_examples: if openapi_examples:
@ -232,8 +236,17 @@ def get_openapi_operation_metadata(
if route.tags: if route.tags:
operation["tags"] = route.tags operation["tags"] = route.tags
operation["summary"] = generate_operation_summary(route=route, method=method) operation["summary"] = generate_operation_summary(route=route, method=method)
if route.parsed_docstring:
operation["description"] = "\n\n".join(
[i.value for i in route.parsed_docstring if i.kind == "text"]
)
operation["description"] = operation["description"].split("\f")[0].strip()
if not operation["description"]:
del operation["description"]
if route.description: if route.description:
operation["description"] = route.description operation["description"] = route.description
operation["description"] = operation["description"].split("\f")[0].strip()
operation_id = route.operation_id or route.unique_id operation_id = route.operation_id or route.unique_id
if operation_id in operation_ids: if operation_id in operation_ids:
message = ( message = (
@ -273,6 +286,10 @@ def get_openapi_path(
assert current_response_class, "A response class is needed to generate OpenAPI" assert current_response_class, "A response class is needed to generate OpenAPI"
route_response_media_type: Optional[str] = current_response_class.media_type route_response_media_type: Optional[str] = current_response_class.media_type
if route.include_in_schema: if route.include_in_schema:
args = [i.value for i in route.parsed_docstring if i.kind == "parameters"]
args_docstring_mapping = (
{a.name: a.description for a in args[0]} if len(args) == 1 else {}
)
for method in route.methods: for method in route.methods:
operation = get_openapi_operation_metadata( operation = get_openapi_operation_metadata(
route=route, method=method, operation_ids=operation_ids route=route, method=method, operation_ids=operation_ids
@ -292,6 +309,7 @@ def get_openapi_path(
model_name_map=model_name_map, model_name_map=model_name_map,
field_mapping=field_mapping, field_mapping=field_mapping,
separate_input_output_schemas=separate_input_output_schemas, separate_input_output_schemas=separate_input_output_schemas,
field_docstring=args_docstring_mapping,
) )
parameters.extend(operation_parameters) parameters.extend(operation_parameters)
if parameters: if parameters:
@ -348,9 +366,19 @@ def get_openapi_path(
if status_code_param is not None: if status_code_param is not None:
if isinstance(status_code_param.default, int): if isinstance(status_code_param.default, int):
status_code = str(status_code_param.default) status_code = str(status_code_param.default)
response_description = ""
if route.parsed_docstring:
ret = [i.value for i in route.parsed_docstring if i.kind == "returns"]
response_description = (
",".join(x.description for x in ret[0])
if len(ret) == 1
else "Successful Response"
)
if route.response_description:
response_description = route.response_description
operation.setdefault("responses", {}).setdefault(status_code, {})[ operation.setdefault("responses", {}).setdefault(status_code, {})[
"description" "description"
] = route.response_description ] = response_description
if route_response_media_type and is_body_allowed_for_status_code( if route_response_media_type and is_body_allowed_for_status_code(
route.status_code route.status_code
): ):

47
fastapi/routing.py

@ -1,7 +1,6 @@
import asyncio import asyncio
import dataclasses import dataclasses
import email.message import email.message
import inspect
import json import json
from contextlib import AsyncExitStack, asynccontextmanager from contextlib import AsyncExitStack, asynccontextmanager
from enum import Enum, IntEnum from enum import Enum, IntEnum
@ -57,6 +56,7 @@ from fastapi.utils import (
get_value_or_default, get_value_or_default,
is_body_allowed_for_status_code, is_body_allowed_for_status_code,
) )
from griffe import Docstring
from pydantic import BaseModel from pydantic import BaseModel
from starlette import routing from starlette import routing
from starlette.concurrency import run_in_threadpool from starlette.concurrency import run_in_threadpool
@ -438,7 +438,7 @@ class APIRoute(routing.Route):
dependencies: Optional[Sequence[params.Depends]] = None, dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
description: Optional[str] = None, description: Optional[str] = None,
response_description: str = "Successful Response", response_description: Optional[str] = None,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
name: Optional[str] = None, name: Optional[str] = None,
@ -529,10 +529,13 @@ class APIRoute(routing.Route):
self.response_field = None # type: ignore self.response_field = None # type: ignore
self.secure_cloned_response_field = None self.secure_cloned_response_field = None
self.dependencies = list(dependencies or []) self.dependencies = list(dependencies or [])
self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "") self.description = description
self.docstring = self.endpoint.__doc__ or ""
self.parsed_docstring = Docstring(
self.docstring, parser="google", parser_options={"warnings": False}
).parsed
# if a "form feed" character (page break) is found in the description text, # if a "form feed" character (page break) is found in the description text,
# truncate description text to the content preceding the first "form feed" # truncate description text to the content preceding the first "form feed"
self.description = self.description.split("\f")[0].strip()
response_fields = {} response_fields = {}
for additional_status_code, response in self.responses.items(): for additional_status_code, response in self.responses.items():
assert isinstance(response, dict), "An additional response must be a dict" assert isinstance(response, dict), "An additional response must be a dict"
@ -890,7 +893,7 @@ class APIRouter(routing.Router):
dependencies: Optional[Sequence[params.Depends]] = None, dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
description: Optional[str] = None, description: Optional[str] = None,
response_description: str = "Successful Response", response_description: Optional[str] = None,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
methods: Optional[Union[Set[str], List[str]]] = None, methods: Optional[Union[Set[str], List[str]]] = None,
@ -971,7 +974,7 @@ class APIRouter(routing.Router):
dependencies: Optional[Sequence[params.Depends]] = None, dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None, summary: Optional[str] = None,
description: Optional[str] = None, description: Optional[str] = None,
response_description: str = "Successful Response", response_description: Optional[str] = None,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None, methods: Optional[List[str]] = None,
@ -1481,7 +1484,7 @@ class APIRouter(routing.Router):
), ),
] = None, ] = None,
response_description: Annotated[ response_description: Annotated[
str, Optional[str],
Doc( Doc(
""" """
The description for the default response. The description for the default response.
@ -1489,7 +1492,7 @@ class APIRouter(routing.Router):
It will be added to the generated OpenAPI (e.g. visible at `/docs`). It will be added to the generated OpenAPI (e.g. visible at `/docs`).
""" """
), ),
] = "Successful Response", ] = None,
responses: Annotated[ responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]], Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc( Doc(
@ -1858,7 +1861,7 @@ class APIRouter(routing.Router):
), ),
] = None, ] = None,
response_description: Annotated[ response_description: Annotated[
str, Optional[str],
Doc( Doc(
""" """
The description for the default response. The description for the default response.
@ -1866,7 +1869,7 @@ class APIRouter(routing.Router):
It will be added to the generated OpenAPI (e.g. visible at `/docs`). It will be added to the generated OpenAPI (e.g. visible at `/docs`).
""" """
), ),
] = "Successful Response", ] = None,
responses: Annotated[ responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]], Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc( Doc(
@ -2240,7 +2243,7 @@ class APIRouter(routing.Router):
), ),
] = None, ] = None,
response_description: Annotated[ response_description: Annotated[
str, Optional[str],
Doc( Doc(
""" """
The description for the default response. The description for the default response.
@ -2248,7 +2251,7 @@ class APIRouter(routing.Router):
It will be added to the generated OpenAPI (e.g. visible at `/docs`). It will be added to the generated OpenAPI (e.g. visible at `/docs`).
""" """
), ),
] = "Successful Response", ] = None,
responses: Annotated[ responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]], Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc( Doc(
@ -2622,7 +2625,7 @@ class APIRouter(routing.Router):
), ),
] = None, ] = None,
response_description: Annotated[ response_description: Annotated[
str, Optional[str],
Doc( Doc(
""" """
The description for the default response. The description for the default response.
@ -2630,7 +2633,7 @@ class APIRouter(routing.Router):
It will be added to the generated OpenAPI (e.g. visible at `/docs`). It will be added to the generated OpenAPI (e.g. visible at `/docs`).
""" """
), ),
] = "Successful Response", ] = None,
responses: Annotated[ responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]], Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc( Doc(
@ -2999,7 +3002,7 @@ class APIRouter(routing.Router):
), ),
] = None, ] = None,
response_description: Annotated[ response_description: Annotated[
str, Optional[str],
Doc( Doc(
""" """
The description for the default response. The description for the default response.
@ -3007,7 +3010,7 @@ class APIRouter(routing.Router):
It will be added to the generated OpenAPI (e.g. visible at `/docs`). It will be added to the generated OpenAPI (e.g. visible at `/docs`).
""" """
), ),
] = "Successful Response", ] = None,
responses: Annotated[ responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]], Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc( Doc(
@ -3376,7 +3379,7 @@ class APIRouter(routing.Router):
), ),
] = None, ] = None,
response_description: Annotated[ response_description: Annotated[
str, Optional[str],
Doc( Doc(
""" """
The description for the default response. The description for the default response.
@ -3384,7 +3387,7 @@ class APIRouter(routing.Router):
It will be added to the generated OpenAPI (e.g. visible at `/docs`). It will be added to the generated OpenAPI (e.g. visible at `/docs`).
""" """
), ),
] = "Successful Response", ] = None,
responses: Annotated[ responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]], Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc( Doc(
@ -3758,7 +3761,7 @@ class APIRouter(routing.Router):
), ),
] = None, ] = None,
response_description: Annotated[ response_description: Annotated[
str, Optional[str],
Doc( Doc(
""" """
The description for the default response. The description for the default response.
@ -3766,7 +3769,7 @@ class APIRouter(routing.Router):
It will be added to the generated OpenAPI (e.g. visible at `/docs`). It will be added to the generated OpenAPI (e.g. visible at `/docs`).
""" """
), ),
] = "Successful Response", ] = None,
responses: Annotated[ responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]], Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc( Doc(
@ -4140,7 +4143,7 @@ class APIRouter(routing.Router):
), ),
] = None, ] = None,
response_description: Annotated[ response_description: Annotated[
str, Optional[str],
Doc( Doc(
""" """
The description for the default response. The description for the default response.
@ -4148,7 +4151,7 @@ class APIRouter(routing.Router):
It will be added to the generated OpenAPI (e.g. visible at `/docs`). It will be added to the generated OpenAPI (e.g. visible at `/docs`).
""" """
), ),
] = "Successful Response", ] = None,
responses: Annotated[ responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]], Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc( Doc(

1
pyproject.toml

@ -46,6 +46,7 @@ dependencies = [
"starlette>=0.40.0,<0.48.0", "starlette>=0.40.0,<0.48.0",
"pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0",
"typing-extensions>=4.8.0", "typing-extensions>=4.8.0",
"griffe>=1.4.0",
] ]
[project.urls] [project.urls]

Loading…
Cancel
Save