diff --git a/README.md b/README.md index 569a1827..1ba31b57 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,10 @@ By default, it will have **auto-reload** enabled, so it will automatically reloa By default it will listen on the IP address `127.0.0.1`, which is the IP for your machine to communicate with itself alone (`localhost`). +Before importing your app, `fastapi dev` sets the `FASTAPI_ENV` environment variable to `development`. If `FASTAPI_ENV` is already set, its existing value is preserved. This lets app startup code choose development-friendly behavior while allowing you to provide an app-specific environment such as `staging`. + +The conventional `FASTAPI_ENV` values are `development` and `production`. `fastapi run` currently leaves `FASTAPI_ENV` unchanged, so set it explicitly if your app needs to detect production mode. + ## `fastapi run` When you run `fastapi run`, it will run on production mode by default. diff --git a/src/fastapi_cli/cli.py b/src/fastapi_cli/cli.py index 1f1b6064..5c71c829 100644 --- a/src/fastapi_cli/cli.py +++ b/src/fastapi_cli/cli.py @@ -399,6 +399,8 @@ def dev( Otherwise, it uses the first [bold]FastAPI[/bold] app found in the imported module or package. """ + os.environ.setdefault("FASTAPI_ENV", "development") + _run( path=path, host=host, diff --git a/tests/assets/environment_app.py b/tests/assets/environment_app.py new file mode 100644 index 00000000..02080ec2 --- /dev/null +++ b/tests/assets/environment_app.py @@ -0,0 +1,12 @@ +import os + +from fastapi import FastAPI + +app = FastAPI() + +fastapi_env_at_import = os.environ.get("FASTAPI_ENV") + + +@app.get("/fastapi-env") +def get_fastapi_env() -> dict[str, str | None]: + return {"fastapi_env": fastapi_env_at_import} diff --git a/tests/test_cli.py b/tests/test_cli.py index 8f8ee5b4..818155de 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -6,7 +6,10 @@ import pytest import uvicorn +from fastapi.testclient import TestClient +from httpx import Response from typer.testing import CliRunner +from uvicorn.importer import import_from_string from fastapi_cli.cli import app from fastapi_cli.utils.cli import get_uvicorn_log_config @@ -18,7 +21,8 @@ @pytest.fixture(autouse=True) -def force_rich_logs(monkeypatch: pytest.MonkeyPatch) -> None: +def isolate_cli_environment(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("FASTAPI_ENV", raising=False) monkeypatch.setattr("fastapi_cli.cli.should_use_rich_logs", lambda: True) @@ -53,6 +57,49 @@ def test_dev() -> None: assert "Configuration sources:" not in result.output +def _get_fastapi_env_response(*, command: str, fastapi_env: str | None) -> Response: + module_name = "environment_app" + sys.modules.pop(module_name, None) + + try: + with changing_dir(assets_path): + with patch.object(uvicorn, "run") as mock_run: + result = runner.invoke( + app, + [command, f"{module_name}.py"], + env={"FASTAPI_ENV": fastapi_env}, + ) + + assert result.exit_code == 0, result.output + assert mock_run.call_args + imported_app = import_from_string(mock_run.call_args.kwargs["app"]) + with TestClient(imported_app) as test_client: + return test_client.get("/fastapi-env") + finally: + sys.modules.pop(module_name, None) + + +def test_dev_sets_fastapi_env_before_app_import() -> None: + response = _get_fastapi_env_response(command="dev", fastapi_env=None) + + assert response.status_code == 200 + assert response.json() == {"fastapi_env": "development"} + + +def test_dev_does_not_override_existing_fastapi_env() -> None: + response = _get_fastapi_env_response(command="dev", fastapi_env="chicken") + + assert response.status_code == 200 + assert response.json() == {"fastapi_env": "chicken"} + + +def test_run_does_not_set_fastapi_env() -> None: + response = _get_fastapi_env_response(command="run", fastapi_env=None) + + assert response.status_code == 200 + assert response.json() == {"fastapi_env": None} + + def test_run_uses_uvicorn_default_log_config_without_rich_logs( monkeypatch: pytest.MonkeyPatch, ) -> None: