diff --git a/src/fastapi_cli/cli.py b/src/fastapi_cli/cli.py index d5bcb8e..bde723e 100644 --- a/src/fastapi_cli/cli.py +++ b/src/fastapi_cli/cli.py @@ -1,8 +1,11 @@ +import importlib from logging import getLogger from pathlib import Path from typing import Any, Union import typer +import uvicorn +from fastapi import FastAPI from rich import print from rich.padding import Padding from rich.panel import Panel @@ -49,6 +52,13 @@ def callback( """ +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) + return fastapi_app.docs_url + + def _run( path: Union[Path, None] = None, *, @@ -66,7 +76,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..3e3b0ce 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -221,3 +221,173 @@ 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 "│ 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 + + +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 "│ 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