From 5330087a1f2d9c30fe5a914dd01a1356341b5b73 Mon Sep 17 00:00:00 2001 From: ulises-jeremias Date: Wed, 22 Jul 2026 11:29:41 -0300 Subject: [PATCH 1/2] fix(cli): do not treat trailing project dir as an --addons value Greedy expansion swallowed a directory that appeared immediately after the last addon when no positional was seen earlier. Peel that token back as project_directory and cover the CI/dir-last shapes in tests. Co-authored-by: Cursor --- CHANGELOG.md | 6 ++ .../create-awesome-python-app/pyproject.toml | 2 +- .../src/create_awesome_python_app/__init__.py | 2 +- .../src/create_awesome_python_app/cli.py | 18 +++- .../tests/test_cli.py | 100 ++++++++++++++++++ uv.lock | 2 +- 6 files changed, 126 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0392cee..d06c2b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.2.9 - 2026-07-22 + +### CLI / argv trailing directory + +- Stop treating a trailing `project_directory` after `--addons` / `--extend` as another addon value (e.g. `--addons a --addons b /tmp/app`). + ## 0.2.8 - 2026-07-22 ### CLI / argv diff --git a/packages/create-awesome-python-app/pyproject.toml b/packages/create-awesome-python-app/pyproject.toml index 84c85db..cbdadf3 100644 --- a/packages/create-awesome-python-app/pyproject.toml +++ b/packages/create-awesome-python-app/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "create-awesome-python-app" -version = "0.2.8" +version = "0.2.9" description = "Composable scaffolding CLI for production-ready Python apps" readme = "README.md" requires-python = ">=3.12" diff --git a/packages/create-awesome-python-app/src/create_awesome_python_app/__init__.py b/packages/create-awesome-python-app/src/create_awesome_python_app/__init__.py index 8abe760..39ab44d 100644 --- a/packages/create-awesome-python-app/src/create_awesome_python_app/__init__.py +++ b/packages/create-awesome-python-app/src/create_awesome_python_app/__init__.py @@ -1,3 +1,3 @@ """Create Awesome Python App CLI.""" -__version__ = "0.2.8" +__version__ = "0.2.9" 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 a3993cb..3d92d33 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 @@ -76,10 +76,15 @@ def _expand_variadic_option(argv: list[str], option: str) -> list[str]: Typer's ``list[str]`` Option only accepts one value per flag. Commander uses ``--addons [extensions...]``, so users naturally write space-separated lists. + + When ``project_directory`` comes *after* options and no positional was seen + yet, peel the final trailing token at EOS back as the directory so that + ``--addons a --addons b /tmp/app`` does not treat ``/tmp/app`` as an addon. """ out: list[str] = [] i = 0 prefix = option + "=" + saw_positional = False while i < len(argv): arg = argv[i] if arg == option: @@ -88,17 +93,26 @@ def _expand_variadic_option(argv: list[str], option: str) -> list[str]: while i < len(argv) and not argv[i].startswith("-"): values.append(argv[i]) i += 1 + ended_at_eos = i >= len(argv) + trailing: str | None = None + if ended_at_eos and not saw_positional and len(values) >= 2: + trailing = values.pop() if not values: out.append(option) else: for value in values: out.extend([option, value]) + if trailing is not None: + out.append(trailing) + saw_positional = True continue if arg.startswith(prefix): value = arg[len(prefix) :] out.extend([option, value] if value else [option]) i += 1 continue + if i > 0 and not arg.startswith("-"): + saw_positional = True out.append(arg) i += 1 return out @@ -106,7 +120,9 @@ def _expand_variadic_option(argv: list[str], option: str) -> list[str]: def _preprocess_cli_argv(argv: list[str] | None = None) -> list[str]: """Apply argv rewrites needed before Typer parses the CLI.""" - out = _preprocess_fixture_argv(argv) + raw = list(sys.argv if argv is None else argv) + # Pass an explicit list so fixture preprocess does not mutate sys.argv early. + out = _preprocess_fixture_argv(raw) out = _expand_variadic_option(out, "--addons") out = _expand_variadic_option(out, "--extend") if argv is None: diff --git a/packages/create-awesome-python-app/tests/test_cli.py b/packages/create-awesome-python-app/tests/test_cli.py index 1bf15d1..a402b63 100644 --- a/packages/create-awesome-python-app/tests/test_cli.py +++ b/packages/create-awesome-python-app/tests/test_cli.py @@ -320,6 +320,55 @@ def test_expand_variadic_addons_and_extend() -> None: ] +def test_expand_preserves_trailing_project_directory() -> None: + """Repeated ``--addons`` with directory last must not swallow the path.""" + from create_awesome_python_app.cli import _preprocess_cli_argv + + assert _preprocess_cli_argv( + [ + "cpa", + "--addons", + "github-setup", + "--addons", + "fastapi-docker", + "/tmp/app", + ] + ) == [ + "cpa", + "--addons", + "github-setup", + "--addons", + "fastapi-docker", + "/tmp/app", + ] + # CI-shaped argv: flags between last addon and directory. + assert _preprocess_cli_argv( + [ + "cpa", + "--template", + "fastapi-starter", + "--addons", + "fastapi-docker", + "--addons", + "github-setup", + "--no-interactive", + "--no-install", + "--force", + "/tmp/app", + ] + )[-1] == "/tmp/app" + # Space-separated addons with directory last (no prior positional). + assert _preprocess_cli_argv( + ["cpa", "--addons", "fastapi-docker", "github-setup", "/tmp/app"] + ) == [ + "cpa", + "--addons", + "fastapi-docker", + "--addons", + "github-setup", + "/tmp/app", + ] + def test_space_separated_addons_after_project_directory( tmp_path: Path, monkeypatch ) -> None: @@ -373,6 +422,57 @@ async def fake_create_python_app(project_directory, options, *_args, **_kwargs): assert any("github-setup" in a for a in addons) +def test_repeated_addons_with_directory_last(tmp_path: Path, monkeypatch) -> None: + """``--addons a --addons b `` must keep ```` as project_directory.""" + from create_awesome_python_app.cli import _preprocess_cli_argv + + tpl = tmp_path / "tpl" + tpl.mkdir() + target = tmp_path / "app" + captured: dict[str, object] = {} + + async def fake_check_for_latest_version(_package_name): + return None + + async def fake_create_python_app(project_directory, options, *_args, **_kwargs): + captured["project_directory"] = project_directory + captured["options"] = options + + monkeypatch.setattr( + "create_awesome_python_app.cli.check_for_latest_version", + fake_check_for_latest_version, + ) + monkeypatch.setattr( + "create_awesome_python_app.cli.create_python_app", + fake_create_python_app, + ) + + argv = _preprocess_cli_argv( + [ + "cpa", + "--template", + f"file://{tpl}", + "--addons", + "fastapi-docker", + "--addons", + "github-setup", + "--no-install", + "--no-interactive", + str(target), + ] + ) + result = runner.invoke(app, argv[1:]) + text = (result.stdout or "") + (result.stderr or "") + assert result.exit_code == 0, text + assert captured["project_directory"] == str(target) + options = captured["options"] + assert isinstance(options, dict) + addons = options["addons"] + assert isinstance(addons, list) + assert len(addons) == 2 + assert not any(str(target) in a for a in addons) + + def test_preprocess_fixture_argv_bare_and_with_dir() -> None: from create_awesome_python_app.cli import _FIXTURE_AUTO, _preprocess_fixture_argv diff --git a/uv.lock b/uv.lock index 2d081f5..87bbf14 100644 --- a/uv.lock +++ b/uv.lock @@ -138,7 +138,7 @@ wheels = [ [[package]] name = "create-awesome-python-app" -version = "0.2.8" +version = "0.2.9" source = { editable = "packages/create-awesome-python-app" } dependencies = [ { name = "create-python-app-core" }, From e102aa81a2de8b56ccf5ec9609560a73ff3c8532 Mon Sep 17 00:00:00 2001 From: ulises-jeremias Date: Wed, 22 Jul 2026 11:31:05 -0300 Subject: [PATCH 2/2] style: ruff format test_cli.py Co-authored-by: Cursor --- .../tests/test_cli.py | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/create-awesome-python-app/tests/test_cli.py b/packages/create-awesome-python-app/tests/test_cli.py index a402b63..19d6dd2 100644 --- a/packages/create-awesome-python-app/tests/test_cli.py +++ b/packages/create-awesome-python-app/tests/test_cli.py @@ -342,21 +342,24 @@ def test_expand_preserves_trailing_project_directory() -> None: "/tmp/app", ] # CI-shaped argv: flags between last addon and directory. - assert _preprocess_cli_argv( - [ - "cpa", - "--template", - "fastapi-starter", - "--addons", - "fastapi-docker", - "--addons", - "github-setup", - "--no-interactive", - "--no-install", - "--force", - "/tmp/app", - ] - )[-1] == "/tmp/app" + assert ( + _preprocess_cli_argv( + [ + "cpa", + "--template", + "fastapi-starter", + "--addons", + "fastapi-docker", + "--addons", + "github-setup", + "--no-interactive", + "--no-install", + "--force", + "/tmp/app", + ] + )[-1] + == "/tmp/app" + ) # Space-separated addons with directory last (no prior positional). assert _preprocess_cli_argv( ["cpa", "--addons", "fastapi-docker", "github-setup", "/tmp/app"] @@ -369,6 +372,7 @@ def test_expand_preserves_trailing_project_directory() -> None: "/tmp/app", ] + def test_space_separated_addons_after_project_directory( tmp_path: Path, monkeypatch ) -> None: