From 22a2c03a27a4e714c7df4f73a6f659a5ba2e3319 Mon Sep 17 00:00:00 2001 From: rasput Date: Sun, 11 Aug 2024 20:26:59 +0300 Subject: [PATCH 1/3] added factory support --- src/fastapi_cli/cli.py | 17 ++++++++++++++--- src/fastapi_cli/discover.py | 21 ++++++++++++++------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/fastapi_cli/cli.py b/src/fastapi_cli/cli.py index d5bcb8e..999468e 100644 --- a/src/fastapi_cli/cli.py +++ b/src/fastapi_cli/cli.py @@ -60,9 +60,10 @@ 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 @@ -84,7 +85,6 @@ def _run( padding=(1, 2), style="green", ) - print(Padding(panel, 1)) if not uvicorn: raise FastAPICLIException( "Could not import Uvicorn, try running 'pip install uvicorn'" @@ -97,6 +97,7 @@ def _run( workers=workers, root_path=root_path, proxy_headers=proxy_headers, + factory=is_factory, ) @@ -105,7 +106,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, *, @@ -145,6 +146,10 @@ 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 () -> callable.") + ] = False, ) -> Any: """ Run a [bold]FastAPI[/bold] app in [yellow]development[/yellow] mode. ๐Ÿงช @@ -180,6 +185,7 @@ def dev( app=app, command="dev", proxy_headers=proxy_headers, + is_factory=factory, ) @@ -234,6 +240,10 @@ 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 () -> callable.") + ] = False, ) -> Any: """ Run a [bold]FastAPI[/bold] app in [green]production[/green] mode. ๐Ÿš€ @@ -270,6 +280,7 @@ def run( app=app, command="run", proxy_headers=proxy_headers, + is_factory=factory ) diff --git a/src/fastapi_cli/discover.py b/src/fastapi_cli/discover.py index f442438..b9b4813 100644 --- a/src/fastapi_cli/discover.py +++ b/src/fastapi_cli/discover.py @@ -98,7 +98,7 @@ 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: @@ -119,25 +119,32 @@ 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() @@ -147,7 +154,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 = 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" ) From 5668b532af41f1df25d70188f4bc2fe19db65db0 Mon Sep 17 00:00:00 2001 From: rasput Date: Mon, 12 Aug 2024 13:22:26 +0300 Subject: [PATCH 2/3] fixed tests --- src/fastapi_cli/cli.py | 15 +++++++++++---- tests/test_cli.py | 4 ++++ tests/test_utils_package.py | 3 ++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/fastapi_cli/cli.py b/src/fastapi_cli/cli.py index 999468e..a806d43 100644 --- a/src/fastapi_cli/cli.py +++ b/src/fastapi_cli/cli.py @@ -63,7 +63,9 @@ def _run( is_factory: bool = False, ) -> None: try: - use_uvicorn_app = get_import_string(path=path, app_name=app, is_factory=is_factory) + 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 @@ -85,6 +87,7 @@ def _run( padding=(1, 2), style="green", ) + print(Padding(panel, 1)) if not uvicorn: raise FastAPICLIException( "Could not import Uvicorn, try running 'pip install uvicorn'" @@ -148,7 +151,9 @@ def dev( ] = True, factory: Annotated[ bool, - typer.Option(help="Treat [bold]path[bold] as an application factory, i.e. a () -> callable.") + typer.Option( + help="Treat [bold]path[/bold] as an application factory, i.e. a () -> callable." + ), ] = False, ) -> Any: """ @@ -242,7 +247,9 @@ def run( ] = True, factory: Annotated[ bool, - typer.Option(help="Treat [bold]path[bold] as an application factory, i.e. a () -> callable.") + typer.Option( + help="Treat [bold]path[/bold] as an application factory, i.e. a () -> callable." + ), ] = False, ) -> Any: """ @@ -280,7 +287,7 @@ def run( app=app, command="run", proxy_headers=proxy_headers, - is_factory=factory + is_factory=factory, ) diff --git a/tests/test_cli.py b/tests/test_cli.py index 44c14d2..58fe23d 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 ( @@ -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 ( @@ -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 ( @@ -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 ( diff --git a/tests/test_utils_package.py b/tests/test_utils_package.py index d5573db..60672bd 100644 --- a/tests/test_utils_package.py +++ b/tests/test_utils_package.py @@ -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] ) From 34aa51a9d95a355f101666f8fa1f115e3c4b90a4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 10:34:29 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20for?= =?UTF-8?q?mat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fastapi_cli/discover.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/fastapi_cli/discover.py b/src/fastapi_cli/discover.py index b9b4813..80dbd7b 100644 --- a/src/fastapi_cli/discover.py +++ b/src/fastapi_cli/discover.py @@ -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, is_factory: bool = False) -> 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: @@ -140,11 +142,16 @@ def get_app_name(*, mod_data: ModuleData, app_name: Union[str, None] = None, is_ return name elif callable(name) and is_factory: return name - raise FastAPICLIException("Could not find FastAPI app or app factory 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, is_factory: bool = False, + *, + path: Union[Path, None] = None, + app_name: Union[str, None] = None, + is_factory: bool = False, ) -> str: if not path: path = get_default_path() @@ -154,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, is_factory=is_factory) + 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" )