From fb3ee8e94cc533e425a874ab7142e0ed43d6c709 Mon Sep 17 00:00:00 2001 From: Dino Aguilar Date: Mon, 13 May 2024 15:16:07 -0300 Subject: [PATCH 1/4] feat: fetch app docs_url --- src/fastapi_cli/cli.py | 23 +++++- tests/assets/with_docs_url_set.py | 8 ++ tests/assets/without_docs_url_none.py | 8 ++ tests/assets/without_docs_url_set.py | 8 ++ tests/test_cli.py | 114 +++++++++++++++++++++++++- 5 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 tests/assets/with_docs_url_set.py create mode 100644 tests/assets/without_docs_url_none.py create mode 100644 tests/assets/without_docs_url_set.py diff --git a/src/fastapi_cli/cli.py b/src/fastapi_cli/cli.py index 2aea254..87e7bce 100644 --- a/src/fastapi_cli/cli.py +++ b/src/fastapi_cli/cli.py @@ -1,6 +1,7 @@ +import importlib from logging import getLogger from pathlib import Path -from typing import Any, Union +from typing import Any, Final, Union import typer import uvicorn @@ -15,6 +16,8 @@ from . import __version__ from .logging import setup_logging +DEFAULT_DOCS_URL: Final[str] = "/docs" + app = typer.Typer(rich_markup_mode="rich") setup_logging() @@ -45,6 +48,13 @@ def callback( """ +def _get_docs_url(uvicorn_path: str) -> str: + module_path, app_name = uvicorn_path.split(sep=":") + module = importlib.import_module(module_path) + app = getattr(module, app_name) + return app.docs_url + + def _run( path: Union[Path, None] = None, *, @@ -62,7 +72,16 @@ def _run( except FastAPICLIException as e: logger.error(str(e)) raise typer.Exit(code=1) from None - serving_str = f"[dim]Serving at:[/dim] [link]http://{host}:{port}[/link]\n\n[dim]API docs:[/dim] [link]http://{host}:{port}/docs[/link]" + + docs_url = _get_docs_url(use_uvicorn_app) + + api_docs_string = ( + f"API docs:[/dim] [link]http://{host}:{port}{docs_url}[/link]" + if docs_url + else "" + ) + + serving_str = f"[dim]Serving at:[/dim] [link]http://{host}:{port}[/link]\n\n[dim]{api_docs_string}" if command == "dev": panel = Panel( diff --git a/tests/assets/with_docs_url_set.py b/tests/assets/with_docs_url_set.py new file mode 100644 index 0000000..3aa5e37 --- /dev/null +++ b/tests/assets/with_docs_url_set.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI + +app = FastAPI(docs_url="/any-other-path") + + +@app.get("/") +def api_root(): + return {"message": "any message"} diff --git a/tests/assets/without_docs_url_none.py b/tests/assets/without_docs_url_none.py new file mode 100644 index 0000000..f78560a --- /dev/null +++ b/tests/assets/without_docs_url_none.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI + +app = FastAPI(docs_url=None) + + +@app.get("/") +def api_root(): + return {"message": "any message"} diff --git a/tests/assets/without_docs_url_set.py b/tests/assets/without_docs_url_set.py new file mode 100644 index 0000000..3e1bca3 --- /dev/null +++ b/tests/assets/without_docs_url_set.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def api_root(): + return {"message": "any message"} diff --git a/tests/test_cli.py b/tests/test_cli.py index 44c14d2..92f926e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,7 +4,7 @@ from unittest.mock import patch import uvicorn -from fastapi_cli.cli import app +from fastapi_cli.cli import DEFAULT_DOCS_URL, app from typer.testing import CliRunner from tests.utils import changing_dir @@ -221,3 +221,115 @@ def test_script() -> None: encoding="utf-8", ) assert "Usage" in result.stdout + + +def test_dev_and_fastapi_app_with_docs_url_set_should_show_correctly_url_in_stdout() -> ( + None +): + with changing_dir(assets_path): + with patch.object(uvicorn, "run") as mock_run: + result = runner.invoke(app, ["dev", "with_docs_url_set.py"]) + assert result.exit_code == 0, result.output + assert mock_run.called + assert mock_run.call_args + assert mock_run.call_args.kwargs == { + "app": "with_docs_url_set:app", + "host": "127.0.0.1", + "port": 8000, + "reload": True, + "workers": None, + "root_path": "", + "proxy_headers": True, + } + assert "Using import string with_docs_url_set:app" in result.output + assert ( + "╭────────── FastAPI CLI - Development mode ───────────╮" in result.output + ) + assert "│ Serving at: http://127.0.0.1:8000" in result.output + assert "│ API docs: http://127.0.0.1:8000/any-other-path" in result.output + assert "│ Running in development mode, for production use:" in result.output + assert "│ fastapi run" in result.output + + +def test_dev_and_fastapi_app_without_docs_url_set_should_show_default_url_in_stdout() -> ( + None +): + with changing_dir(assets_path): + with patch.object(uvicorn, "run") as mock_run: + result = runner.invoke(app, ["dev", "without_docs_url_set.py"]) + assert result.exit_code == 0, result.output + assert mock_run.called + assert mock_run.call_args + assert mock_run.call_args.kwargs == { + "app": "without_docs_url_set:app", + "host": "127.0.0.1", + "port": 8000, + "reload": True, + "workers": None, + "root_path": "", + "proxy_headers": True, + } + assert "Using import string without_docs_url_set:app" in result.output + assert ( + "╭────────── FastAPI CLI - Development mode ───────────╮" in result.output + ) + assert "│ Serving at: http://127.0.0.1:8000" in result.output + assert f"│ API docs: http://127.0.0.1:8000{DEFAULT_DOCS_URL}" in result.output + assert "│ Running in development mode, for production use:" in result.output + assert "│ fastapi run" in result.output + + +def test_run_and_fastapi_app_with_docs_url_set_should_show_correctly_url_in_stdout() -> ( + None +): + with changing_dir(assets_path): + with patch.object(uvicorn, "run") as mock_run: + result = runner.invoke(app, ["run", "with_docs_url_set.py"]) + assert result.exit_code == 0, result.output + assert mock_run.called + assert mock_run.call_args + assert mock_run.call_args.kwargs == { + "app": "with_docs_url_set:app", + "host": "0.0.0.0", + "port": 8000, + "reload": False, + "workers": None, + "root_path": "", + "proxy_headers": True, + } + assert "Using import string with_docs_url_set:app" in result.output + assert ( + "╭─────────── FastAPI CLI - Production mode ───────────╮" in result.output + ) + assert "│ Serving at: http://0.0.0.0:8000" in result.output + assert "│ API docs: http://0.0.0.0:8000/any-other-path" in result.output + assert "│ Running in production mode, for development use:" in result.output + assert "│ fastapi dev" in result.output + + +def test_run_and_fastapi_app_without_docs_url_set_should_show_default_url_in_stdout() -> ( + None +): + with changing_dir(assets_path): + with patch.object(uvicorn, "run") as mock_run: + result = runner.invoke(app, ["run", "without_docs_url_set.py"]) + assert result.exit_code == 0, result.output + assert mock_run.called + assert mock_run.call_args + assert mock_run.call_args.kwargs == { + "app": "without_docs_url_set:app", + "host": "0.0.0.0", + "port": 8000, + "reload": False, + "workers": None, + "root_path": "", + "proxy_headers": True, + } + assert "Using import string without_docs_url_set:app" in result.output + assert ( + "╭─────────── FastAPI CLI - Production mode ───────────╮" in result.output + ) + assert "│ Serving at: http://0.0.0.0:8000" in result.output + assert f"│ API docs: http://0.0.0.0:8000{DEFAULT_DOCS_URL}" in result.output + assert "│ Running in production mode, for development use:" in result.output + assert "│ fastapi dev" in result.output From 5617ed452ea251edc18ea9302a81bff2c6d4c1c5 Mon Sep 17 00:00:00 2001 From: Dino Aguilar Date: Wed, 15 May 2024 14:31:36 -0300 Subject: [PATCH 2/4] feat: remove unused var DEFAULT_DOCS_URL --- src/fastapi_cli/cli.py | 4 +-- tests/test_cli.py | 64 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/fastapi_cli/cli.py b/src/fastapi_cli/cli.py index 87e7bce..9564443 100644 --- a/src/fastapi_cli/cli.py +++ b/src/fastapi_cli/cli.py @@ -1,7 +1,7 @@ import importlib from logging import getLogger from pathlib import Path -from typing import Any, Final, Union +from typing import Any, Union import typer import uvicorn @@ -16,8 +16,6 @@ from . import __version__ from .logging import setup_logging -DEFAULT_DOCS_URL: Final[str] = "/docs" - app = typer.Typer(rich_markup_mode="rich") setup_logging() diff --git a/tests/test_cli.py b/tests/test_cli.py index 92f926e..3e3b0ce 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,7 +4,7 @@ from unittest.mock import patch import uvicorn -from fastapi_cli.cli import DEFAULT_DOCS_URL, app +from fastapi_cli.cli import app from typer.testing import CliRunner from tests.utils import changing_dir @@ -274,7 +274,7 @@ def test_dev_and_fastapi_app_without_docs_url_set_should_show_default_url_in_std "╭────────── FastAPI CLI - Development mode ───────────╮" in result.output ) assert "│ Serving at: http://127.0.0.1:8000" in result.output - assert f"│ API docs: http://127.0.0.1:8000{DEFAULT_DOCS_URL}" in result.output + assert "│ API docs: http://127.0.0.1:8000/docs" in result.output assert "│ Running in development mode, for production use:" in result.output assert "│ fastapi run" in result.output @@ -330,6 +330,64 @@ def test_run_and_fastapi_app_without_docs_url_set_should_show_default_url_in_std "╭─────────── FastAPI CLI - Production mode ───────────╮" in result.output ) assert "│ Serving at: http://0.0.0.0:8000" in result.output - assert f"│ API docs: http://0.0.0.0:8000{DEFAULT_DOCS_URL}" in result.output + assert "│ API docs: http://0.0.0.0:8000/docs" in result.output + assert "│ Running in production mode, for development use:" in result.output + assert "│ fastapi dev" in result.output + + +def test_run_and_fastapi_app_docs_url_set_to_none_should_not_show_api_docs_section() -> ( + None +): + with changing_dir(assets_path): + with patch.object(uvicorn, "run") as mock_run: + result = runner.invoke(app, ["run", "without_docs_url_none.py"]) + assert result.exit_code == 0, result.output + assert mock_run.called + assert mock_run.call_args + assert mock_run.call_args.kwargs == { + "app": "without_docs_url_none:app", + "host": "0.0.0.0", + "port": 8000, + "reload": False, + "workers": None, + "root_path": "", + "proxy_headers": True, + } + assert "Using import string without_docs_url_none:app" in result.output + assert ( + "╭─────────── FastAPI CLI - Production mode ───────────╮" in result.output + ) + assert "│ Serving at: http://0.0.0.0:8000" in result.output assert "│ Running in production mode, for development use:" in result.output assert "│ fastapi dev" in result.output + + assert "│ API docs" not in result.output + + +def test_dev_and_fastapi_app_docs_url_set_to_none_should_not_show_api_docs_section() -> ( + None +): + with changing_dir(assets_path): + with patch.object(uvicorn, "run") as mock_run: + result = runner.invoke(app, ["dev", "without_docs_url_none.py"]) + assert result.exit_code == 0, result.output + assert mock_run.called + assert mock_run.call_args + assert mock_run.call_args.kwargs == { + "app": "without_docs_url_none:app", + "host": "127.0.0.1", + "port": 8000, + "reload": True, + "workers": None, + "root_path": "", + "proxy_headers": True, + } + assert "Using import string without_docs_url_none:app" in result.output + assert ( + "╭────────── FastAPI CLI - Development mode ───────────" + ) in result.output + assert "│ Serving at: http://127.0.0.1:8000" in result.output + assert "│ Running in development mode, for production use:" in result.output + assert "│ fastapi run" in result.output + + assert "│ API docs" not in result.output From 87263384d40af0d2e61aaa058903b6e12cc8865e Mon Sep 17 00:00:00 2001 From: Dino Aguilar Date: Wed, 15 May 2024 14:41:14 -0300 Subject: [PATCH 3/4] style: linter --- src/fastapi_cli/cli.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/fastapi_cli/cli.py b/src/fastapi_cli/cli.py index 9564443..a5e07b0 100644 --- a/src/fastapi_cli/cli.py +++ b/src/fastapi_cli/cli.py @@ -5,6 +5,7 @@ import typer import uvicorn +from fastapi import FastAPI from rich import print from rich.padding import Padding from rich.panel import Panel @@ -46,11 +47,11 @@ def callback( """ -def _get_docs_url(uvicorn_path: str) -> str: +def _get_docs_url(uvicorn_path: str) -> str | None: module_path, app_name = uvicorn_path.split(sep=":") module = importlib.import_module(module_path) - app = getattr(module, app_name) - return app.docs_url + fastapi_app: FastAPI = getattr(module, app_name) + return fastapi_app.docs_url def _run( From b078a937f4dd01771cbcf8f6844283a22f85dc4e Mon Sep 17 00:00:00 2001 From: Dino Aguilar Date: Wed, 15 May 2024 14:42:51 -0300 Subject: [PATCH 4/4] style: use Union --- src/fastapi_cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fastapi_cli/cli.py b/src/fastapi_cli/cli.py index a5e07b0..43c57bb 100644 --- a/src/fastapi_cli/cli.py +++ b/src/fastapi_cli/cli.py @@ -47,7 +47,7 @@ def callback( """ -def _get_docs_url(uvicorn_path: str) -> str | None: +def _get_docs_url(uvicorn_path: str) -> Union[str, None]: module_path, app_name = uvicorn_path.split(sep=":") module = importlib.import_module(module_path) fastapi_app: FastAPI = getattr(module, app_name)