Browse Source

Use griffe to parse docstrings and add descriptions

pull/13767/head
Konstantin Zangerle 1 month ago
parent
commit
0e5dca336e
  1. 27
      fastapi/openapi/utils.py
  2. 47
      fastapi/routing.py
  3. 1
      pyproject.toml

27
fastapi/openapi/utils.py

@ -100,6 +100,7 @@ def _get_openapi_operation_parameters(
field_mapping: Dict[
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
],
field_docstring: Dict[str, str] | None = None,
separate_input_output_schemas: bool = True,
) -> List[Dict[str, Any]]:
parameters = []
@ -115,6 +116,7 @@ def _get_openapi_operation_parameters(
(ParamTypes.cookie, cookie_params),
]
default_convert_underscores = True
field_docstring = field_docstring or {}
if len(flat_dependant.header_params) == 1:
first_field = flat_dependant.header_params[0]
if lenient_issubclass(first_field.type_, BaseModel):
@ -155,6 +157,8 @@ def _get_openapi_operation_parameters(
}
if 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)
example = getattr(field_info, "example", None)
if openapi_examples:
@ -232,8 +236,14 @@ def get_openapi_operation_metadata(
if route.tags:
operation["tags"] = route.tags
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"]
)
if route.description:
operation["description"] = route.description
operation["description"] = operation["description"].split("\f")[0].strip()
operation_id = route.operation_id or route.unique_id
if operation_id in operation_ids:
message = (
@ -273,6 +283,10 @@ def get_openapi_path(
assert current_response_class, "A response class is needed to generate OpenAPI"
route_response_media_type: Optional[str] = current_response_class.media_type
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:
operation = get_openapi_operation_metadata(
route=route, method=method, operation_ids=operation_ids
@ -292,6 +306,7 @@ def get_openapi_path(
model_name_map=model_name_map,
field_mapping=field_mapping,
separate_input_output_schemas=separate_input_output_schemas,
field_docstring=args_docstring_mapping,
)
parameters.extend(operation_parameters)
if parameters:
@ -348,9 +363,19 @@ def get_openapi_path(
if status_code_param is not None:
if isinstance(status_code_param.default, int):
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, {})[
"description"
] = route.response_description
] = response_description
if route_response_media_type and is_body_allowed_for_status_code(
route.status_code
):

47
fastapi/routing.py

@ -1,7 +1,6 @@
import asyncio
import dataclasses
import email.message
import inspect
import json
from contextlib import AsyncExitStack, asynccontextmanager
from enum import Enum, IntEnum
@ -56,6 +55,7 @@ from fastapi.utils import (
get_value_or_default,
is_body_allowed_for_status_code,
)
from griffe import Docstring
from pydantic import BaseModel
from starlette import routing
from starlette.concurrency import run_in_threadpool
@ -437,7 +437,7 @@ class APIRoute(routing.Route):
dependencies: Optional[Sequence[params.Depends]] = None,
summary: 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,
deprecated: Optional[bool] = None,
name: Optional[str] = None,
@ -528,10 +528,13 @@ class APIRoute(routing.Route):
self.response_field = None # type: ignore
self.secure_cloned_response_field = None
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,
# truncate description text to the content preceding the first "form feed"
self.description = self.description.split("\f")[0].strip()
response_fields = {}
for additional_status_code, response in self.responses.items():
assert isinstance(response, dict), "An additional response must be a dict"
@ -889,7 +892,7 @@ class APIRouter(routing.Router):
dependencies: Optional[Sequence[params.Depends]] = None,
summary: 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,
deprecated: Optional[bool] = None,
methods: Optional[Union[Set[str], List[str]]] = None,
@ -970,7 +973,7 @@ class APIRouter(routing.Router):
dependencies: Optional[Sequence[params.Depends]] = None,
summary: 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,
deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None,
@ -1480,7 +1483,7 @@ class APIRouter(routing.Router):
),
] = None,
response_description: Annotated[
str,
Optional[str],
Doc(
"""
The description for the default response.
@ -1488,7 +1491,7 @@ class APIRouter(routing.Router):
It will be added to the generated OpenAPI (e.g. visible at `/docs`).
"""
),
] = "Successful Response",
] = None,
responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc(
@ -1857,7 +1860,7 @@ class APIRouter(routing.Router):
),
] = None,
response_description: Annotated[
str,
Optional[str],
Doc(
"""
The description for the default response.
@ -1865,7 +1868,7 @@ class APIRouter(routing.Router):
It will be added to the generated OpenAPI (e.g. visible at `/docs`).
"""
),
] = "Successful Response",
] = None,
responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc(
@ -2239,7 +2242,7 @@ class APIRouter(routing.Router):
),
] = None,
response_description: Annotated[
str,
Optional[str],
Doc(
"""
The description for the default response.
@ -2247,7 +2250,7 @@ class APIRouter(routing.Router):
It will be added to the generated OpenAPI (e.g. visible at `/docs`).
"""
),
] = "Successful Response",
] = None,
responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc(
@ -2621,7 +2624,7 @@ class APIRouter(routing.Router):
),
] = None,
response_description: Annotated[
str,
Optional[str],
Doc(
"""
The description for the default response.
@ -2629,7 +2632,7 @@ class APIRouter(routing.Router):
It will be added to the generated OpenAPI (e.g. visible at `/docs`).
"""
),
] = "Successful Response",
] = None,
responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc(
@ -2998,7 +3001,7 @@ class APIRouter(routing.Router):
),
] = None,
response_description: Annotated[
str,
Optional[str],
Doc(
"""
The description for the default response.
@ -3006,7 +3009,7 @@ class APIRouter(routing.Router):
It will be added to the generated OpenAPI (e.g. visible at `/docs`).
"""
),
] = "Successful Response",
] = None,
responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc(
@ -3375,7 +3378,7 @@ class APIRouter(routing.Router):
),
] = None,
response_description: Annotated[
str,
Optional[str],
Doc(
"""
The description for the default response.
@ -3383,7 +3386,7 @@ class APIRouter(routing.Router):
It will be added to the generated OpenAPI (e.g. visible at `/docs`).
"""
),
] = "Successful Response",
] = None,
responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc(
@ -3757,7 +3760,7 @@ class APIRouter(routing.Router):
),
] = None,
response_description: Annotated[
str,
Optional[str],
Doc(
"""
The description for the default response.
@ -3765,7 +3768,7 @@ class APIRouter(routing.Router):
It will be added to the generated OpenAPI (e.g. visible at `/docs`).
"""
),
] = "Successful Response",
] = None,
responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc(
@ -4139,7 +4142,7 @@ class APIRouter(routing.Router):
),
] = None,
response_description: Annotated[
str,
Optional[str],
Doc(
"""
The description for the default response.
@ -4147,7 +4150,7 @@ class APIRouter(routing.Router):
It will be added to the generated OpenAPI (e.g. visible at `/docs`).
"""
),
] = "Successful Response",
] = None,
responses: Annotated[
Optional[Dict[Union[int, str], Dict[str, Any]]],
Doc(

1
pyproject.toml

@ -46,6 +46,7 @@ dependencies = [
"starlette>=0.40.0,<0.47.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",
"griffe>=1.7.0",
]
[project.urls]

Loading…
Cancel
Save