diff --git a/src/fastapi_cli/cli.py b/src/fastapi_cli/cli.py index 28afa297..c6e6aa80 100644 --- a/src/fastapi_cli/cli.py +++ b/src/fastapi_cli/cli.py @@ -195,39 +195,44 @@ 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. If not provided, a default set of paths will be tried.", + envvar="FASTAPI_PATH", ), ] = None, *, host: Annotated[ str, typer.Option( - help="The host to serve on. For local development in localhost use [blue]127.0.0.1[/blue]. To enable public access, e.g. in a container, use all the IP addresses available with [blue]0.0.0.0[/blue]." + help="The host to serve on. For local development in localhost use [blue]127.0.0.1[/blue]. To enable public access, e.g. in a container, use all the IP addresses available with [blue]0.0.0.0[/blue].", + envvar="FASTAPI_HOST", ), ] = "127.0.0.1", port: Annotated[ int, typer.Option( help="The port to serve on. You would normally have a termination proxy on top (another program) handling HTTPS on port [blue]443[/blue] and HTTP on port [blue]80[/blue], transferring the communication to your app.", - envvar="PORT", + envvar=["FASTAPI_PORT", "PORT"], ), ] = 8000, reload: Annotated[ bool, typer.Option( - help="Enable auto-reload of the server when (code) files change. This is [bold]resource intensive[/bold], use it only during development." + help="Enable auto-reload of the server when (code) files change. This is [bold]resource intensive[/bold], use it only during development.", + envvar="FASTAPI_RELOAD", ), ] = True, root_path: Annotated[ str, typer.Option( - help="The root path is used to tell your app that it is being served to the outside world with some [bold]path prefix[/bold] set up in some termination proxy or similar." + help="The root path is used to tell your app that it is being served to the outside world with some [bold]path prefix[/bold] set up in some termination proxy or similar.", + envvar="FASTAPI_ROOTPATH", ), ] = "", app: Annotated[ Union[str, None], typer.Option( - help="The name of the variable that contains the [bold]FastAPI[/bold] app in the imported module or package. If not provided, it is detected automatically." + help="The name of the variable that contains the [bold]FastAPI[/bold] app in the imported module or package. If not provided, it is detected automatically.", + envvar="FASTAPI_APP", ), ] = None, entrypoint: Annotated[ @@ -236,18 +241,21 @@ def dev( "--entrypoint", "-e", help="The FastAPI app import string in the format 'some.importable_module:app_name'.", + envvar="FASTAPI_ENTRYPOINT", ), ] = None, proxy_headers: Annotated[ bool, typer.Option( - help="Enable/Disable X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port to populate remote address info." + help="Enable/Disable X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port to populate remote address info.", + envvar="FASTAPI_PROXY_HEADERS", ), ] = True, forwarded_allow_ips: Annotated[ Union[str, None], typer.Option( - help="Comma separated list of IP Addresses to trust with proxy headers. The literal '*' means trust everything." + help="Comma separated list of IP Addresses to trust with proxy headers. The literal '*' means trust everything.", + envvar="FASTAPI_FORWARDED_ALLOW_IPS", ), ] = None, ) -> Any: @@ -295,45 +303,51 @@ def run( 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. If not provided, a default set of paths will be tried.", + envvar="FASTAPI_PATH", ), ] = None, *, host: Annotated[ str, typer.Option( - help="The host to serve on. For local development in localhost use [blue]127.0.0.1[/blue]. To enable public access, e.g. in a container, use all the IP addresses available with [blue]0.0.0.0[/blue]." + help="The host to serve on. For local development in localhost use [blue]127.0.0.1[/blue]. To enable public access, e.g. in a container, use all the IP addresses available with [blue]0.0.0.0[/blue].", + envvar="FASTAPI_HOST", ), ] = "0.0.0.0", port: Annotated[ int, typer.Option( help="The port to serve on. You would normally have a termination proxy on top (another program) handling HTTPS on port [blue]443[/blue] and HTTP on port [blue]80[/blue], transferring the communication to your app.", - envvar="PORT", + envvar=["FASTAPI_PORT", "PORT"], ), ] = 8000, reload: Annotated[ bool, typer.Option( - help="Enable auto-reload of the server when (code) files change. This is [bold]resource intensive[/bold], use it only during development." + help="Enable auto-reload of the server when (code) files change. This is [bold]resource intensive[/bold], use it only during development.", + envvar="FASTAPI_RELOAD", ), ] = False, workers: Annotated[ Union[int, None], typer.Option( - help="Use multiple worker processes. Mutually exclusive with the --reload flag." + help="Use multiple worker processes. Mutually exclusive with the --reload flag.", + envvar="FASTAPI_WORKERS", ), ] = None, root_path: Annotated[ str, typer.Option( - help="The root path is used to tell your app that it is being served to the outside world with some [bold]path prefix[/bold] set up in some termination proxy or similar." + help="The root path is used to tell your app that it is being served to the outside world with some [bold]path prefix[/bold] set up in some termination proxy or similar.", + envvar="FASTAPI_ROOTPATH", ), ] = "", app: Annotated[ Union[str, None], typer.Option( - help="The name of the variable that contains the [bold]FastAPI[/bold] app in the imported module or package. If not provided, it is detected automatically." + help="The name of the variable that contains the [bold]FastAPI[/bold] app in the imported module or package. If not provided, it is detected automatically.", + envvar="FASTAPI_APP", ), ] = None, entrypoint: Annotated[ @@ -342,18 +356,21 @@ def run( "--entrypoint", "-e", help="The FastAPI app import string in the format 'some.importable_module:app_name'.", + envvar="FASTAPI_ENTRYPOINT", ), ] = None, proxy_headers: Annotated[ bool, typer.Option( - help="Enable/Disable X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port to populate remote address info." + help="Enable/Disable X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port to populate remote address info.", + envvar="FASTAPI_PROXY_HEADERS", ), ] = True, forwarded_allow_ips: Annotated[ Union[str, None], typer.Option( - help="Comma separated list of IP Addresses to trust with proxy headers. The literal '*' means trust everything." + help="Comma separated list of IP Addresses to trust with proxy headers. The literal '*' means trust everything.", + envvar="FASTAPI_FORWARDED_ALLOW_IPS", ), ] = None, ) -> Any: diff --git a/tests/test_cli.py b/tests/test_cli.py index b87a811a..9ac492a6 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -126,32 +126,69 @@ def test_dev_env_vars() -> None: with changing_dir(assets_path): with patch.object(uvicorn, "run") as mock_run: result = runner.invoke( - app, ["dev", "single_file_app.py"], env={"PORT": "8111"} + app, + ["dev", "single_file_app.py"], + env={ + "FASTAPI_HOST": "127.0.0.2", + "FASTAPI_PORT": "8111", + "FASTAPI_RELOAD": "false", + "FASTAPI_ROOTPATH": "/api", + "FASTAPI_APP": "api", + "FASTAPI_PROXY_HEADERS": "false", + "FASTAPI_FORWARDED_ALLOW_IPS": "*", + }, ) assert result.exit_code == 0, result.output assert mock_run.called assert mock_run.call_args assert mock_run.call_args.kwargs == { - "app": "single_file_app:app", - "host": "127.0.0.1", + "app": "single_file_app:api", + "host": "127.0.0.2", "port": 8111, - "reload": True, + "reload": False, "workers": None, - "root_path": "", - "proxy_headers": True, - "forwarded_allow_ips": None, + "root_path": "/api", + "proxy_headers": False, + "forwarded_allow_ips": "*", "log_config": get_uvicorn_log_config(), } - assert "Using import string: single_file_app:app" in result.output + assert "Using import string: single_file_app:api" in result.output assert "Starting development server 🚀" in result.output - assert "Server started at http://127.0.0.1:8111" in result.output - assert "Documentation at http://127.0.0.1:8111/docs" in result.output + assert "Server started at http://127.0.0.2:8111" in result.output + assert "Documentation at http://127.0.0.2:8111/docs" in result.output assert ( "Running in development mode, for production use: fastapi run" in result.output ) +def test_dev_env_vars_port() -> None: + with changing_dir(assets_path): + with patch.object(uvicorn, "run") as mock_run: + result = runner.invoke( + app, + ["dev", "single_file_app.py"], + env={"PORT": "8111"}, + ) + assert result.exit_code == 0, result.output + assert mock_run.call_args.kwargs["port"] == 8111 + + +def test_dev_env_vars_port_precedence() -> None: + with changing_dir(assets_path): + with patch.object(uvicorn, "run") as mock_run: + result = runner.invoke( + app, + ["dev", "single_file_app.py"], + env={ + "PORT": "8111", + "FASTAPI_PORT": "8112", + }, + ) + assert result.exit_code == 0, result.output + assert mock_run.call_args.kwargs["port"] == 8112 + + def test_dev_env_vars_and_args() -> None: with changing_dir(assets_path): with patch.object(uvicorn, "run") as mock_run: @@ -163,7 +200,7 @@ def test_dev_env_vars_and_args() -> None: "--port", "8080", ], - env={"PORT": "8111"}, + env={"FASTAPI_PORT": "8111"}, ) assert result.exit_code == 0, result.output assert mock_run.called @@ -294,26 +331,64 @@ def test_run_env_vars() -> None: with changing_dir(assets_path): with patch.object(uvicorn, "run") as mock_run: result = runner.invoke( - app, ["run", "single_file_app.py"], env={"PORT": "8111"} + app, + ["run", "single_file_app.py"], + env={ + "FASTAPI_HOST": "192.168.1.1", + "FASTAPI_PORT": "8111", + "FASTAPI_RELOAD": "true", + "FASTAPI_WORKERS": "4", + "FASTAPI_ROOTPATH": "/api", + "FASTAPI_APP": "api", + "FASTAPI_PROXY_HEADERS": "false", + "FASTAPI_FORWARDED_ALLOW_IPS": "*", + }, ) assert result.exit_code == 0, result.output assert mock_run.called assert mock_run.call_args assert mock_run.call_args.kwargs == { - "app": "single_file_app:app", - "host": "0.0.0.0", + "app": "single_file_app:api", + "host": "192.168.1.1", "port": 8111, - "reload": False, - "workers": None, - "root_path": "", - "proxy_headers": True, - "forwarded_allow_ips": None, + "reload": True, + "workers": 4, + "root_path": "/api", + "proxy_headers": False, + "forwarded_allow_ips": "*", "log_config": get_uvicorn_log_config(), } - assert "Using import string: single_file_app:app" in result.output + assert "Using import string: single_file_app:api" in result.output assert "Starting production server 🚀" in result.output - assert "Server started at http://0.0.0.0:8111" in result.output - assert "Documentation at http://0.0.0.0:8111/docs" in result.output + assert "Server started at http://192.168.1.1:8111" in result.output + assert "Documentation at http://192.168.1.1:8111/docs" in result.output + + +def test_run_env_vars_port() -> None: + with changing_dir(assets_path): + with patch.object(uvicorn, "run") as mock_run: + result = runner.invoke( + app, + ["run", "single_file_app.py"], + env={"PORT": "8111"}, + ) + assert result.exit_code == 0, result.output + assert mock_run.call_args.kwargs["port"] == 8111 + + +def test_run_env_vars_port_precedence() -> None: + with changing_dir(assets_path): + with patch.object(uvicorn, "run") as mock_run: + result = runner.invoke( + app, + ["run", "single_file_app.py"], + env={ + "PORT": "8111", + "FASTAPI_PORT": "8112", + }, + ) + assert result.exit_code == 0, result.output + assert mock_run.call_args.kwargs["port"] == 8112 def test_run_env_vars_and_args() -> None: @@ -327,7 +402,7 @@ def test_run_env_vars_and_args() -> None: "--port", "8080", ], - env={"PORT": "8111"}, + env={"FASTAPI_PORT": "8111"}, ) assert result.exit_code == 0, result.output assert mock_run.called