From 0e5dca336e4aa7560abf0b5939115aa0865daf45 Mon Sep 17 00:00:00 2001 From: Konstantin Zangerle Date: Wed, 4 Jun 2025 15:34:49 +0200 Subject: [PATCH 1/5] Use griffe to parse docstrings and add descriptions --- fastapi/openapi/utils.py | 27 ++++++++++++++++++++++- fastapi/routing.py | 47 +++++++++++++++++++++------------------- pyproject.toml | 1 + 3 files changed, 52 insertions(+), 23 deletions(-) diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 808646cc2..c2cb8b5df 100644 --- a/fastapi/openapi/utils.py +++ b/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 ): diff --git a/fastapi/routing.py b/fastapi/routing.py index 457481e32..f14e81e01 100644 --- a/fastapi/routing.py +++ b/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( diff --git a/pyproject.toml b/pyproject.toml index 1c540e2f6..973313b15 100644 --- a/pyproject.toml +++ b/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] From 64f88569243c38847ce1a6b2d9768c09c9055ad3 Mon Sep 17 00:00:00 2001 From: Konstantin Zangerle Date: Wed, 4 Jun 2025 15:52:24 +0200 Subject: [PATCH 2/5] make compatible with 3.8 --- fastapi/openapi/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index c2cb8b5df..be23ba346 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -100,7 +100,7 @@ def _get_openapi_operation_parameters( field_mapping: Dict[ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue ], - field_docstring: Dict[str, str] | None = None, + field_docstring: Optional[Dict[str, str]] = None, separate_input_output_schemas: bool = True, ) -> List[Dict[str, Any]]: parameters = [] @@ -240,9 +240,12 @@ def get_openapi_operation_metadata( 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: operation["description"] = route.description - operation["description"] = operation["description"].split("\f")[0].strip() + operation["description"] = operation["description"].split("\f")[0].strip() operation_id = route.operation_id or route.unique_id if operation_id in operation_ids: From 3fab577dc18e6da9bafcd2c2ff6f99c715eaa6de Mon Sep 17 00:00:00 2001 From: Konstantin Zangerle Date: Wed, 4 Jun 2025 16:05:10 +0200 Subject: [PATCH 3/5] fix signature consistency --- fastapi/applications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index 6d427cdc2..d3427dec0 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -141,7 +141,7 @@ class FastAPI(Starlette): ), ] = None, description: Annotated[ - str, + Optional[str], Doc( ''' A description of the API. Supports Markdown (using From 8683d33b319d4c488b90aee5c475ef07880172cc Mon Sep 17 00:00:00 2001 From: Konstantin Zangerle Date: Wed, 4 Jun 2025 16:08:28 +0200 Subject: [PATCH 4/5] try with griffe 1.4.0 for python 3.8 suppotr --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 973313b15..3eddfd446 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +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", + "griffe>=1.4.0", ] [project.urls] From 0226b66f3867aff0b603b07cf7d721f4349fcaf0 Mon Sep 17 00:00:00 2001 From: Konstantin Zangerle Date: Wed, 4 Jun 2025 16:26:40 +0200 Subject: [PATCH 5/5] more signature consistency --- fastapi/applications.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index d3427dec0..a0dccb507 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -1064,7 +1064,7 @@ class FastAPI(Starlette): dependencies: Optional[Sequence[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, @@ -1122,7 +1122,7 @@ class FastAPI(Starlette): dependencies: Optional[Sequence[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, @@ -1574,7 +1574,7 @@ class FastAPI(Starlette): ), ] = None, response_description: Annotated[ - str, + Optional[str], Doc( """ 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`). """ ), - ] = "Successful Response", + ] = None, responses: Annotated[ Optional[Dict[Union[int, str], Dict[str, Any]]], Doc( @@ -1947,7 +1947,7 @@ class FastAPI(Starlette): ), ] = None, response_description: Annotated[ - str, + Optional[str], Doc( """ 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`). """ ), - ] = "Successful Response", + ] = None, responses: Annotated[ Optional[Dict[Union[int, str], Dict[str, Any]]], Doc( @@ -2325,7 +2325,7 @@ class FastAPI(Starlette): ), ] = None, response_description: Annotated[ - str, + Optional[str], Doc( """ 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`). """ ), - ] = "Successful Response", + ] = None, responses: Annotated[ Optional[Dict[Union[int, str], Dict[str, Any]]], Doc( @@ -2703,7 +2703,7 @@ class FastAPI(Starlette): ), ] = None, response_description: Annotated[ - str, + Optional[str], Doc( """ 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`). """ ), - ] = "Successful Response", + ] = None, responses: Annotated[ Optional[Dict[Union[int, str], Dict[str, Any]]], Doc( @@ -3076,7 +3076,7 @@ class FastAPI(Starlette): ), ] = None, response_description: Annotated[ - str, + Optional[str], Doc( """ 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`). """ ), - ] = "Successful Response", + ] = None, responses: Annotated[ Optional[Dict[Union[int, str], Dict[str, Any]]], Doc( @@ -3449,7 +3449,7 @@ class FastAPI(Starlette): ), ] = None, response_description: Annotated[ - str, + Optional[str], Doc( """ 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`). """ ), - ] = "Successful Response", + ] = None, responses: Annotated[ Optional[Dict[Union[int, str], Dict[str, Any]]], Doc( @@ -3822,7 +3822,7 @@ class FastAPI(Starlette): ), ] = None, response_description: Annotated[ - str, + Optional[str], Doc( """ 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`). """ ), - ] = "Successful Response", + ] = None, responses: Annotated[ Optional[Dict[Union[int, str], Dict[str, Any]]], Doc( @@ -4200,7 +4200,7 @@ class FastAPI(Starlette): ), ] = None, response_description: Annotated[ - str, + Optional[str], Doc( """ 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`). """ ), - ] = "Successful Response", + ] = None, responses: Annotated[ Optional[Dict[Union[int, str], Dict[str, Any]]], Doc(