Skip to content

Commit dfec882

Browse files
committed
feat: fetch app docs_url
1 parent 35c0428 commit dfec882

File tree

4 files changed

+147
-3
lines changed

4 files changed

+147
-3
lines changed

src/fastapi_cli/cli.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import importlib
12
from logging import getLogger
23
from pathlib import Path
3-
from typing import Any, Union
4+
from typing import Any, Final, Union
45

56
import typer
67
import uvicorn
@@ -15,6 +16,8 @@
1516
from . import __version__
1617
from .logging import setup_logging
1718

19+
DEFAULT_DOCS_URL: Final[str] = "/docs"
20+
1821
app = typer.Typer(rich_markup_mode="rich")
1922

2023
setup_logging()
@@ -45,6 +48,13 @@ def callback(
4548
"""
4649

4750

51+
def _get_docs_url(uvicorn_path: str) -> str:
52+
module_path, app_name = uvicorn_path.split(sep=":")
53+
module = importlib.import_module(module_path)
54+
app = getattr(module, app_name)
55+
return app.docs_url or DEFAULT_DOCS_URL
56+
57+
4858
def _run(
4959
path: Union[Path, None] = None,
5060
*,
@@ -62,7 +72,13 @@ def _run(
6272
except FastAPICLIException as e:
6373
logger.error(str(e))
6474
raise typer.Exit(code=1) from None
65-
serving_str = f"[dim]Serving at:[/dim] [link]http://{host}:{port}[/link]\n\n[dim]API docs:[/dim] [link]http://{host}:{port}/docs[/link]"
75+
76+
docs_url = _get_docs_url(use_uvicorn_app)
77+
78+
serving_str = (
79+
f"[dim]Serving at:[/dim] [link]http://{host}:{port}[/link]\n\n[dim]"
80+
f"API docs:[/dim] [link]http://{host}:{port}{docs_url}[/link]"
81+
)
6682

6783
if command == "dev":
6884
panel = Panel(

tests/assets/with_docs_url_set.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from fastapi import FastAPI
2+
3+
app = FastAPI(docs_url="/any-other-path")
4+
5+
6+
@app.get("/")
7+
def api_root():
8+
return {"message": "any message"}

tests/assets/without_docs_url_set.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from fastapi import FastAPI
2+
3+
app = FastAPI()
4+
5+
6+
@app.get("/")
7+
def api_root():
8+
return {"message": "any message"}

tests/test_cli.py

+113-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from unittest.mock import patch
55

66
import uvicorn
7-
from fastapi_cli.cli import app
7+
from fastapi_cli.cli import DEFAULT_DOCS_URL, app
88
from typer.testing import CliRunner
99

1010
from tests.utils import changing_dir
@@ -221,3 +221,115 @@ def test_script() -> None:
221221
encoding="utf-8",
222222
)
223223
assert "Usage" in result.stdout
224+
225+
226+
def test_dev_and_fastapi_app_with_docs_url_set_should_show_correctly_url_in_stdout() -> (
227+
None
228+
):
229+
with changing_dir(assets_path):
230+
with patch.object(uvicorn, "run") as mock_run:
231+
result = runner.invoke(app, ["dev", "with_docs_url_set.py"])
232+
assert result.exit_code == 0, result.output
233+
assert mock_run.called
234+
assert mock_run.call_args
235+
assert mock_run.call_args.kwargs == {
236+
"app": "with_docs_url_set:app",
237+
"host": "127.0.0.1",
238+
"port": 8000,
239+
"reload": True,
240+
"workers": None,
241+
"root_path": "",
242+
"proxy_headers": True,
243+
}
244+
assert "Using import string with_docs_url_set:app" in result.output
245+
assert (
246+
"╭────────── FastAPI CLI - Development mode ───────────╮" in result.output
247+
)
248+
assert "│ Serving at: http://127.0.0.1:8000" in result.output
249+
assert "│ API docs: http://127.0.0.1:8000/any-other-path" in result.output
250+
assert "│ Running in development mode, for production use:" in result.output
251+
assert "│ fastapi run" in result.output
252+
253+
254+
def test_dev_and_fastapi_app_without_docs_url_set_should_show_default_url_in_stdout() -> (
255+
None
256+
):
257+
with changing_dir(assets_path):
258+
with patch.object(uvicorn, "run") as mock_run:
259+
result = runner.invoke(app, ["dev", "without_docs_url_set.py"])
260+
assert result.exit_code == 0, result.output
261+
assert mock_run.called
262+
assert mock_run.call_args
263+
assert mock_run.call_args.kwargs == {
264+
"app": "without_docs_url_set:app",
265+
"host": "127.0.0.1",
266+
"port": 8000,
267+
"reload": True,
268+
"workers": None,
269+
"root_path": "",
270+
"proxy_headers": True,
271+
}
272+
assert "Using import string without_docs_url_set:app" in result.output
273+
assert (
274+
"╭────────── FastAPI CLI - Development mode ───────────╮" in result.output
275+
)
276+
assert "│ Serving at: http://127.0.0.1:8000" in result.output
277+
assert f"│ API docs: http://127.0.0.1:8000{DEFAULT_DOCS_URL}" in result.output
278+
assert "│ Running in development mode, for production use:" in result.output
279+
assert "│ fastapi run" in result.output
280+
281+
282+
def test_run_and_fastapi_app_with_docs_url_set_should_show_correctly_url_in_stdout() -> (
283+
None
284+
):
285+
with changing_dir(assets_path):
286+
with patch.object(uvicorn, "run") as mock_run:
287+
result = runner.invoke(app, ["run", "with_docs_url_set.py"])
288+
assert result.exit_code == 0, result.output
289+
assert mock_run.called
290+
assert mock_run.call_args
291+
assert mock_run.call_args.kwargs == {
292+
"app": "with_docs_url_set:app",
293+
"host": "0.0.0.0",
294+
"port": 8000,
295+
"reload": False,
296+
"workers": None,
297+
"root_path": "",
298+
"proxy_headers": True,
299+
}
300+
assert "Using import string with_docs_url_set:app" in result.output
301+
assert (
302+
"╭─────────── FastAPI CLI - Production mode ───────────╮" in result.output
303+
)
304+
assert "│ Serving at: http://0.0.0.0:8000" in result.output
305+
assert "│ API docs: http://0.0.0.0:8000/any-other-path" in result.output
306+
assert "│ Running in production mode, for development use:" in result.output
307+
assert "│ fastapi dev" in result.output
308+
309+
310+
def test_run_and_fastapi_app_without_docs_url_set_should_show_default_url_in_stdout() -> (
311+
None
312+
):
313+
with changing_dir(assets_path):
314+
with patch.object(uvicorn, "run") as mock_run:
315+
result = runner.invoke(app, ["run", "without_docs_url_set.py"])
316+
assert result.exit_code == 0, result.output
317+
assert mock_run.called
318+
assert mock_run.call_args
319+
assert mock_run.call_args.kwargs == {
320+
"app": "without_docs_url_set:app",
321+
"host": "0.0.0.0",
322+
"port": 8000,
323+
"reload": False,
324+
"workers": None,
325+
"root_path": "",
326+
"proxy_headers": True,
327+
}
328+
assert "Using import string without_docs_url_set:app" in result.output
329+
assert (
330+
"╭─────────── FastAPI CLI - Production mode ───────────╮" in result.output
331+
)
332+
assert "│ Serving at: http://0.0.0.0:8000" in result.output
333+
assert f"│ API docs: http://0.0.0.0:8000{DEFAULT_DOCS_URL}" in result.output
334+
assert "│ Running in production mode, for development use:" in result.output
335+
assert "│ fastapi dev" in result.output

0 commit comments

Comments
 (0)