From 1253a6291393618b12077fb0f86c3445e45e8d42 Mon Sep 17 00:00:00 2001 From: zumuta Date: Fri, 22 Nov 2024 20:48:14 +0100 Subject: [PATCH] feat: add fastapi template and api extra field --- CHANGELOG.md | 5 ++ README.md | 3 +- pyproject.toml | 5 +- src/griffe_fastapi/__init__.py | 11 +++- src/griffe_fastapi/_extension.py | 12 +++- .../material/_base/fastapi.html.jinja | 59 +++++++++++++++++++ .../templates/material/fastapi.html.jinja | 1 + tests/test_extension.py | 17 +++--- 8 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 src/griffe_fastapi/templates/material/_base/fastapi.html.jinja create mode 100644 src/griffe_fastapi/templates/material/fastapi.html.jinja diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d9ef2b..aa56f67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project will be documented in this file. +## [0.1.4](https://github.com/fbraem/griffe-fastapi/releases/tag/0.1.2) - 2024-11-22 + +- Feat: add api to the extra dictionary +- Feat: add fastapi template + ## [0.1.3](https://github.com/fbraem/griffe-fastapi/releases/tag/0.1.2) - 2024-11-19 - Fix generated table in docstring. diff --git a/README.md b/README.md index 4d6b4fd..902e43c 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,14 @@ Griffe FastAPI Extension This extension will search for functions that are decorated with an APIRouter and adds the following extra fields to a function: ++ api: the path of the api + method: the HTTP method + responses: A dictionary with the responses These fields are stored in the extra property of the function. The extra property is a dictionary and `griffe_fastapi` is the key for the fields of this extension. -Create a custom function template to handle these extra fields in your documentation. +A fastapi template is used to handle these extra fields in your documentation. Installation ------------ diff --git a/pyproject.toml b/pyproject.toml index da87b5e..4068ba4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "griffe-fastapi" -version = "0.1.3" +version = "0.1.4" description = "Griffe extension for FastAPI." authors = ["fbraem "] readme = "README.md" @@ -24,3 +24,6 @@ black = "^24.10.0" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[project.entry-points."mkdocstrings.python.templates"] +griffe-fastapi = "griffe_fastapi:get_templates_path" diff --git a/src/griffe_fastapi/__init__.py b/src/griffe_fastapi/__init__.py index a6c9568..86d489a 100644 --- a/src/griffe_fastapi/__init__.py +++ b/src/griffe_fastapi/__init__.py @@ -3,6 +3,15 @@ Griffe extension for FastAPI. """ +from pathlib import Path + from griffe_fastapi._extension import FastAPIExtension -__all__: list[str] = ["FastAPIExtension"] + +def get_templates_path() -> Path: + """Return the templates directory path.""" + print("Hey!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + return Path(__file__).parent / "templates" + + +__all__: list[str] = ["get_templates_path", "FastAPIExtension"] diff --git a/src/griffe_fastapi/_extension.py b/src/griffe_fastapi/_extension.py index 361038f..f233468 100644 --- a/src/griffe_fastapi/_extension.py +++ b/src/griffe_fastapi/_extension.py @@ -1,7 +1,7 @@ """griffe_fastapi extension.""" import ast -from typing import Any +from typing import Any, cast from griffe import ( Decorator, Docstring, @@ -116,13 +116,21 @@ def on_function_instance( if decorator is None: return + decorator_expr: ExprCall = cast(ExprCall, decorator.value) + func.extra[self_namespace] = {"method": decorator.value.canonical_name} + func.extra["mkdocstrings"]["template"] = "fastapi.html.jinja" + + if len(decorator_expr.arguments) > 0: + func.extra[self_namespace]["api"] = ast.literal_eval( + decorator_expr.arguments[0] + ) # Search the "responses" keyword in the arguments of the function. responses = next( ( x - for x in decorator.value.arguments + for x in decorator_expr if isinstance(x, ExprKeyword) and x.name == "responses" ), None, diff --git a/src/griffe_fastapi/templates/material/_base/fastapi.html.jinja b/src/griffe_fastapi/templates/material/_base/fastapi.html.jinja new file mode 100644 index 0000000..cc32323 --- /dev/null +++ b/src/griffe_fastapi/templates/material/_base/fastapi.html.jinja @@ -0,0 +1,59 @@ +{#- Template for Python FastAPI. + +This template renders a Python function that is decorated with a FastAPI APIRouter. + +Context: + function (griffe.Function): The function that implements the api to render. + root (bool): Whether this is the root object, injected with `:::` in a Markdown page. + heading_level (int): The HTML heading level to use. + config (dict): The configuration options. +-#} + +{% block logs scoped %} + {#- Logging block. + + This block can be used to log debug messages, deprecation messages, warnings, etc. + -#} + {{ log.debug("Rendering " + function.path) }} +{% endblock logs %} + +{% import "language"|get_template as lang with context %} +{#- Language module providing the `t` translation method. -#} + +
+ {% with obj = function, html_id = function.extra.griffe_fastapi.api %} + + {% set function_name = function.extra.griffe_fastapi.api %} + + {% filter heading( + heading_level, + role="api", + id=html_id, + class="doc doc-heading", + toc_label=((' ')|safe if config.show_symbol_type_toc else '') + function.name, + ) %} + {% endfilter %} + +
+ {% block contents scoped %} + {#- Contents block. + + This block renders the contents of the function. + It contains other blocks that users can override. + Overriding the contents block allows to rearrange the order of the blocks. + -#} + {% block docstring scoped %} + {#- Docstring block. + + This block renders the docstring for the function. + -#} + {% with docstring_sections = function.docstring.parsed %} + {% include "docstring"|get_template with context %} + {% endwith %} + {% endblock docstring %} + + {% endblock contents %} +
+ + {% endwith %} +
diff --git a/src/griffe_fastapi/templates/material/fastapi.html.jinja b/src/griffe_fastapi/templates/material/fastapi.html.jinja new file mode 100644 index 0000000..4b6483f --- /dev/null +++ b/src/griffe_fastapi/templates/material/fastapi.html.jinja @@ -0,0 +1 @@ +{% extends "_base/fastapi.html.jinja" %} diff --git a/tests/test_extension.py b/tests/test_extension.py index 87d424e..6a851dc 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -10,7 +10,7 @@ def test_extension() -> None: router = APIRouter() - @router.get("/", responses={200:{"description": "Ok"}}) + @router.get("/teams", responses={200:{"description": "Ok"}}) def get_teams() -> list[str]: '''Get the teams. ''' @@ -28,6 +28,7 @@ def get_teams() -> list[str]: assert "griffe_fastapi" in package.functions["get_teams"].extra extra = package.functions["get_teams"].extra["griffe_fastapi"] assert extra["method"] == "get" + assert extra["api"] == "/teams" assert extra["responses"]["200"]["description"] == "Ok" @@ -37,7 +38,7 @@ def test_extension_with_constant() -> None: router = APIRouter() - @router.get("/", responses={status.HTTP_200_OK:{"description": "Ok"}}) + @router.get("/teams", responses={status.HTTP_200_OK:{"description": "Ok"}}) def get_teams() -> list[str]: '''Get the teams.''' return [] @@ -63,7 +64,7 @@ def test_extension_with_multiple_responses() -> None: router = APIRouter() - @router.get("/", responses={ + @router.get("/teams", responses={ 200:{"description": "Ok"}, 404:{"description": "Not found"} }) @@ -93,7 +94,7 @@ def test_extension_with_a_response_with_headers() -> None: router = APIRouter() - @router.get("/", responses={ + @router.get("/teams", responses={ 200:{"description": "Ok", "content": {"image/png": {}}}, 404:{"description": "Not found"} }) @@ -129,7 +130,7 @@ def test_extension_with_a_dict() -> None: 404: {"description": "Not Found"}, } - @router.get("/", responses={**responses}) + @router.get("/teams", responses={**responses}) def get_teams() -> list[str]: '''Get the teams.''' return [] @@ -160,7 +161,7 @@ def test_extension_mixed() -> None: 200: {"description": "Ok"} } - @router.get("/", responses={404: {"description": "Not Found"}, **responses}) + @router.get("/teams", responses={404: {"description": "Not Found"}, **responses}) def get_teams() -> list[str]: '''Get the teams.''' return [] @@ -187,7 +188,7 @@ def test_with_paths() -> None: router = APIRouter() - @router.get("/", responses={200:{"description": "Ok"}}) + @router.get("/teams", responses={200:{"description": "Ok"}}) def get_teams() -> list[str]: '''Get the teams.''' return [] @@ -211,7 +212,7 @@ def test_extension_with_table() -> None: router = APIRouter() - @router.get("/", responses={200:{"description": "Ok"}}) + @router.get("/teams", responses={200:{"description": "Ok"}}) def get_teams() -> list[str]: '''Get the teams. '''