diff --git a/fastapi/applications.py b/fastapi/applications.py index 05c7bd2be..23185b2f3 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 @@ -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( diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 808646cc2..be23ba346 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: Optional[Dict[str, str]] = 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,17 @@ 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"] + ) + 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_id = route.operation_id or route.unique_id if operation_id in operation_ids: message = ( @@ -273,6 +286,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 +309,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 +366,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 54c75a027..f884ed06a 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 @@ -57,6 +56,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 @@ -438,7 +438,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, @@ -529,10 +529,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" @@ -890,7 +893,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, @@ -971,7 +974,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, @@ -1481,7 +1484,7 @@ class APIRouter(routing.Router): ), ] = None, response_description: Annotated[ - str, + Optional[str], Doc( """ 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`). """ ), - ] = "Successful Response", + ] = None, responses: Annotated[ Optional[Dict[Union[int, str], Dict[str, Any]]], Doc( @@ -1858,7 +1861,7 @@ class APIRouter(routing.Router): ), ] = None, response_description: Annotated[ - str, + Optional[str], Doc( """ 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`). """ ), - ] = "Successful Response", + ] = None, responses: Annotated[ Optional[Dict[Union[int, str], Dict[str, Any]]], Doc( @@ -2240,7 +2243,7 @@ class APIRouter(routing.Router): ), ] = None, response_description: Annotated[ - str, + Optional[str], Doc( """ 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`). """ ), - ] = "Successful Response", + ] = None, responses: Annotated[ Optional[Dict[Union[int, str], Dict[str, Any]]], Doc( @@ -2622,7 +2625,7 @@ class APIRouter(routing.Router): ), ] = None, response_description: Annotated[ - str, + Optional[str], Doc( """ 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`). """ ), - ] = "Successful Response", + ] = None, responses: Annotated[ Optional[Dict[Union[int, str], Dict[str, Any]]], Doc( @@ -2999,7 +3002,7 @@ class APIRouter(routing.Router): ), ] = None, response_description: Annotated[ - str, + Optional[str], Doc( """ 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`). """ ), - ] = "Successful Response", + ] = None, responses: Annotated[ Optional[Dict[Union[int, str], Dict[str, Any]]], Doc( @@ -3376,7 +3379,7 @@ class APIRouter(routing.Router): ), ] = None, response_description: Annotated[ - str, + Optional[str], Doc( """ 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`). """ ), - ] = "Successful Response", + ] = None, responses: Annotated[ Optional[Dict[Union[int, str], Dict[str, Any]]], Doc( @@ -3758,7 +3761,7 @@ class APIRouter(routing.Router): ), ] = None, response_description: Annotated[ - str, + Optional[str], Doc( """ 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`). """ ), - ] = "Successful Response", + ] = None, responses: Annotated[ Optional[Dict[Union[int, str], Dict[str, Any]]], Doc( @@ -4140,7 +4143,7 @@ class APIRouter(routing.Router): ), ] = None, response_description: Annotated[ - str, + Optional[str], Doc( """ 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`). """ ), - ] = "Successful Response", + ] = None, responses: Annotated[ Optional[Dict[Union[int, str], Dict[str, Any]]], Doc( diff --git a/pyproject.toml b/pyproject.toml index 7709451ff..a4438f9a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ dependencies = [ "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", "typing-extensions>=4.8.0", + "griffe>=1.4.0", ] [project.urls]