From 569afb4378c80e0bff5dc4a45f26d012e498eda6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Jan 2022 18:43:04 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20tags=20with?= =?UTF-8?q?=20Enums=20(#4468)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tutorial/path-operation-configuration.md | 12 ++++ .../tutorial002b.py | 20 +++++++ fastapi/applications.py | 23 ++++---- fastapi/routing.py | 32 +++++------ .../test_tutorial002b.py | 56 +++++++++++++++++++ 5 files changed, 116 insertions(+), 27 deletions(-) create mode 100644 docs_src/path_operation_configuration/tutorial002b.py create mode 100644 tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py diff --git a/docs/en/docs/tutorial/path-operation-configuration.md b/docs/en/docs/tutorial/path-operation-configuration.md index 1ff448e76..884a762e2 100644 --- a/docs/en/docs/tutorial/path-operation-configuration.md +++ b/docs/en/docs/tutorial/path-operation-configuration.md @@ -64,6 +64,18 @@ They will be added to the OpenAPI schema and used by the automatic documentation +### Tags with Enums + +If you have a big application, you might end up accumulating **several tags**, and you would want to make sure you always use the **same tag** for related *path operations*. + +In these cases, it could make sense to store the tags in an `Enum`. + +**FastAPI** supports that the same way as with plain strings: + +```Python hl_lines="1 8-10 13 18" +{!../../../docs_src/path_operation_configuration/tutorial002b.py!} +``` + ## Summary and description You can add a `summary` and `description`: diff --git a/docs_src/path_operation_configuration/tutorial002b.py b/docs_src/path_operation_configuration/tutorial002b.py new file mode 100644 index 000000000..d53b4d817 --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial002b.py @@ -0,0 +1,20 @@ +from enum import Enum + +from fastapi import FastAPI + +app = FastAPI() + + +class Tags(Enum): + items = "items" + users = "users" + + +@app.get("/items/", tags=[Tags.items]) +async def get_items(): + return ["Portal gun", "Plumbus"] + + +@app.get("/users/", tags=[Tags.users]) +async def read_users(): + return ["Rick", "Morty"] diff --git a/fastapi/applications.py b/fastapi/applications.py index d71d4190a..dbfd76fb9 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import Any, Callable, Coroutine, Dict, List, Optional, Sequence, Type, Union from fastapi import routing @@ -219,7 +220,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -273,7 +274,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -342,7 +343,7 @@ class FastAPI(Starlette): router: routing.APIRouter, *, prefix: str = "", - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, @@ -368,7 +369,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -419,7 +420,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -470,7 +471,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -521,7 +522,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -572,7 +573,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -623,7 +624,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -674,7 +675,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -725,7 +726,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, diff --git a/fastapi/routing.py b/fastapi/routing.py index 63ad72964..f6d5370d6 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1,9 +1,9 @@ import asyncio import dataclasses import email.message -import enum import inspect import json +from enum import Enum, IntEnum from typing import ( Any, Callable, @@ -305,7 +305,7 @@ class APIRoute(routing.Route): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -330,7 +330,7 @@ class APIRoute(routing.Route): openapi_extra: Optional[Dict[str, Any]] = None, ) -> None: # normalise enums e.g. http.HTTPStatus - if isinstance(status_code, enum.IntEnum): + if isinstance(status_code, IntEnum): status_code = int(status_code) self.path = path self.endpoint = endpoint @@ -438,7 +438,7 @@ class APIRouter(routing.Router): self, *, prefix: str = "", - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, default_response_class: Type[Response] = Default(JSONResponse), responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, @@ -466,7 +466,7 @@ class APIRouter(routing.Router): "/" ), "A path prefix must not end with '/', as the routes will start with '/'" self.prefix = prefix - self.tags: List[str] = tags or [] + self.tags: List[Union[str, Enum]] = tags or [] self.dependencies = list(dependencies or []) or [] self.deprecated = deprecated self.include_in_schema = include_in_schema @@ -483,7 +483,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -557,7 +557,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -634,7 +634,7 @@ class APIRouter(routing.Router): router: "APIRouter", *, prefix: str = "", - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, default_response_class: Type[Response] = Default(JSONResponse), responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, @@ -738,7 +738,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -790,7 +790,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -842,7 +842,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -894,7 +894,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -946,7 +946,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -998,7 +998,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -1050,7 +1050,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -1102,7 +1102,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py new file mode 100644 index 000000000..be9f2afec --- /dev/null +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py @@ -0,0 +1,56 @@ +from fastapi.testclient import TestClient + +from docs_src.path_operation_configuration.tutorial002b import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "tags": ["items"], + "summary": "Get Items", + "operationId": "get_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_get_items(): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == ["Portal gun", "Plumbus"] + + +def test_get_users(): + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == ["Rick", "Morty"]