Skip to content

Commit 59fc428

Browse files
committed
Respect root_path in URLs
1 parent 4a7ea66 commit 59fc428

4 files changed

Lines changed: 96 additions & 4 deletions

File tree

src/fastapi_cli/cli.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
get_docs_urls,
1313
get_import_data,
1414
get_import_data_from_import_string,
15+
get_root_path,
1516
)
1617
from fastapi_cli.exceptions import FastAPICLIException
1718

@@ -196,6 +197,11 @@ def _run(
196197
docs_urls = get_docs_urls(import_data)
197198

198199
url = f"http://{host}:{port}"
200+
201+
use_root_path = root_path or get_root_path(import_data)
202+
if use_root_path:
203+
url += use_root_path
204+
199205
docs_url = ""
200206

201207
if docs_urls.openapi_url and (docs_urls.docs_url or docs_urls.redoc_url):

src/fastapi_cli/discover.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,15 @@ def get_docs_urls(import_data: ImportData) -> DocsURLs:
180180
docs_url=app.docs_url,
181181
redoc_url=app.redoc_url,
182182
)
183+
184+
185+
def get_root_path(import_data: ImportData) -> str:
186+
module = importlib.import_module(import_data.module_data.module_import_str)
187+
app_name = import_data.app_name
188+
app = getattr(module, app_name)
189+
190+
# Just for type checking, it's already checked in get_app_name
191+
assert FastAPI is not None
192+
assert isinstance(app, FastAPI)
193+
194+
return app.root_path

tests/assets/single_file_docs.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,25 @@ def custom_docs_root():
7575
@custom_redoc.get("/")
7676
def custom_redoc_root():
7777
return {"message": "single file custom_redoc"}
78+
79+
80+
# App 8: Swagger docs enabled, root_path set to "/api". ReDoc disabled.
81+
# ------------------------------------------------------------------------------------
82+
83+
docs_root_path = FastAPI(redoc_url=None, root_path="/api")
84+
85+
86+
@docs_root_path.get("/")
87+
def docs_root_path_root():
88+
return {"message": "single file docs_root_path"}
89+
90+
91+
# App 9: ReDoc enabled, root_path set to "/api". Swagger docs disabled.
92+
# ------------------------------------------------------------------------------------
93+
94+
redoc_root_path = FastAPI(docs_url=None, root_path="/api")
95+
96+
97+
@redoc_root_path.get("/")
98+
def redoc_root_path_root():
99+
return {"message": "single file redoc_root_path"}

tests/test_cli.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ def test_dev_args() -> None:
133133
}
134134
assert "Using import string: single_file_app:api" in result.output
135135
assert "Starting development server 🚀" in result.output
136-
assert "Server started at http://192.168.0.2:8080" in result.output
137-
assert "Documentation at http://192.168.0.2:8080/docs" in result.output
136+
assert "Server started at http://192.168.0.2:8080/api" in result.output
137+
assert "Documentation at http://192.168.0.2:8080/api/docs" in result.output
138138
assert (
139139
"Running in development mode, for production use: fastapi run"
140140
in result.output
@@ -323,8 +323,8 @@ def test_run_args() -> None:
323323

324324
assert "Using import string: single_file_app:api" in result.output
325325
assert "Starting production server 🚀" in result.output
326-
assert "Server started at http://192.168.0.2:8080" in result.output
327-
assert "Documentation at http://192.168.0.2:8080/docs" in result.output
326+
assert "Server started at http://192.168.0.2:8080/api" in result.output
327+
assert "Documentation at http://192.168.0.2:8080/api/docs" in result.output
328328
assert (
329329
"Running in development mode, for production use: fastapi run"
330330
not in result.output
@@ -475,6 +475,58 @@ def test_docs_urls_custom_redoc() -> None:
475475
assert "http://127.0.0.1:8000/custom-redoc-url" in result.output
476476

477477

478+
@pytest.mark.parametrize(
479+
("app_name", "expected_url"),
480+
[
481+
("only_docs", "http://127.0.0.1:8000/api/docs"),
482+
("only_redoc", "http://127.0.0.1:8000/api/redoc"),
483+
],
484+
)
485+
def test_docs_urls_root_path_option(app_name: str, expected_url: str) -> None:
486+
with changing_dir(assets_path):
487+
with patch.object(uvicorn, "run") as mock_run:
488+
result = runner.invoke(
489+
app,
490+
[
491+
"dev",
492+
"single_file_docs.py",
493+
"--app",
494+
app_name,
495+
"--root-path",
496+
"/api",
497+
],
498+
)
499+
assert result.exit_code == 0, result.output
500+
assert mock_run.called
501+
502+
assert expected_url in result.output
503+
504+
505+
@pytest.mark.parametrize(
506+
("app_name", "expected_url"),
507+
[
508+
("docs_root_path", "http://127.0.0.1:8000/api/docs"),
509+
("redoc_root_path", "http://127.0.0.1:8000/api/redoc"),
510+
],
511+
)
512+
def test_docs_urls_root_path_param(app_name: str, expected_url: str) -> None:
513+
with changing_dir(assets_path):
514+
with patch.object(uvicorn, "run") as mock_run:
515+
result = runner.invoke(
516+
app,
517+
[
518+
"dev",
519+
"single_file_docs.py",
520+
"--app",
521+
app_name,
522+
],
523+
)
524+
assert result.exit_code == 0, result.output
525+
assert mock_run.called
526+
527+
assert expected_url in result.output
528+
529+
478530
def test_run_error() -> None:
479531
with changing_dir(assets_path):
480532
result = runner.invoke(app, ["run", "non_existing_file.py"])

0 commit comments

Comments
 (0)