Skip to content

Commit 03ea860

Browse files
committed
Split docs_url discovery from import_data discovery
1 parent edca1c7 commit 03ea860

6 files changed

Lines changed: 79 additions & 68 deletions

File tree

src/fastapi_cli/cli.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
from rich.tree import Tree
99

1010
from fastapi_cli.config import FastAPIConfig
11-
from fastapi_cli.discover import get_import_data, get_import_data_from_import_string
11+
from fastapi_cli.discover import (
12+
get_docs_urls,
13+
get_import_data,
14+
get_import_data_from_import_string,
15+
)
1216
from fastapi_cli.exceptions import FastAPICLIException
1317

1418
from . import __version__
@@ -164,9 +168,6 @@ def _run(
164168

165169
module_data = import_data.module_data
166170
import_string = import_data.import_string
167-
openapi_url = import_data.openapi_url
168-
docs_url = import_data.docs_url
169-
redoc_url = import_data.redoc_url
170171

171172
toolkit.print(f"Importing from {module_data.extra_sys_path}")
172173
toolkit.print_line()
@@ -192,28 +193,30 @@ def _run(
192193
tag="app",
193194
)
194195

195-
url = f"http://{host}:{port}"
196-
docs_str = ""
197-
198-
if openapi_url and (docs_url or redoc_url):
199-
if docs_url:
200-
docs_str += f"[link={url}{docs_url}]{url}{docs_url}[/]"
196+
docs_urls = get_docs_urls(import_data)
201197

202-
if docs_url and redoc_url:
203-
docs_str += " or "
198+
url = f"http://{host}:{port}"
199+
docs_url = ""
204200

205-
if redoc_url:
206-
docs_str += f"[link={url}{redoc_url}]{url}{redoc_url}[/]"
201+
if docs_urls.openapi_url and (docs_urls.docs_url or docs_urls.redoc_url):
202+
if docs_urls.docs_url:
203+
docs_url = (
204+
f"[link={url}{docs_urls.docs_url}]{url}{docs_urls.docs_url}[/]"
205+
)
206+
else:
207+
docs_url = (
208+
f"[link={url}{docs_urls.redoc_url}]{url}{docs_urls.redoc_url}[/]"
209+
)
207210

208211
toolkit.print_line()
209212
toolkit.print(
210213
f"Server started at [link={url}]{url}[/]",
211214
tag="server",
212215
)
213216

214-
if docs_str:
217+
if docs_url:
215218
toolkit.print(
216-
f"Documentation at {docs_str}",
219+
f"Documentation at {docs_url}",
217220
tag="server",
218221
)
219222

src/fastapi_cli/discover.py

Lines changed: 34 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,7 @@ def get_module_data_from_path(path: Path) -> ModuleData:
6969
)
7070

7171

72-
def get_app_infos(
73-
*, mod_data: ModuleData, app_name: str | None = None
74-
) -> tuple[str, str | None, str | None, str | None]:
72+
def get_app_name(*, mod_data: ModuleData, app_name: str | None = None) -> str:
7573
try:
7674
mod = importlib.import_module(mod_data.module_import_str)
7775
except (ImportError, ValueError) as e:
@@ -80,41 +78,32 @@ def get_app_infos(
8078
"Ensure all the package directories have an [blue]__init__.py[/blue] file"
8179
)
8280
raise
83-
8481
if not FastAPI: # type: ignore[truthy-function]
8582
raise FastAPICLIException(
8683
"Could not import FastAPI, try running 'pip install fastapi'"
8784
) from None
88-
8985
object_names = dir(mod)
9086
object_names_set = set(object_names)
91-
9287
if app_name:
9388
if app_name not in object_names_set:
9489
raise FastAPICLIException(
9590
f"Could not find app name {app_name} in {mod_data.module_import_str}"
9691
)
97-
9892
app = getattr(mod, app_name)
99-
10093
if not isinstance(app, FastAPI):
10194
raise FastAPICLIException(
10295
f"The app name {app_name} in {mod_data.module_import_str} doesn't seem to be a FastAPI app"
10396
)
104-
105-
return app_name, app.openapi_url, app.docs_url, app.redoc_url
106-
97+
return app_name
10798
for preferred_name in ["app", "api"]:
10899
if preferred_name in object_names_set:
109100
obj = getattr(mod, preferred_name)
110101
if isinstance(obj, FastAPI):
111-
return preferred_name, obj.openapi_url, obj.docs_url, obj.redoc_url
112-
102+
return preferred_name
113103
for name in object_names:
114104
obj = getattr(mod, name)
115105
if isinstance(obj, FastAPI):
116-
return name, obj.openapi_url, obj.docs_url, obj.redoc_url
117-
106+
return name
118107
raise FastAPICLIException("Could not find FastAPI app in module, try using --app")
119108

120109

@@ -123,9 +112,6 @@ class ImportData:
123112
app_name: str
124113
module_data: ModuleData
125114
import_string: str
126-
openapi_url: str | None = None
127-
docs_url: str | None = None
128-
redoc_url: str | None = None
129115

130116

131117
def get_import_data(
@@ -139,22 +125,14 @@ def get_import_data(
139125

140126
if not path.exists():
141127
raise FastAPICLIException(f"Path does not exist {path}")
142-
143128
mod_data = get_module_data_from_path(path)
144129
sys.path.insert(0, str(mod_data.extra_sys_path))
145-
use_app_name, openapi_url, docs_url, redoc_url = get_app_infos(
146-
mod_data=mod_data, app_name=app_name
147-
)
130+
use_app_name = get_app_name(mod_data=mod_data, app_name=app_name)
148131

149132
import_string = f"{mod_data.module_import_str}:{use_app_name}"
150133

151134
return ImportData(
152-
app_name=use_app_name,
153-
module_data=mod_data,
154-
import_string=import_string,
155-
openapi_url=openapi_url,
156-
docs_url=docs_url,
157-
redoc_url=redoc_url,
135+
app_name=use_app_name, module_data=mod_data, import_string=import_string
158136
)
159137

160138

@@ -170,21 +148,35 @@ def get_import_data_from_import_string(import_string: str) -> ImportData:
170148

171149
sys.path.insert(0, str(here))
172150

173-
module_data = ModuleData(
174-
module_import_str=module_str,
175-
extra_sys_path=here,
176-
module_paths=[],
177-
)
178-
179-
_, openapi_url, docs_url, redoc_url = get_app_infos(
180-
mod_data=module_data, app_name=app_name
181-
)
182-
183151
return ImportData(
184152
app_name=app_name,
185-
module_data=module_data,
153+
module_data=ModuleData(
154+
module_import_str=module_str,
155+
extra_sys_path=here,
156+
module_paths=[],
157+
),
186158
import_string=import_string,
187-
openapi_url=openapi_url,
188-
docs_url=docs_url,
189-
redoc_url=redoc_url,
159+
)
160+
161+
162+
@dataclass
163+
class DocsURLs:
164+
openapi_url: str | None
165+
docs_url: str | None
166+
redoc_url: str | None
167+
168+
169+
def get_docs_urls(import_data: ImportData) -> DocsURLs:
170+
module = importlib.import_module(import_data.module_data.module_import_str)
171+
app_name = import_data.app_name
172+
app = getattr(module, app_name)
173+
174+
# Just for type checking, it's already checked in get_app_name
175+
assert FastAPI is not None
176+
assert isinstance(app, FastAPI)
177+
178+
return DocsURLs(
179+
openapi_url=app.openapi_url,
180+
docs_url=app.docs_url,
181+
redoc_url=app.redoc_url,
190182
)

tests/assets/single_file_docs.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,17 @@ def full_docs_root():
4040
return {"message": "single file full_docs"}
4141

4242

43-
custom_docs = FastAPI(docs_url="/custom-docs-url", redoc_url="/custom-redoc-url")
43+
custom_docs = FastAPI(docs_url="/custom-docs-url")
4444

4545

4646
@custom_docs.get("/")
4747
def custom_docs_root():
4848
return {"message": "single file custom_docs"}
49+
50+
51+
custom_redoc = FastAPI(docs_url=None, redoc_url="/custom-redoc-url")
52+
53+
54+
@custom_redoc.get("/")
55+
def custom_redoc_root():
56+
return {"message": "single file custom_redoc"}

tests/test_cli.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ def test_full_docs() -> None:
453453
assert mock_run.called
454454

455455
assert "http://127.0.0.1:8000/docs" in result.output
456-
assert "http://127.0.0.1:8000/redoc" in result.output
456+
assert "http://127.0.0.1:8000/redoc" not in result.output # docs has precedence
457457

458458

459459
def test_custom_docs() -> None:
@@ -466,6 +466,17 @@ def test_custom_docs() -> None:
466466
assert mock_run.called
467467

468468
assert "http://127.0.0.1:8000/custom-docs-url" in result.output
469+
470+
471+
def test_custom_redoc() -> None:
472+
with changing_dir(assets_path):
473+
with patch.object(uvicorn, "run") as mock_run:
474+
result = runner.invoke(
475+
app, ["dev", "single_file_docs.py", "--app", "custom_redoc"]
476+
)
477+
assert result.exit_code == 0, result.output
478+
assert mock_run.called
479+
469480
assert "http://127.0.0.1:8000/custom-redoc-url" in result.output
470481

471482

tests/test_discover.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,18 @@
77
get_import_data_from_import_string,
88
)
99
from fastapi_cli.exceptions import FastAPICLIException
10-
from tests.utils import changing_dir
1110

1211
assets_path = Path(__file__).parent / "assets"
1312

1413

1514
def test_get_import_data_from_import_string_valid() -> None:
16-
with changing_dir(assets_path):
17-
result = get_import_data_from_import_string("package.mod.app:app")
15+
result = get_import_data_from_import_string("module.submodule:app")
1816

1917
assert isinstance(result, ImportData)
2018
assert result.app_name == "app"
21-
assert result.import_string == "package.mod.app:app"
22-
assert result.module_data.module_import_str == "package.mod.app"
23-
assert result.module_data.extra_sys_path == Path(assets_path).resolve()
19+
assert result.import_string == "module.submodule:app"
20+
assert result.module_data.module_import_str == "module.submodule"
21+
assert result.module_data.extra_sys_path == Path(".").resolve()
2422
assert result.module_data.module_paths == []
2523

2624

tests/utils.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
from collections.abc import Generator
33
from contextlib import contextmanager
44
from pathlib import Path
5-
from typing import Union
65

76

87
@contextmanager
9-
def changing_dir(directory: Union[str, Path]) -> Generator[None, None, None]:
8+
def changing_dir(directory: str | Path) -> Generator[None, None, None]:
109
initial_dir = os.getcwd()
1110
os.chdir(directory)
1211
try:

0 commit comments

Comments
 (0)