diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3289dd0..c3d1561 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.0 + rev: v0.4.9 hooks: - id: ruff args: diff --git a/src/fastapi_cli/cli.py b/src/fastapi_cli/cli.py index d5bcb8e..03522a7 100644 --- a/src/fastapi_cli/cli.py +++ b/src/fastapi_cli/cli.py @@ -62,7 +62,7 @@ def _run( proxy_headers: bool = False, ) -> None: try: - use_uvicorn_app = get_import_string(path=path, app_name=app) + use_uvicorn_app, is_factory = get_import_string(path=path, app_name=app) except FastAPICLIException as e: logger.error(str(e)) raise typer.Exit(code=1) from None @@ -97,6 +97,7 @@ def _run( workers=workers, root_path=root_path, proxy_headers=proxy_headers, + factory=is_factory, ) diff --git a/src/fastapi_cli/discover.py b/src/fastapi_cli/discover.py index f442438..a835406 100644 --- a/src/fastapi_cli/discover.py +++ b/src/fastapi_cli/discover.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from logging import getLogger from pathlib import Path -from typing import Union +from typing import Any, Callable, Tuple, Union, get_type_hints from rich import print from rich.padding import Padding @@ -98,7 +98,9 @@ def get_module_data_from_path(path: Path) -> ModuleData: ) -def get_app_name(*, mod_data: ModuleData, app_name: Union[str, None] = None) -> str: +def get_app_name( + *, mod_data: ModuleData, app_name: Union[str, None] = None +) -> Tuple[str, bool]: try: mod = importlib.import_module(mod_data.module_import_str) except (ImportError, ValueError) as e: @@ -119,26 +121,38 @@ def get_app_name(*, mod_data: ModuleData, app_name: Union[str, None] = None) -> f"Could not find app name {app_name} in {mod_data.module_import_str}" ) app = getattr(mod, app_name) + is_factory = False if not isinstance(app, FastAPI): - raise FastAPICLIException( - f"The app name {app_name} in {mod_data.module_import_str} doesn't seem to be a FastAPI app" - ) - return app_name - for preferred_name in ["app", "api"]: + is_factory = check_factory(app) + if not is_factory: + raise FastAPICLIException( + f"The app name {app_name} in {mod_data.module_import_str} doesn't seem to be a FastAPI app" + ) + return app_name, is_factory + for preferred_name in ["app", "api", "create_app", "create_api"]: if preferred_name in object_names_set: obj = getattr(mod, preferred_name) if isinstance(obj, FastAPI): - return preferred_name + return preferred_name, False + if check_factory(obj): + return preferred_name, True for name in object_names: obj = getattr(mod, name) if isinstance(obj, FastAPI): - return name + return name, False raise FastAPICLIException("Could not find FastAPI app in module, try using --app") +def check_factory(fn: Callable[[], Any]) -> bool: + """Checks whether the return-type of a factory function is FastAPI""" + type_hints = get_type_hints(fn) + return_type = type_hints.get("return") + return return_type is not None and issubclass(return_type, FastAPI) + + def get_import_string( *, path: Union[Path, None] = None, app_name: Union[str, None] = None -) -> str: +) -> Tuple[str, bool]: if not path: path = get_default_path() logger.info(f"Using path [blue]{path}[/blue]") @@ -147,7 +161,7 @@ def get_import_string( raise FastAPICLIException(f"Path does not exist {path}") mod_data = get_module_data_from_path(path) sys.path.insert(0, str(mod_data.extra_sys_path)) - use_app_name = get_app_name(mod_data=mod_data, app_name=app_name) + use_app_name, is_factory = get_app_name(mod_data=mod_data, app_name=app_name) import_example = Syntax( f"from {mod_data.module_import_str} import {use_app_name}", "python" ) @@ -164,4 +178,4 @@ def get_import_string( print(import_panel) import_string = f"{mod_data.module_import_str}:{use_app_name}" logger.info(f"Using import string [b green]{import_string}[/b green]") - return import_string + return import_string, is_factory diff --git a/tests/assets/factory_create_api.py b/tests/assets/factory_create_api.py new file mode 100644 index 0000000..f477471 --- /dev/null +++ b/tests/assets/factory_create_api.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI + + +def create_api() -> FastAPI: + app = FastAPI() + + @app.get("/") + def app_root(): + return {"message": "single file factory app"} + + return app diff --git a/tests/assets/factory_create_app.py b/tests/assets/factory_create_app.py new file mode 100644 index 0000000..9980d5b --- /dev/null +++ b/tests/assets/factory_create_app.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI + + +class App(FastAPI): ... + + +def create_app_other() -> App: + app = App() + + @app.get("/") + def app_root(): + return {"message": "single file factory app inherited"} + + return app + + +def create_app() -> FastAPI: + app = FastAPI() + + @app.get("/") + def app_root(): + return {"message": "single file factory app"} + + return app diff --git a/tests/assets/package/mod/factory_api.py b/tests/assets/package/mod/factory_api.py new file mode 100644 index 0000000..e303195 --- /dev/null +++ b/tests/assets/package/mod/factory_api.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI + + +def create_api() -> FastAPI: + app = FastAPI() + + @app.get("/") + def root(): + return {"message": "package create_api"} + + return app diff --git a/tests/assets/package/mod/factory_app.py b/tests/assets/package/mod/factory_app.py new file mode 100644 index 0000000..a652b6e --- /dev/null +++ b/tests/assets/package/mod/factory_app.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI + + +def create_app() -> FastAPI: + app = FastAPI() + + @app.get("/") + def root(): + return {"message": "package create_app"} + + return app diff --git a/tests/assets/package/mod/factory_inherit.py b/tests/assets/package/mod/factory_inherit.py new file mode 100644 index 0000000..590afdb --- /dev/null +++ b/tests/assets/package/mod/factory_inherit.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI + + +class App(FastAPI): ... + + +def create_app() -> App: + app = App() + + @app.get("/") + def root(): + return {"message": "package build_app"} + + return app diff --git a/tests/assets/package/mod/factory_other.py b/tests/assets/package/mod/factory_other.py new file mode 100644 index 0000000..ccd0f79 --- /dev/null +++ b/tests/assets/package/mod/factory_other.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI + + +def build_app() -> FastAPI: + app = FastAPI() + + @app.get("/") + def root(): + return {"message": "package build_app"} + + return app diff --git a/tests/test_cli.py b/tests/test_cli.py index 44c14d2..b64330e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -29,6 +29,7 @@ def test_dev() -> None: "workers": None, "root_path": "", "proxy_headers": True, + "factory": False, } assert "Using import string single_file_app:app" in result.output assert ( @@ -40,6 +41,33 @@ def test_dev() -> None: assert "│ fastapi run" in result.output +def test_dev_factory() -> None: + with changing_dir(assets_path): + with patch.object(uvicorn, "run") as mock_run: + result = runner.invoke(app, ["dev", "factory_create_app.py"]) + assert result.exit_code == 0, result.output + assert mock_run.called + assert mock_run.call_args + assert mock_run.call_args.kwargs == { + "app": "factory_create_app:create_app", + "host": "127.0.0.1", + "port": 8000, + "reload": True, + "workers": None, + "root_path": "", + "proxy_headers": True, + "factory": True, + } + assert "Using import string factory_create_app:create_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_dev_args() -> None: with changing_dir(assets_path): with patch.object(uvicorn, "run") as mock_run: @@ -71,6 +99,7 @@ def test_dev_args() -> None: "workers": None, "root_path": "/api", "proxy_headers": False, + "factory": False, } assert "Using import string single_file_app:api" in result.output assert ( @@ -97,6 +126,7 @@ def test_run() -> None: "workers": None, "root_path": "", "proxy_headers": True, + "factory": False, } assert "Using import string single_file_app:app" in result.output assert ( @@ -108,6 +138,33 @@ def test_run() -> None: assert "│ fastapi dev" in result.output +def test_run_factory() -> None: + with changing_dir(assets_path): + with patch.object(uvicorn, "run") as mock_run: + result = runner.invoke(app, ["run", "factory_create_app.py"]) + assert result.exit_code == 0, result.output + assert mock_run.called + assert mock_run.call_args + assert mock_run.call_args.kwargs == { + "app": "factory_create_app:create_app", + "host": "0.0.0.0", + "port": 8000, + "reload": False, + "workers": None, + "root_path": "", + "proxy_headers": True, + "factory": True, + } + assert "Using import string factory_create_app:create_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_args() -> None: with changing_dir(assets_path): with patch.object(uvicorn, "run") as mock_run: @@ -141,6 +198,7 @@ def test_run_args() -> None: "workers": 2, "root_path": "/api", "proxy_headers": False, + "factory": False, } assert "Using import string single_file_app:api" in result.output assert ( diff --git a/tests/test_utils_check_factory.py b/tests/test_utils_check_factory.py new file mode 100644 index 0000000..966d84c --- /dev/null +++ b/tests/test_utils_check_factory.py @@ -0,0 +1,32 @@ +from fastapi import FastAPI +from fastapi_cli.discover import check_factory + + +def test_check_untyped_factory() -> None: + def create_app(): # type: ignore[no-untyped-def] + return FastAPI() # pragma: no cover + + assert check_factory(create_app) is False + + +def test_check_typed_factory() -> None: + def create_app() -> FastAPI: + return FastAPI() # pragma: no cover + + assert check_factory(create_app) is True + + +def test_check_typed_factory_inherited() -> None: + class MyApp(FastAPI): ... + + def create_app() -> MyApp: + return MyApp() # pragma: no cover + + assert check_factory(create_app) is True + + +def test_create_app_with_different_type() -> None: + def create_app() -> int: + return 1 # pragma: no cover + + assert check_factory(create_app) is False diff --git a/tests/test_utils_default_dir.py b/tests/test_utils_default_dir.py index 2665203..09a3a4c 100644 --- a/tests/test_utils_default_dir.py +++ b/tests/test_utils_default_dir.py @@ -12,8 +12,9 @@ def test_app_dir_main(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path / "default_files" / "default_app_dir_main"): - import_string = get_import_string() + import_string, is_factory = get_import_string() assert import_string == "app.main:app" + assert is_factory is False captured = capsys.readouterr() assert "Using path app/main.py" in captured.out @@ -36,8 +37,9 @@ def test_app_dir_main(capsys: CaptureFixture[str]) -> None: def test_app_dir_app(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path / "default_files" / "default_app_dir_app"): - import_string = get_import_string() + import_string, is_factory = get_import_string() assert import_string == "app.app:app" + assert is_factory is False captured = capsys.readouterr() assert "Using path app/app.py" in captured.out @@ -58,8 +60,9 @@ def test_app_dir_app(capsys: CaptureFixture[str]) -> None: def test_app_dir_api(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path / "default_files" / "default_app_dir_api"): - import_string = get_import_string() + import_string, is_factory = get_import_string() assert import_string == "app.api:app" + assert is_factory is False captured = capsys.readouterr() assert "Using path app/api.py" in captured.out diff --git a/tests/test_utils_default_file.py b/tests/test_utils_default_file.py index f5c87c8..5e67d0b 100644 --- a/tests/test_utils_default_file.py +++ b/tests/test_utils_default_file.py @@ -20,8 +20,9 @@ def test_single_file_main(capsys: CaptureFixture[str]) -> None: mod = importlib.import_module("main") importlib.reload(mod) - import_string = get_import_string() + import_string, is_factory = get_import_string() assert import_string == "main:app" + assert is_factory is False captured = capsys.readouterr() assert "Using path main.py" in captured.out @@ -47,8 +48,9 @@ def test_single_file_app(capsys: CaptureFixture[str]) -> None: mod = importlib.import_module("app") importlib.reload(mod) - import_string = get_import_string() + import_string, is_factory = get_import_string() assert import_string == "app:app" + assert is_factory is False captured = capsys.readouterr() assert "Using path app.py" in captured.out @@ -74,8 +76,9 @@ def test_single_file_api(capsys: CaptureFixture[str]) -> None: mod = importlib.import_module("api") importlib.reload(mod) - import_string = get_import_string() + import_string, is_factory = get_import_string() assert import_string == "api:app" + assert is_factory is False captured = capsys.readouterr() assert "Using path api.py" in captured.out diff --git a/tests/test_utils_package.py b/tests/test_utils_package.py index d5573db..a66d012 100644 --- a/tests/test_utils_package.py +++ b/tests/test_utils_package.py @@ -12,8 +12,9 @@ def test_package_app_root(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path): - import_string = get_import_string(path=Path("package/mod/app.py")) + import_string, is_factory = get_import_string(path=Path("package/mod/app.py")) assert import_string == "package.mod.app:app" + assert is_factory is False captured = capsys.readouterr() assert "Using path package/mod/app.py" in captured.out @@ -40,8 +41,9 @@ def test_package_app_root(capsys: CaptureFixture[str]) -> None: def test_package_api_root(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path): - import_string = get_import_string(path=Path("package/mod/api.py")) + import_string, is_factory = get_import_string(path=Path("package/mod/api.py")) assert import_string == "package.mod.api:api" + assert is_factory is False captured = capsys.readouterr() assert "Using path package/mod/api.py" in captured.out @@ -68,8 +70,9 @@ def test_package_api_root(capsys: CaptureFixture[str]) -> None: def test_package_other_root(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path): - import_string = get_import_string(path=Path("package/mod/other.py")) + import_string, is_factory = get_import_string(path=Path("package/mod/other.py")) assert import_string == "package.mod.other:first_other" + assert is_factory is False captured = capsys.readouterr() assert "Using path package/mod/other.py" in captured.out @@ -94,10 +97,135 @@ def test_package_other_root(capsys: CaptureFixture[str]) -> None: assert "Using import string package.mod.other:first_other" in captured.out +def test_package_factory_app_root(capsys: CaptureFixture[str]) -> None: + with changing_dir(assets_path): + import_string, is_factory = get_import_string( + path=Path("package/mod/factory_app.py") + ) + assert import_string == "package.mod.factory_app:create_app" + assert is_factory is True + + captured = capsys.readouterr() + assert "Using path package/mod/factory_app.py" in captured.out + assert "Resolved absolute path" in captured.out + assert "tests/assets/package/mod/factory_app.py" in captured.out + assert ( + "Searching for package file structure from directories with __init__.py files" + in captured.out + ) + assert "Importing from" in captured.out + assert "tests/assets" in captured.out + assert "╭─ Python package file structure ─╮" in captured.out + assert "│ 📁 package" in captured.out + assert "│ ├── 🐍 __init__.py" in captured.out + assert "│ └── 📁 mod" in captured.out + assert "│ ├── 🐍 __init__.py " in captured.out + assert "│ └── 🐍 factory_app.py" in captured.out + assert "Importing module package.mod.factory_app" in captured.out + assert "Found importable FastAPI app" in captured.out + assert "Importable FastAPI app" in captured.out + assert "from package.mod.factory_app import create_app" in captured.out + assert "Using import string package.mod.factory_app:create_app" in captured.out + + +def test_package_factory_api_root(capsys: CaptureFixture[str]) -> None: + with changing_dir(assets_path): + import_string, is_factory = get_import_string( + path=Path("package/mod/factory_api.py") + ) + assert import_string == "package.mod.factory_api:create_api" + assert is_factory is True + + captured = capsys.readouterr() + assert "Using path package/mod/factory_api.py" in captured.out + assert "Resolved absolute path" in captured.out + assert "tests/assets/package/mod/factory_api.py" in captured.out + assert ( + "Searching for package file structure from directories with __init__.py files" + in captured.out + ) + assert "Importing from" in captured.out + assert "tests/assets" in captured.out + assert "╭─ Python package file structure ─╮" in captured.out + assert "│ 📁 package" in captured.out + assert "│ ├── 🐍 __init__.py" in captured.out + assert "│ └── 📁 mod" in captured.out + assert "│ ├── 🐍 __init__.py " in captured.out + assert "│ └── 🐍 factory_api.py" in captured.out + assert "Importing module package.mod.factory_api" in captured.out + assert "Found importable FastAPI app" in captured.out + assert "Importable FastAPI app" in captured.out + assert "from package.mod.factory_api import create_api" in captured.out + assert "Using import string package.mod.factory_api:create_api" in captured.out + + +def test_package_factory_other_root(capsys: CaptureFixture[str]) -> None: + with changing_dir(assets_path): + import_string, is_factory = get_import_string( + path=Path("package/mod/factory_other.py"), app_name="build_app" + ) + assert import_string == "package.mod.factory_other:build_app" + assert is_factory is True + + captured = capsys.readouterr() + assert "Using path package/mod/factory_other.py" in captured.out + assert "Resolved absolute path" in captured.out + assert "tests/assets/package/mod/factory_other.py" in captured.out + assert ( + "Searching for package file structure from directories with __init__.py files" + in captured.out + ) + assert "Importing from" in captured.out + assert "tests/assets" in captured.out + assert "╭─ Python package file structure ─╮" in captured.out + assert "│ 📁 package" in captured.out + assert "│ ├── 🐍 __init__.py" in captured.out + assert "│ └── 📁 mod" in captured.out + assert "│ ├── 🐍 __init__.py " in captured.out + assert "│ └── 🐍 factory_other.py" in captured.out + assert "Importing module package.mod.factory_other" in captured.out + assert "Found importable FastAPI app" in captured.out + assert "Importable FastAPI app" in captured.out + assert "from package.mod.factory_other import build_app" in captured.out + assert "Using import string package.mod.factory_other:build_app" in captured.out + + +def test_package_factory_inherit_root(capsys: CaptureFixture[str]) -> None: + with changing_dir(assets_path): + import_string, is_factory = get_import_string( + path=Path("package/mod/factory_inherit.py") + ) + assert import_string == "package.mod.factory_inherit:create_app" + assert is_factory is True + + captured = capsys.readouterr() + assert "Using path package/mod/factory_inherit.py" in captured.out + assert "Resolved absolute path" in captured.out + assert "tests/assets/package/mod/factory_inherit.py" in captured.out + assert ( + "Searching for package file structure from directories with __init__.py files" + in captured.out + ) + assert "Importing from" in captured.out + assert "tests/assets" in captured.out + assert "╭─ Python package file structure ─╮" in captured.out + assert "│ 📁 package" in captured.out + assert "│ ├── 🐍 __init__.py" in captured.out + assert "│ └── 📁 mod" in captured.out + assert "│ ├── 🐍 __init__.py " in captured.out + assert "│ └── 🐍 factory_inherit.py" in captured.out + assert "Importing module package.mod.factory_inherit" in captured.out + assert "Found importable FastAPI app" in captured.out + assert "Importable FastAPI app" in captured.out + assert "from package.mod.factory_inherit import create_app" in captured.out + assert "Using import string package.mod.factory_inherit:create_app" in captured.out + + def test_package_app_mod(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path / "package/mod"): - import_string = get_import_string(path=Path("app.py")) + import_string, is_factory = get_import_string(path=Path("app.py")) assert import_string == "package.mod.app:app" + assert is_factory is False captured = capsys.readouterr() assert "Using path app.py" in captured.out @@ -124,8 +252,9 @@ def test_package_app_mod(capsys: CaptureFixture[str]) -> None: def test_package_api_mod(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path / "package/mod"): - import_string = get_import_string(path=Path("api.py")) + import_string, is_factory = get_import_string(path=Path("api.py")) assert import_string == "package.mod.api:api" + assert is_factory is False captured = capsys.readouterr() assert "Using path api.py" in captured.out @@ -152,8 +281,9 @@ def test_package_api_mod(capsys: CaptureFixture[str]) -> None: def test_package_other_mod(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path / "package/mod"): - import_string = get_import_string(path=Path("other.py")) + import_string, is_factory = get_import_string(path=Path("other.py")) assert import_string == "package.mod.other:first_other" + assert is_factory is False captured = capsys.readouterr() assert "Using path other.py" in captured.out @@ -178,10 +308,131 @@ def test_package_other_mod(capsys: CaptureFixture[str]) -> None: assert "Using import string package.mod.other:first_other" in captured.out -def test_package_app_above(capsys: CaptureFixture[str]) -> None: +def test_package_factory_app_mod(capsys: CaptureFixture[str]) -> None: + with changing_dir(assets_path / "package/mod"): + import_string, is_factory = get_import_string(path=Path("factory_app.py")) + assert import_string == "package.mod.factory_app:create_app" + assert is_factory is True + + captured = capsys.readouterr() + assert "Using path factory_app.py" in captured.out + assert "Resolved absolute path" in captured.out + assert "tests/assets/package/mod/factory_app.py" in captured.out + assert ( + "Searching for package file structure from directories with __init__.py files" + in captured.out + ) + assert "Importing from" in captured.out + assert "tests/assets" in captured.out + assert "╭─ Python package file structure ─╮" in captured.out + assert "│ 📁 package" in captured.out + assert "│ ├── 🐍 __init__.py" in captured.out + assert "│ └── 📁 mod" in captured.out + assert "│ ├── 🐍 __init__.py " in captured.out + assert "│ └── 🐍 factory_app.py" in captured.out + assert "Importing module package.mod.factory_app" in captured.out + assert "Found importable FastAPI app" in captured.out + assert "Importable FastAPI app" in captured.out + assert "from package.mod.factory_app import create_app" in captured.out + assert "Using import string package.mod.factory_app:create_app" in captured.out + + +def test_package_factory_api_mod(capsys: CaptureFixture[str]) -> None: + with changing_dir(assets_path / "package/mod"): + import_string, is_factory = get_import_string(path=Path("factory_api.py")) + assert import_string == "package.mod.factory_api:create_api" + assert is_factory is True + + captured = capsys.readouterr() + assert "Using path factory_api.py" in captured.out + assert "Resolved absolute path" in captured.out + assert "tests/assets/package/mod/factory_api.py" in captured.out + assert ( + "Searching for package file structure from directories with __init__.py files" + in captured.out + ) + assert "Importing from" in captured.out + assert "tests/assets" in captured.out + assert "╭─ Python package file structure ─╮" in captured.out + assert "│ 📁 package" in captured.out + assert "│ ├── 🐍 __init__.py" in captured.out + assert "│ └── 📁 mod" in captured.out + assert "│ ├── 🐍 __init__.py " in captured.out + assert "│ └── 🐍 factory_api.py" in captured.out + assert "Importing module package.mod.factory_api" in captured.out + assert "Found importable FastAPI app" in captured.out + assert "Importable FastAPI app" in captured.out + assert "from package.mod.factory_api import create_api" in captured.out + assert "Using import string package.mod.factory_api:create_api" in captured.out + + +def test_package_factory_other_mod(capsys: CaptureFixture[str]) -> None: + with changing_dir(assets_path / "package/mod"): + import_string, is_factory = get_import_string( + path=Path("factory_other.py"), app_name="build_app" + ) + assert import_string == "package.mod.factory_other:build_app" + assert is_factory is True + + captured = capsys.readouterr() + assert "Using path factory_other.py" in captured.out + assert "Resolved absolute path" in captured.out + assert "tests/assets/package/mod/factory_other.py" in captured.out + assert ( + "Searching for package file structure from directories with __init__.py files" + in captured.out + ) + assert "Importing from" in captured.out + assert "tests/assets" in captured.out + assert "╭─ Python package file structure ─╮" in captured.out + assert "│ 📁 package" in captured.out + assert "│ ├── 🐍 __init__.py" in captured.out + assert "│ └── 📁 mod" in captured.out + assert "│ ├── 🐍 __init__.py " in captured.out + assert "│ └── 🐍 factory_other.py" in captured.out + assert "Importing module package.mod.factory_other" in captured.out + assert "Found importable FastAPI app" in captured.out + assert "Importable FastAPI app" in captured.out + assert "from package.mod.factory_other import build_app" in captured.out + assert "Using import string package.mod.factory_other:build_app" in captured.out + + +def test_package_factory_inherit_mod(capsys: CaptureFixture[str]) -> None: + with changing_dir(assets_path / "package/mod"): + import_string, is_factory = get_import_string(path=Path("factory_inherit.py")) + assert import_string == "package.mod.factory_inherit:create_app" + assert is_factory is True + + captured = capsys.readouterr() + assert "Using path factory_inherit.py" in captured.out + assert "Resolved absolute path" in captured.out + assert "tests/assets/package/mod/factory_inherit.py" in captured.out + assert ( + "Searching for package file structure from directories with __init__.py files" + in captured.out + ) + assert "Importing from" in captured.out + assert "tests/assets" in captured.out + assert "╭─ Python package file structure ─╮" in captured.out + assert "│ 📁 package" in captured.out + assert "│ ├── 🐍 __init__.py" in captured.out + assert "│ └── 📁 mod" in captured.out + assert "│ ├── 🐍 __init__.py " in captured.out + assert "│ └── 🐍 factory_inherit.py" in captured.out + assert "Importing module package.mod.factory_inherit" in captured.out + assert "Found importable FastAPI app" in captured.out + assert "Importable FastAPI app" in captured.out + assert "from package.mod.factory_inherit import create_app" in captured.out + assert "Using import string package.mod.factory_inherit:create_app" in captured.out + + +def test_package_app_parent(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path.parent): - import_string = get_import_string(path=Path("assets/package/mod/app.py")) + import_string, is_factory = get_import_string( + path=Path("assets/package/mod/app.py") + ) assert import_string == "package.mod.app:app" + assert is_factory is False captured = capsys.readouterr() assert "Using path assets/package/mod/app.py" in captured.out @@ -208,8 +459,11 @@ def test_package_app_above(capsys: CaptureFixture[str]) -> None: def test_package_api_parent(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path.parent): - import_string = get_import_string(path=Path("assets/package/mod/api.py")) + import_string, is_factory = get_import_string( + path=Path("assets/package/mod/api.py") + ) assert import_string == "package.mod.api:api" + assert is_factory is False captured = capsys.readouterr() assert "Using path assets/package/mod/api.py" in captured.out @@ -236,8 +490,11 @@ def test_package_api_parent(capsys: CaptureFixture[str]) -> None: def test_package_other_parent(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path.parent): - import_string = get_import_string(path=Path("assets/package/mod/other.py")) + import_string, is_factory = get_import_string( + path=Path("assets/package/mod/other.py") + ) assert import_string == "package.mod.other:first_other" + assert is_factory is False captured = capsys.readouterr() assert "Using path assets/package/mod/other.py" in captured.out @@ -262,10 +519,136 @@ def test_package_other_parent(capsys: CaptureFixture[str]) -> None: assert "Using import string package.mod.other:first_other" in captured.out +def test_package_factory_app_parent(capsys: CaptureFixture[str]) -> None: + with changing_dir(assets_path.parent): + import_string, is_factory = get_import_string( + path=Path("assets/package/mod/factory_app.py") + ) + assert import_string == "package.mod.factory_app:create_app" + assert is_factory is True + + captured = capsys.readouterr() + assert "Using path assets/package/mod/factory_app.py" in captured.out + assert "Resolved absolute path" in captured.out + assert "tests/assets/package/mod/factory_app.py" in captured.out + assert ( + "Searching for package file structure from directories with __init__.py files" + in captured.out + ) + assert "Importing from" in captured.out + assert "tests/assets" in captured.out + assert "╭─ Python package file structure ─╮" in captured.out + assert "│ 📁 package" in captured.out + assert "│ ├── 🐍 __init__.py" in captured.out + assert "│ └── 📁 mod" in captured.out + assert "│ ├── 🐍 __init__.py " in captured.out + assert "│ └── 🐍 factory_app.py" in captured.out + assert "Importing module package.mod.factory_app" in captured.out + assert "Found importable FastAPI app" in captured.out + assert "Importable FastAPI app" in captured.out + assert "from package.mod.factory_app import create_app" in captured.out + assert "Using import string package.mod.factory_app:create_app" in captured.out + + +def test_package_factory_api_parent(capsys: CaptureFixture[str]) -> None: + with changing_dir(assets_path.parent): + import_string, is_factory = get_import_string( + path=Path("assets/package/mod/factory_api.py") + ) + assert import_string == "package.mod.factory_api:create_api" + assert is_factory is True + + captured = capsys.readouterr() + assert "Using path assets/package/mod/factory_api.py" in captured.out + assert "Resolved absolute path" in captured.out + assert "tests/assets/package/mod/factory_api.py" in captured.out + assert ( + "Searching for package file structure from directories with __init__.py files" + in captured.out + ) + assert "Importing from" in captured.out + assert "tests/assets" in captured.out + assert "╭─ Python package file structure ─╮" in captured.out + assert "│ 📁 package" in captured.out + assert "│ ├── 🐍 __init__.py" in captured.out + assert "│ └── 📁 mod" in captured.out + assert "│ ├── 🐍 __init__.py " in captured.out + assert "│ └── 🐍 factory_api.py" in captured.out + assert "Importing module package.mod.factory_api" in captured.out + assert "Found importable FastAPI app" in captured.out + assert "Importable FastAPI app" in captured.out + assert "from package.mod.factory_api import create_api" in captured.out + assert "Using import string package.mod.factory_api:create_api" in captured.out + + +def test_package_factory_other_parent(capsys: CaptureFixture[str]) -> None: + with changing_dir(assets_path.parent): + import_string, is_factory = get_import_string( + path=Path("assets/package/mod/factory_other.py"), + app_name="build_app", + ) + assert import_string == "package.mod.factory_other:build_app" + assert is_factory is True + + captured = capsys.readouterr() + assert "Using path assets/package/mod/factory_other.py" in captured.out + assert "Resolved absolute path" in captured.out + assert "tests/assets/package/mod/factory_other.py" in captured.out + assert ( + "Searching for package file structure from directories with __init__.py files" + in captured.out + ) + assert "Importing from" in captured.out + assert "tests/assets" in captured.out + assert "╭─ Python package file structure ─╮" in captured.out + assert "│ 📁 package" in captured.out + assert "│ ├── 🐍 __init__.py" in captured.out + assert "│ └── 📁 mod" in captured.out + assert "│ ├── 🐍 __init__.py " in captured.out + assert "│ └── 🐍 factory_other.py" in captured.out + assert "Importing module package.mod.factory_other" in captured.out + assert "Found importable FastAPI app" in captured.out + assert "Importable FastAPI app" in captured.out + assert "from package.mod.factory_other import build_app" in captured.out + assert "Using import string package.mod.factory_other:build_app" in captured.out + + +def test_package_factory_inherit_parent(capsys: CaptureFixture[str]) -> None: + with changing_dir(assets_path.parent): + import_string, is_factory = get_import_string( + path=Path("assets/package/mod/factory_inherit.py") + ) + assert import_string == "package.mod.factory_inherit:create_app" + assert is_factory is True + + captured = capsys.readouterr() + assert "Using path assets/package/mod/factory_inherit.py" in captured.out + assert "Resolved absolute path" in captured.out + assert "tests/assets/package/mod/factory_inherit.py" in captured.out + assert ( + "Searching for package file structure from directories with __init__.py files" + in captured.out + ) + assert "Importing from" in captured.out + assert "tests/assets" in captured.out + assert "╭─ Python package file structure ─╮" in captured.out + assert "│ 📁 package" in captured.out + assert "│ ├── 🐍 __init__.py" in captured.out + assert "│ └── 📁 mod" in captured.out + assert "│ ├── 🐍 __init__.py " in captured.out + assert "│ └── 🐍 factory_inherit.py" in captured.out + assert "Importing module package.mod.factory_inherit" in captured.out + assert "Found importable FastAPI app" in captured.out + assert "Importable FastAPI app" in captured.out + assert "from package.mod.factory_inherit import create_app" in captured.out + assert "Using import string package.mod.factory_inherit:create_app" in captured.out + + def test_package_mod_init_inside(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path / "package/mod"): - import_string = get_import_string(path=Path("__init__.py")) + import_string, is_factory = get_import_string(path=Path("__init__.py")) assert import_string == "package.mod:app" + assert is_factory is False captured = capsys.readouterr() assert "Using path __init__.py" in captured.out @@ -291,8 +674,9 @@ def test_package_mod_init_inside(capsys: CaptureFixture[str]) -> None: def test_package_mod_dir(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path): - import_string = get_import_string(path=Path("package/mod")) + import_string, is_factory = get_import_string(path=Path("package/mod")) assert import_string == "package.mod:app" + assert is_factory is False captured = capsys.readouterr() assert "Using path package/mod" in captured.out @@ -318,8 +702,9 @@ def test_package_mod_dir(capsys: CaptureFixture[str]) -> None: def test_package_init_inside(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path / "package"): - import_string = get_import_string(path=Path("__init__.py")) + import_string, is_factory = get_import_string(path=Path("__init__.py")) assert import_string == "package:app" + assert is_factory is False captured = capsys.readouterr() assert "Using path __init__.py" in captured.out @@ -343,8 +728,9 @@ def test_package_init_inside(capsys: CaptureFixture[str]) -> None: def test_package_dir_inside_package(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path / "package/mod"): - import_string = get_import_string(path=Path("../")) + import_string, is_factory = get_import_string(path=Path("../")) assert import_string == "package:app" + assert is_factory is False captured = capsys.readouterr() assert "Using path .." in captured.out @@ -368,8 +754,9 @@ def test_package_dir_inside_package(capsys: CaptureFixture[str]) -> None: def test_package_dir_above_package(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path.parent): - import_string = get_import_string(path=Path("assets/package")) + import_string, is_factory = get_import_string(path=Path("assets/package")) assert import_string == "package:app" + assert is_factory is False captured = capsys.readouterr() assert "Using path assets/package" in captured.out @@ -393,8 +780,11 @@ def test_package_dir_above_package(capsys: CaptureFixture[str]) -> None: def test_package_dir_explicit_app(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path): - import_string = get_import_string(path=Path("package"), app_name="api") + import_string, is_factory = get_import_string( + path=Path("package"), app_name="api" + ) assert import_string == "package:api" + assert is_factory is False captured = capsys.readouterr() assert "Using path package" in captured.out diff --git a/tests/test_utils_single_file.py b/tests/test_utils_single_file.py index 6395b32..a8fc768 100644 --- a/tests/test_utils_single_file.py +++ b/tests/test_utils_single_file.py @@ -12,8 +12,9 @@ def test_single_file_app(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path): - import_string = get_import_string(path=Path("single_file_app.py")) + import_string, is_factory = get_import_string(path=Path("single_file_app.py")) assert import_string == "single_file_app:app" + assert is_factory is False captured = capsys.readouterr() assert "Using path single_file_app.py" in captured.out @@ -36,8 +37,9 @@ def test_single_file_app(capsys: CaptureFixture[str]) -> None: def test_single_file_api(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path): - import_string = get_import_string(path=Path("single_file_api.py")) + import_string, is_factory = get_import_string(path=Path("single_file_api.py")) assert import_string == "single_file_api:api" + assert is_factory is False captured = capsys.readouterr() assert "Using path single_file_api.py" in captured.out @@ -60,8 +62,9 @@ def test_single_file_api(capsys: CaptureFixture[str]) -> None: def test_single_file_other(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path): - import_string = get_import_string(path=Path("single_file_other.py")) + import_string, is_factory = get_import_string(path=Path("single_file_other.py")) assert import_string == "single_file_other:first_other" + assert is_factory is False captured = capsys.readouterr() assert "Using path single_file_other.py" in captured.out @@ -84,10 +87,11 @@ def test_single_file_other(capsys: CaptureFixture[str]) -> None: def test_single_file_explicit_object(capsys: CaptureFixture[str]) -> None: with changing_dir(assets_path): - import_string = get_import_string( + import_string, is_factory = get_import_string( path=Path("single_file_app.py"), app_name="second_other" ) assert import_string == "single_file_app:second_other" + assert is_factory is False captured = capsys.readouterr() assert "Using path single_file_app.py" in captured.out @@ -108,6 +112,116 @@ def test_single_file_explicit_object(capsys: CaptureFixture[str]) -> None: assert "Using import string single_file_app:second_other" in captured.out +def test_single_file_create_app_factory_function(capsys: CaptureFixture[str]) -> None: + with changing_dir(assets_path): + import_string, is_factory = get_import_string( + path=Path("factory_create_app.py") + ) + assert import_string == "factory_create_app:create_app" + assert is_factory is True + + captured = capsys.readouterr() + assert "Using path factory_create_app.py" in captured.out + assert "Resolved absolute path" in captured.out + assert "tests/assets/factory_create_app.py" in captured.out + assert ( + "Searching for package file structure from directories with __init__.py files" + in captured.out + ) + assert "Importing from" in captured.out + assert "tests/assets" in captured.out + assert "╭──── Python module file ────╮" in captured.out + assert "│ 🐍 factory_create_app.py" in captured.out + assert "Importing module factory_create_app" in captured.out + assert "Found importable FastAPI app" in captured.out + assert "Importable FastAPI app" in captured.out + assert "from factory_create_app import create_app" in captured.out + assert "Using import string factory_create_app:create_app" in captured.out + + +def test_single_file_create_api_factory_function(capsys: CaptureFixture[str]) -> None: + with changing_dir(assets_path): + import_string, is_factory = get_import_string( + path=Path("factory_create_api.py") + ) + assert import_string == "factory_create_api:create_api" + assert is_factory is True + + captured = capsys.readouterr() + assert "Using path factory_create_api.py" in captured.out + assert "Resolved absolute path" in captured.out + assert "tests/assets/factory_create_api.py" in captured.out + assert ( + "Searching for package file structure from directories with __init__.py files" + in captured.out + ) + assert "Importing from" in captured.out + assert "tests/assets" in captured.out + assert "╭──── Python module file ────╮" in captured.out + assert "│ 🐍 factory_create_api.py" in captured.out + assert "Importing module factory_create_api" in captured.out + assert "Found importable FastAPI app" in captured.out + assert "Importable FastAPI app" in captured.out + assert "from factory_create_api import create_api" in captured.out + assert "Using import string factory_create_api:create_api" in captured.out + + +def test_single_file_explicit_factory_function(capsys: CaptureFixture[str]) -> None: + with changing_dir(assets_path): + import_string, is_factory = get_import_string( + path=Path("factory_create_app.py"), app_name="create_app" + ) + assert import_string == "factory_create_app:create_app" + assert is_factory is True + + captured = capsys.readouterr() + assert "Using path factory_create_app.py" in captured.out + assert "Resolved absolute path" in captured.out + assert "tests/assets/factory_create_app.py" in captured.out + assert ( + "Searching for package file structure from directories with __init__.py files" + in captured.out + ) + assert "Importing from" in captured.out + assert "tests/assets" in captured.out + assert "╭──── Python module file ────╮" in captured.out + assert "│ 🐍 factory_create_app.py" in captured.out + assert "Importing module factory_create_app" in captured.out + assert "Found importable FastAPI app" in captured.out + assert "Importable FastAPI app" in captured.out + assert "from factory_create_app import create_app" in captured.out + assert "Using import string factory_create_app:create_app" in captured.out + + +def test_single_file_explicit_factory_function_other( + capsys: CaptureFixture[str], +) -> None: + with changing_dir(assets_path): + import_string, is_factory = get_import_string( + path=Path("factory_create_app.py"), app_name="create_app_other" + ) + assert import_string == "factory_create_app:create_app_other" + assert is_factory is True + + captured = capsys.readouterr() + assert "Using path factory_create_app.py" in captured.out + assert "Resolved absolute path" in captured.out + assert "tests/assets/factory_create_app.py" in captured.out + assert ( + "Searching for package file structure from directories with __init__.py files" + in captured.out + ) + assert "Importing from" in captured.out + assert "tests/assets" in captured.out + assert "╭──── Python module file ────╮" in captured.out + assert "│ 🐍 factory_create_app.py" in captured.out + assert "Importing module factory_create_app" in captured.out + assert "Found importable FastAPI app" in captured.out + assert "Importable FastAPI app" in captured.out + assert "from factory_create_app import create_app_other" in captured.out + assert "Using import string factory_create_app:create_app_other" in captured.out + + def test_single_non_existing_file() -> None: with changing_dir(assets_path): with pytest.raises(FastAPICLIException) as e: