Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding factory argument #64

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions src/fastapi_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,12 @@ def _run(
command: str,
app: Union[str, None] = None,
proxy_headers: bool = False,
is_factory: bool = False,
) -> None:
try:
use_uvicorn_app = get_import_string(path=path, app_name=app)
use_uvicorn_app = get_import_string(
path=path, app_name=app, is_factory=is_factory
)
except FastAPICLIException as e:
logger.error(str(e))
raise typer.Exit(code=1) from None
Expand Down Expand Up @@ -97,6 +100,7 @@ def _run(
workers=workers,
root_path=root_path,
proxy_headers=proxy_headers,
factory=is_factory,
)


Expand All @@ -105,7 +109,7 @@ def dev(
path: Annotated[
Union[Path, None],
typer.Argument(
help="A path to a Python file or package directory (with [blue]__init__.py[/blue] files) containing a [bold]FastAPI[/bold] app. If not provided, a default set of paths will be tried."
help="A path to a Python file or package directory (with [blue]__init__.py[/blue] files) containing a [bold]FastAPI[/bold] app or app factory. If not provided, a default set of paths will be tried."
),
] = None,
*,
Expand Down Expand Up @@ -145,6 +149,12 @@ def dev(
help="Enable/Disable X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port to populate remote address info."
),
] = True,
factory: Annotated[
bool,
typer.Option(
help="Treat [bold]path[/bold] as an application factory, i.e. a () -> <ASGI app> callable."
),
] = False,
) -> Any:
"""
Run a [bold]FastAPI[/bold] app in [yellow]development[/yellow] mode. 🧪
Expand Down Expand Up @@ -180,6 +190,7 @@ def dev(
app=app,
command="dev",
proxy_headers=proxy_headers,
is_factory=factory,
)


Expand Down Expand Up @@ -234,6 +245,12 @@ def run(
help="Enable/Disable X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port to populate remote address info."
),
] = True,
factory: Annotated[
bool,
typer.Option(
help="Treat [bold]path[/bold] as an application factory, i.e. a () -> <ASGI app> callable."
),
] = False,
) -> Any:
"""
Run a [bold]FastAPI[/bold] app in [green]production[/green] mode. 🚀
Expand Down Expand Up @@ -270,6 +287,7 @@ def run(
app=app,
command="run",
proxy_headers=proxy_headers,
is_factory=factory,
)


Expand Down
30 changes: 23 additions & 7 deletions src/fastapi_cli/discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, is_factory: bool = False
) -> str:
try:
mod = importlib.import_module(mod_data.module_import_str)
except (ImportError, ValueError) as e:
Expand All @@ -119,25 +121,37 @@ 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)
if not isinstance(app, FastAPI):
if not isinstance(app, FastAPI) and 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"
)
else:
if not callable(app) and is_factory:
raise FastAPICLIException(
f"The app factory {app_name} in {mod_data.module_import_str} doesn't seem to be a function"
)
return app_name
for preferred_name in ["app", "api"]:
if preferred_name in object_names_set:
obj = getattr(mod, preferred_name)
if isinstance(obj, FastAPI):
if isinstance(obj, FastAPI) and not is_factory:
return preferred_name
for name in object_names:
obj = getattr(mod, name)
if isinstance(obj, FastAPI):
if isinstance(obj, FastAPI) and not is_factory:
return name
elif callable(name) and is_factory:
return name
raise FastAPICLIException("Could not find FastAPI app in module, try using --app")
raise FastAPICLIException(
"Could not find FastAPI app or app factory in module, try using --app"
)


def get_import_string(
*, path: Union[Path, None] = None, app_name: Union[str, None] = None
*,
path: Union[Path, None] = None,
app_name: Union[str, None] = None,
is_factory: bool = False,
) -> str:
if not path:
path = get_default_path()
Expand All @@ -147,7 +161,9 @@ 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 = get_app_name(
mod_data=mod_data, app_name=app_name, is_factory=is_factory
)
import_example = Syntax(
f"from {mod_data.module_import_str} import {use_app_name}", "python"
)
Expand Down
4 changes: 4 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -71,6 +72,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 (
Expand All @@ -97,6 +99,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 (
Expand Down Expand Up @@ -141,6 +144,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 (
Expand Down
3 changes: 2 additions & 1 deletion tests/test_utils_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,8 @@ def test_package_dir_no_app() -> None:
with pytest.raises(FastAPICLIException) as e:
get_import_string(path=Path("package/core/utils.py"))
assert (
"Could not find FastAPI app in module, try using --app" in e.value.args[0]
"Could not find FastAPI app or app factory in module, try using --app"
in e.value.args[0]
)


Expand Down