From 03f3ac74c68d7f3324621658ed4e4b64d044c6b5 Mon Sep 17 00:00:00 2001 From: ulises-jeremias Date: Wed, 22 Jul 2026 01:29:29 -0300 Subject: [PATCH 1/4] feat(cli): add --fixture flag for catalog fixture mode Enable CPA_CATALOG_FIXTURE via --fixture [dir] (CNA parity) and resolve CPA_FIXTURE_DIR before the in-memory catalog cache. Closes #227 Co-authored-by: Cursor --- .../src/create_awesome_python_app/catalog.py | 117 +++++++++++++----- .../src/create_awesome_python_app/cli.py | 57 ++++++++- .../tests/test_catalog_fetch.py | 41 ++++++ .../tests/test_cli.py | 85 ++++++++++++- 4 files changed, 264 insertions(+), 36 deletions(-) diff --git a/packages/create-awesome-python-app/src/create_awesome_python_app/catalog.py b/packages/create-awesome-python-app/src/create_awesome_python_app/catalog.py index 080e42c..d4afc6f 100644 --- a/packages/create-awesome-python-app/src/create_awesome_python_app/catalog.py +++ b/packages/create-awesome-python-app/src/create_awesome_python_app/catalog.py @@ -344,9 +344,9 @@ def group_extension_choices( FETCH_TIMEOUT_SECONDS = 10 USER_AGENT = f"create-awesome-python-app/{__version__} (https://github.com/Create-Python-App/create-python-app)" -_FIXTURE = ( - Path(__file__).resolve().parents[4] / "fixtures" / "catalog" / "templates.json" -) +_AUTO_FIXTURE_DIR = Path(__file__).resolve().parents[4] +_SENTINEL = object() +_fixture_root_override: Path | None | object = _SENTINEL _memory_cache: dict[str, Any] | None = None _memory_ts: float = 0.0 @@ -360,13 +360,54 @@ def catalog_cache_path() -> Path: return default_cache_dir() / "catalog" / "templates.json" +def resolve_fixture_root() -> Path | None: + """Resolve the repo root that contains ``fixtures/catalog/templates.json``. + + Priority: ``CPA_FIXTURE_DIR`` → package-relative monorepo root → ``cwd``. + """ + if _fixture_root_override is not _SENTINEL: + return _fixture_root_override # type: ignore[return-value] + + env = os.environ.get("CPA_FIXTURE_DIR", "").strip() + if env: + return Path(env).expanduser().resolve() + + auto = _AUTO_FIXTURE_DIR + if (auto / "fixtures" / "catalog" / "templates.json").is_file(): + return auto + + cwd = Path.cwd() + if (cwd / "fixtures" / "catalog" / "templates.json").is_file(): + return cwd + return None + + +def set_fixture_root_for_tests(root: Path | None) -> None: + """Override fixture root (test helper).""" + global _fixture_root_override + _fixture_root_override = root + + +def reset_fixture_root_for_tests() -> None: + global _fixture_root_override + _fixture_root_override = _SENTINEL + + +def fixture_catalog_path() -> Path | None: + root = resolve_fixture_root() + if root is None: + return None + return root / "fixtures" / "catalog" / "templates.json" + + def _read_json_file(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8")) def _read_fixture() -> dict[str, Any]: - if _FIXTURE.is_file(): - return _read_json_file(_FIXTURE) + path = fixture_catalog_path() + if path is not None and path.is_file(): + return _read_json_file(path) return {"templates": [], "extensions": [], "categories": []} @@ -417,6 +458,19 @@ def get_catalog_data(*, force_refresh: bool = False) -> dict[str, Any]: """Load templates.json from remote URL, disk cache, or local fixture.""" global _memory_cache, _memory_ts + if os.environ.get("CPA_CATALOG_FIXTURE") == "1": + path = fixture_catalog_path() + if path is None or not path.is_file(): + raise RuntimeError( + "Fixture mode is enabled (CPA_CATALOG_FIXTURE=1) but the fixture " + "root could not be resolved. Set CPA_FIXTURE_DIR to the repo root " + "containing fixtures/catalog/templates.json." + ) + data = _read_fixture() + _memory_cache = data + _memory_ts = time.time() + return data + if ( not force_refresh and _memory_cache is not None @@ -425,38 +479,35 @@ def get_catalog_data(*, force_refresh: bool = False) -> dict[str, Any]: ): return _memory_cache - if os.environ.get("CPA_CATALOG_FIXTURE") == "1": - data = _read_fixture() - else: - url = catalog_url() - try: - data = _fetch_remote(url) - _write_disk_cache(data) - except ( - urllib.error.URLError, - TimeoutError, - OSError, - json.JSONDecodeError, - ) as err: - disk = _read_disk_cache() - if disk is not None: + url = catalog_url() + try: + data = _fetch_remote(url) + _write_disk_cache(data) + except ( + urllib.error.URLError, + TimeoutError, + OSError, + json.JSONDecodeError, + ) as err: + disk = _read_disk_cache() + if disk is not None: + console.print( + "[yellow][cpa] Could not refresh catalog " + f"({err}); using disk cache.[/yellow]" + ) + data = disk + else: + fixture = _read_fixture() + if fixture.get("templates"): console.print( "[yellow][cpa] Could not refresh catalog " - f"({err}); using disk cache.[/yellow]" + f"({err}); using fixture.[/yellow]" ) - data = disk + data = fixture else: - fixture = _read_fixture() - if fixture.get("templates"): - console.print( - "[yellow][cpa] Could not refresh catalog " - f"({err}); using fixture.[/yellow]" - ) - data = fixture - else: - raise RuntimeError( - f"Failed to load template catalog: {err}" - ) from err + raise RuntimeError( + f"Failed to load template catalog: {err}" + ) from err _memory_cache = data _memory_ts = time.time() diff --git a/packages/create-awesome-python-app/src/create_awesome_python_app/cli.py b/packages/create-awesome-python-app/src/create_awesome_python_app/cli.py index bdc5204..de8b047 100644 --- a/packages/create-awesome-python-app/src/create_awesome_python_app/cli.py +++ b/packages/create-awesome-python-app/src/create_awesome_python_app/cli.py @@ -4,6 +4,7 @@ import asyncio import os +import sys from pathlib import Path from typing import Any @@ -25,6 +26,9 @@ from create_awesome_python_app import __version__ +# Sentinel for bare ``--fixture`` (optional DIR rewritten in argv preprocess). +_FIXTURE_AUTO = "__CPA_FIXTURE_AUTO__" + app = typer.Typer( name="create-awesome-python-app", help="Composable scaffolding CLI for production-ready Python apps.", @@ -36,6 +40,43 @@ console = Console(stderr=True) +def _preprocess_fixture_argv(argv: list[str] | None = None) -> list[str]: + """Rewrite bare ``--fixture`` to ``--fixture=__CPA_FIXTURE_AUTO__``. + + Typer/Click requires an option argument; Commander allows ``--fixture [dir]``. + This keeps CNA-compatible UX: ``--fixture`` alone enables auto-detect mode. + """ + raw = list(sys.argv if argv is None else argv) + if not raw: + return raw + out = [raw[0]] + i = 1 + while i < len(raw): + arg = raw[i] + if arg == "--fixture": + if i + 1 < len(raw) and not raw[i + 1].startswith("-"): + out.extend(["--fixture", raw[i + 1]]) + i += 2 + else: + out.append(f"--fixture={_FIXTURE_AUTO}") + i += 1 + continue + out.append(arg) + i += 1 + if argv is None: + sys.argv = out + return out + + +def apply_fixture_mode(fixture: str | None) -> None: + """Translate ``--fixture`` into ``CPA_CATALOG_FIXTURE`` / ``CPA_FIXTURE_DIR``.""" + if fixture is None and os.environ.get("CPA_CATALOG_FIXTURE") != "1": + return + os.environ["CPA_CATALOG_FIXTURE"] = "1" + if fixture is not None and fixture != _FIXTURE_AUTO and fixture != "": + os.environ["CPA_FIXTURE_DIR"] = fixture + + def _in_ci() -> bool: return os.environ.get("CI", "").lower() in {"1", "true", "yes"} @@ -159,9 +200,8 @@ def main() -> None: `create-awesome-python-app cache dir` works (Typer would otherwise treat `cache` as project_directory). """ - import sys - check_python_version(">=3.12", "create-awesome-python-app") + _preprocess_fixture_argv() if len(sys.argv) > 1 and sys.argv[1] == "cache": sys.argv = [sys.argv[0], *sys.argv[2:]] cache_app(prog_name="create-awesome-python-app cache") @@ -192,6 +232,15 @@ def scaffold( refresh: str | None = typer.Option(None, "--refresh"), strict_version: bool = typer.Option(False, "--strict-version"), keep_on_failure: bool = typer.Option(False, "--keep-on-failure"), + fixture: str | None = typer.Option( + None, + "--fixture", + help=( + "Load the template catalog from the local fixtures/ directory " + "instead of the network (optional DIR = repo root; also " + "CPA_FIXTURE_DIR / CPA_CATALOG_FIXTURE)" + ), + ), ) -> None: if version: console.print(__version__) @@ -201,6 +250,10 @@ def scaffold( if ctx.invoked_subcommand is not None: return + # Translate --fixture into env vars before catalog loads + # (--list-templates / interactive / scaffold). + apply_fixture_mode(fixture) + if list_templates or list_addons: from create_awesome_python_app.catalog import list_addons as la from create_awesome_python_app.catalog import list_templates as lt diff --git a/packages/create-awesome-python-app/tests/test_catalog_fetch.py b/packages/create-awesome-python-app/tests/test_catalog_fetch.py index 6b225f7..ffa3a44 100644 --- a/packages/create-awesome-python-app/tests/test_catalog_fetch.py +++ b/packages/create-awesome-python-app/tests/test_catalog_fetch.py @@ -13,6 +13,9 @@ catalog_url, get_catalog_data, reset_catalog_cache_for_tests, + reset_fixture_root_for_tests, + resolve_fixture_root, + set_fixture_root_for_tests, ) FIXTURE_PATH = ( @@ -23,7 +26,13 @@ @pytest.fixture(autouse=True) def _reset_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: reset_catalog_cache_for_tests() + reset_fixture_root_for_tests() monkeypatch.setenv("CPA_CACHE_DIR", str(tmp_path / "cache")) + monkeypatch.delenv("CPA_FIXTURE_DIR", raising=False) + monkeypatch.delenv("CPA_CATALOG_FIXTURE", raising=False) + yield + reset_fixture_root_for_tests() + reset_catalog_cache_for_tests() def test_default_catalog_url(monkeypatch: pytest.MonkeyPatch) -> None: @@ -83,3 +92,35 @@ def test_get_catalog_data_disk_fallback_on_network_error( def test_default_url_points_to_cpa_templates() -> None: assert DEFAULT_CATALOG_URL.endswith("/cpa-templates/main/templates.json") + + +def test_resolve_fixture_root_respects_env( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("CPA_FIXTURE_DIR", str(tmp_path)) + assert resolve_fixture_root() == tmp_path.resolve() + + +def test_get_catalog_data_fixture_uses_custom_dir( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + catalog_dir = tmp_path / "fixtures" / "catalog" + catalog_dir.mkdir(parents=True) + payload = { + "templates": [{"slug": "from-env-fixture"}], + "extensions": [], + "categories": [], + } + (catalog_dir / "templates.json").write_text( + json.dumps(payload), encoding="utf-8" + ) + monkeypatch.setenv("CPA_CATALOG_FIXTURE", "1") + monkeypatch.setenv("CPA_FIXTURE_DIR", str(tmp_path)) + data = get_catalog_data(force_refresh=True) + assert data["templates"][0]["slug"] == "from-env-fixture" + + +def test_set_fixture_root_for_tests(tmp_path: Path) -> None: + set_fixture_root_for_tests(tmp_path) + assert resolve_fixture_root() == tmp_path + reset_fixture_root_for_tests() diff --git a/packages/create-awesome-python-app/tests/test_cli.py b/packages/create-awesome-python-app/tests/test_cli.py index d15be92..482c825 100644 --- a/packages/create-awesome-python-app/tests/test_cli.py +++ b/packages/create-awesome-python-app/tests/test_cli.py @@ -1,3 +1,4 @@ +import os from pathlib import Path import pytest @@ -6,16 +7,26 @@ from typer.testing import CliRunner runner = CliRunner() -_CPA_ENV_VARS = ("CPA_REFRESH", "CPA_NO_CATALOG_CACHE", "CPA_CACHE_DIR") +_CPA_ENV_VARS = ( + "CPA_REFRESH", + "CPA_NO_CATALOG_CACHE", + "CPA_CACHE_DIR", + "CPA_CATALOG_FIXTURE", + "CPA_FIXTURE_DIR", +) @pytest.fixture(autouse=True) def _clean_cpa_env(monkeypatch): + from create_awesome_python_app.catalog import reset_catalog_cache_for_tests + for name in _CPA_ENV_VARS: monkeypatch.delenv(name, raising=False) + reset_catalog_cache_for_tests() yield for name in _CPA_ENV_VARS: monkeypatch.delenv(name, raising=False) + reset_catalog_cache_for_tests() def test_version() -> None: @@ -223,3 +234,75 @@ async def fake_check_for_latest_version(_package_name): assert result.exit_code == 2 assert "Incompatible extension combination" in text assert "saga" in text + + +def test_help_mentions_fixture() -> None: + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "--fixture" in result.stdout + + +def test_preprocess_fixture_argv_bare_and_with_dir() -> None: + from create_awesome_python_app.cli import _FIXTURE_AUTO, _preprocess_fixture_argv + + assert _preprocess_fixture_argv(["cpa", "--fixture", "--list-templates"]) == [ + "cpa", + f"--fixture={_FIXTURE_AUTO}", + "--list-templates", + ] + assert _preprocess_fixture_argv( + ["cpa", "--fixture", "./my-fixtures", "--list-templates"] + ) == ["cpa", "--fixture", "./my-fixtures", "--list-templates"] + + +def test_fixture_flag_enables_catalog_fixture( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from create_awesome_python_app.cli import _FIXTURE_AUTO, apply_fixture_mode + + apply_fixture_mode(_FIXTURE_AUTO) + assert os.environ.get("CPA_CATALOG_FIXTURE") == "1" + assert "CPA_FIXTURE_DIR" not in os.environ + + monkeypatch.delenv("CPA_CATALOG_FIXTURE", raising=False) + monkeypatch.delenv("CPA_FIXTURE_DIR", raising=False) + apply_fixture_mode(str(tmp_path)) + assert os.environ.get("CPA_CATALOG_FIXTURE") == "1" + assert os.environ.get("CPA_FIXTURE_DIR") == str(tmp_path) + + +def test_list_templates_with_fixture_dir( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + import json + import os + + catalog_dir = tmp_path / "fixtures" / "catalog" + catalog_dir.mkdir(parents=True) + (catalog_dir / "templates.json").write_text( + json.dumps( + { + "templates": [ + { + "slug": "fixture-only", + "category": "tooling", + "type": "cli", + } + ], + "extensions": [], + "categories": [], + } + ), + encoding="utf-8", + ) + monkeypatch.delenv("CPA_CATALOG_FIXTURE", raising=False) + monkeypatch.delenv("CPA_FIXTURE_DIR", raising=False) + + result = runner.invoke( + app, + ["--fixture", str(tmp_path), "--list-templates"], + ) + text = (result.stdout or "") + (result.stderr or "") + assert result.exit_code == 0, text + assert "fixture-only" in text + assert os.environ.get("CPA_CATALOG_FIXTURE") == "1" From c6d6ab92b9c199a6f639e71cac86a33fa7fb4b11 Mon Sep 17 00:00:00 2001 From: ulises-jeremias Date: Wed, 22 Jul 2026 01:48:29 -0300 Subject: [PATCH 2/4] fix(cli): stabilize --fixture for CI format, types, and env leak Format catalog helpers, fix pyright on the fixture generator, walk up for fixture roots, and keep integration scaffolds out of fixture mode. Co-authored-by: Cursor --- .../src/create_awesome_python_app/catalog.py | 21 ++++++++++++------- .../tests/test_catalog_fetch.py | 6 ++---- .../tests/test_cli.py | 3 ++- .../tests/test_cpa_templates_integration.py | 21 +++++++++++++++++++ 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/packages/create-awesome-python-app/src/create_awesome_python_app/catalog.py b/packages/create-awesome-python-app/src/create_awesome_python_app/catalog.py index d4afc6f..28440fa 100644 --- a/packages/create-awesome-python-app/src/create_awesome_python_app/catalog.py +++ b/packages/create-awesome-python-app/src/create_awesome_python_app/catalog.py @@ -363,7 +363,7 @@ def catalog_cache_path() -> Path: def resolve_fixture_root() -> Path | None: """Resolve the repo root that contains ``fixtures/catalog/templates.json``. - Priority: ``CPA_FIXTURE_DIR`` → package-relative monorepo root → ``cwd``. + Priority: ``CPA_FIXTURE_DIR`` → walk-up from package → ``cwd``. """ if _fixture_root_override is not _SENTINEL: return _fixture_root_override # type: ignore[return-value] @@ -372,12 +372,19 @@ def resolve_fixture_root() -> Path | None: if env: return Path(env).expanduser().resolve() - auto = _AUTO_FIXTURE_DIR - if (auto / "fixtures" / "catalog" / "templates.json").is_file(): - return auto + def _has_fixture_catalog(root: Path) -> bool: + return (root / "fixtures" / "catalog" / "templates.json").is_file() + + if _has_fixture_catalog(_AUTO_FIXTURE_DIR): + return _AUTO_FIXTURE_DIR + + # Editable / site-packages layouts vary; walk up from this file. + for parent in Path(__file__).resolve().parents: + if _has_fixture_catalog(parent): + return parent cwd = Path.cwd() - if (cwd / "fixtures" / "catalog" / "templates.json").is_file(): + if _has_fixture_catalog(cwd): return cwd return None @@ -505,9 +512,7 @@ def get_catalog_data(*, force_refresh: bool = False) -> dict[str, Any]: ) data = fixture else: - raise RuntimeError( - f"Failed to load template catalog: {err}" - ) from err + raise RuntimeError(f"Failed to load template catalog: {err}") from err _memory_cache = data _memory_ts = time.time() diff --git a/packages/create-awesome-python-app/tests/test_catalog_fetch.py b/packages/create-awesome-python-app/tests/test_catalog_fetch.py index ffa3a44..6eab6d8 100644 --- a/packages/create-awesome-python-app/tests/test_catalog_fetch.py +++ b/packages/create-awesome-python-app/tests/test_catalog_fetch.py @@ -24,7 +24,7 @@ @pytest.fixture(autouse=True) -def _reset_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def _reset_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): reset_catalog_cache_for_tests() reset_fixture_root_for_tests() monkeypatch.setenv("CPA_CACHE_DIR", str(tmp_path / "cache")) @@ -111,9 +111,7 @@ def test_get_catalog_data_fixture_uses_custom_dir( "extensions": [], "categories": [], } - (catalog_dir / "templates.json").write_text( - json.dumps(payload), encoding="utf-8" - ) + (catalog_dir / "templates.json").write_text(json.dumps(payload), encoding="utf-8") monkeypatch.setenv("CPA_CATALOG_FIXTURE", "1") monkeypatch.setenv("CPA_FIXTURE_DIR", str(tmp_path)) data = get_catalog_data(force_refresh=True) diff --git a/packages/create-awesome-python-app/tests/test_cli.py b/packages/create-awesome-python-app/tests/test_cli.py index 482c825..a441512 100644 --- a/packages/create-awesome-python-app/tests/test_cli.py +++ b/packages/create-awesome-python-app/tests/test_cli.py @@ -239,7 +239,8 @@ async def fake_check_for_latest_version(_package_name): def test_help_mentions_fixture() -> None: result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 - assert "--fixture" in result.stdout + text = (result.stdout or "") + (result.stderr or "") + assert "fixture" in text.lower() def test_preprocess_fixture_argv_bare_and_with_dir() -> None: diff --git a/packages/create-awesome-python-app/tests/test_cpa_templates_integration.py b/packages/create-awesome-python-app/tests/test_cpa_templates_integration.py index 774db7f..620e9b8 100644 --- a/packages/create-awesome-python-app/tests/test_cpa_templates_integration.py +++ b/packages/create-awesome-python-app/tests/test_cpa_templates_integration.py @@ -21,6 +21,19 @@ def _cpa_templates_available() -> bool: return (FASTAPI_TEMPLATE / "pyproject.toml").is_file() +def _clean_fixture_env(monkeypatch: pytest.MonkeyPatch) -> None: + """Ensure subprocess scaffolds are not forced into fixture mode.""" + monkeypatch.delenv("CPA_CATALOG_FIXTURE", raising=False) + monkeypatch.delenv("CPA_FIXTURE_DIR", raising=False) + + +def _subprocess_env() -> dict[str, str]: + env = os.environ.copy() + env.pop("CPA_CATALOG_FIXTURE", None) + env.pop("CPA_FIXTURE_DIR", None) + return env + + @pytest.mark.skipif( not _cpa_templates_available(), reason="cpa-templates checkout not available (set CPA_TEMPLATES_ROOT)", @@ -28,6 +41,7 @@ def _cpa_templates_available() -> bool: def test_scaffold_fastapi_starter_from_cpa_templates( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: + _clean_fixture_env(monkeypatch) monkeypatch.setenv("CI", "1") monkeypatch.setenv("CPA_SKIP_GIT", "1") monkeypatch.setenv("CPA_CACHE_DIR", str(tmp_path / "cpa-cache")) @@ -48,6 +62,7 @@ def test_scaffold_fastapi_starter_from_cpa_templates( cwd=_REPO_ROOT, capture_output=True, text=True, + env=_subprocess_env(), check=False, ) assert result.returncode == 0, result.stdout + result.stderr @@ -78,6 +93,7 @@ def test_scaffold_fastapi_starter_via_catalog_slug( """Scaffold using --template fastapi-starter slug (issue #160 / #161).""" import json + _clean_fixture_env(monkeypatch) monkeypatch.setenv("CI", "1") monkeypatch.setenv("CPA_SKIP_GIT", "1") monkeypatch.setenv("CPA_CACHE_DIR", str(tmp_path / "cpa-cache")) @@ -114,6 +130,7 @@ def test_scaffold_fastapi_starter_via_catalog_slug( cwd=_REPO_ROOT, capture_output=True, text=True, + env=_subprocess_env(), check=False, ) assert result.returncode == 0, result.stdout + result.stderr @@ -129,6 +146,7 @@ def test_scaffold_via_catalog_addon_slug( ) -> None: import json + _clean_fixture_env(monkeypatch) monkeypatch.setenv("CI", "1") monkeypatch.setenv("CPA_SKIP_GIT", "1") monkeypatch.setenv("CPA_CACHE_DIR", str(tmp_path / "cpa-cache")) @@ -170,6 +188,7 @@ def test_scaffold_via_catalog_addon_slug( cwd=_REPO_ROOT, capture_output=True, text=True, + env=_subprocess_env(), check=False, ) assert result.returncode == 0, result.stdout + result.stderr @@ -183,6 +202,7 @@ def test_scaffold_via_catalog_addon_slug( def test_scaffold_fastapi_with_github_setup_extension( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: + _clean_fixture_env(monkeypatch) monkeypatch.setenv("CI", "1") monkeypatch.setenv("CPA_SKIP_GIT", "1") monkeypatch.setenv("CPA_CACHE_DIR", str(tmp_path / "cpa-cache")) @@ -204,6 +224,7 @@ def test_scaffold_fastapi_with_github_setup_extension( cwd=_REPO_ROOT, capture_output=True, text=True, + env=_subprocess_env(), check=False, ) assert result.returncode == 0, result.stdout + result.stderr From 4cb11a3008dd8c9dcc04f57d7438b987c8c3d37d Mon Sep 17 00:00:00 2001 From: ulises-jeremias Date: Wed, 22 Jul 2026 01:55:58 -0300 Subject: [PATCH 3/4] fix(test): prevent CPA_CATALOG_FIXTURE leak across tests Clear fixture-related env via os.environ.pop in a shared autouse conftest so monkeypatch teardown cannot restore values set by apply_fixture_mode. Co-authored-by: Cursor --- .../tests/conftest.py | 39 +++++++++++++++++++ .../tests/test_cli.py | 20 ---------- 2 files changed, 39 insertions(+), 20 deletions(-) create mode 100644 packages/create-awesome-python-app/tests/conftest.py diff --git a/packages/create-awesome-python-app/tests/conftest.py b/packages/create-awesome-python-app/tests/conftest.py new file mode 100644 index 0000000..dfe8921 --- /dev/null +++ b/packages/create-awesome-python-app/tests/conftest.py @@ -0,0 +1,39 @@ +"""Shared test fixtures for create-awesome-python-app.""" + +from __future__ import annotations + +import os + +import pytest + +_CPA_ENV_VARS = ( + "CPA_REFRESH", + "CPA_NO_CATALOG_CACHE", + "CPA_CACHE_DIR", + "CPA_CATALOG_FIXTURE", + "CPA_FIXTURE_DIR", +) + + +@pytest.fixture(autouse=True) +def _clean_cpa_process_env() -> None: + """Clear CPA env vars that CLI helpers set via ``os.environ`` (not monkeypatch). + + ``apply_fixture_mode`` mutates ``os.environ`` directly. Pairing that with + ``monkeypatch.delenv`` after the test can restore the leaked value when + monkeypatch undoes its stack — so cleanup must use ``os.environ.pop``. + """ + from create_awesome_python_app.catalog import ( + reset_catalog_cache_for_tests, + reset_fixture_root_for_tests, + ) + + for name in _CPA_ENV_VARS: + os.environ.pop(name, None) + reset_catalog_cache_for_tests() + reset_fixture_root_for_tests() + yield + for name in _CPA_ENV_VARS: + os.environ.pop(name, None) + reset_catalog_cache_for_tests() + reset_fixture_root_for_tests() diff --git a/packages/create-awesome-python-app/tests/test_cli.py b/packages/create-awesome-python-app/tests/test_cli.py index a441512..f1f53de 100644 --- a/packages/create-awesome-python-app/tests/test_cli.py +++ b/packages/create-awesome-python-app/tests/test_cli.py @@ -7,26 +7,6 @@ from typer.testing import CliRunner runner = CliRunner() -_CPA_ENV_VARS = ( - "CPA_REFRESH", - "CPA_NO_CATALOG_CACHE", - "CPA_CACHE_DIR", - "CPA_CATALOG_FIXTURE", - "CPA_FIXTURE_DIR", -) - - -@pytest.fixture(autouse=True) -def _clean_cpa_env(monkeypatch): - from create_awesome_python_app.catalog import reset_catalog_cache_for_tests - - for name in _CPA_ENV_VARS: - monkeypatch.delenv(name, raising=False) - reset_catalog_cache_for_tests() - yield - for name in _CPA_ENV_VARS: - monkeypatch.delenv(name, raising=False) - reset_catalog_cache_for_tests() def test_version() -> None: From 01b1e813fb484f22ca76a996bb12f951b47ac0ec Mon Sep 17 00:00:00 2001 From: ulises-jeremias Date: Wed, 22 Jul 2026 01:57:30 -0300 Subject: [PATCH 4/4] fix(test): drop invalid None return type on generator fixture Pyright rejects `-> None` on autouse fixtures that yield. Co-authored-by: Cursor --- packages/create-awesome-python-app/tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-awesome-python-app/tests/conftest.py b/packages/create-awesome-python-app/tests/conftest.py index dfe8921..1b5676d 100644 --- a/packages/create-awesome-python-app/tests/conftest.py +++ b/packages/create-awesome-python-app/tests/conftest.py @@ -16,7 +16,7 @@ @pytest.fixture(autouse=True) -def _clean_cpa_process_env() -> None: +def _clean_cpa_process_env(): """Clear CPA env vars that CLI helpers set via ``os.environ`` (not monkeypatch). ``apply_fixture_mode`` mutates ``os.environ`` directly. Pairing that with