Skip to content

Commit a531aad

Browse files
fix(cli): preserve trailing project dir after --addons (0.2.9) (#243)
* 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 <cursoragent@cursor.com> * style: ruff format test_cli.py Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ebdb0cd commit a531aad

6 files changed

Lines changed: 130 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## 0.2.9 - 2026-07-22
4+
5+
### CLI / argv trailing directory
6+
7+
- Stop treating a trailing `project_directory` after `--addons` / `--extend` as another addon value (e.g. `--addons a --addons b /tmp/app`).
8+
39
## 0.2.8 - 2026-07-22
410

511
### CLI / argv

packages/create-awesome-python-app/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "create-awesome-python-app"
3-
version = "0.2.8"
3+
version = "0.2.9"
44
description = "Composable scaffolding CLI for production-ready Python apps"
55
readme = "README.md"
66
requires-python = ">=3.12"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Create Awesome Python App CLI."""
22

3-
__version__ = "0.2.8"
3+
__version__ = "0.2.9"

packages/create-awesome-python-app/src/create_awesome_python_app/cli.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,15 @@ def _expand_variadic_option(argv: list[str], option: str) -> list[str]:
7676
7777
Typer's ``list[str]`` Option only accepts one value per flag. Commander uses
7878
``--addons [extensions...]``, so users naturally write space-separated lists.
79+
80+
When ``project_directory`` comes *after* options and no positional was seen
81+
yet, peel the final trailing token at EOS back as the directory so that
82+
``--addons a --addons b /tmp/app`` does not treat ``/tmp/app`` as an addon.
7983
"""
8084
out: list[str] = []
8185
i = 0
8286
prefix = option + "="
87+
saw_positional = False
8388
while i < len(argv):
8489
arg = argv[i]
8590
if arg == option:
@@ -88,25 +93,36 @@ def _expand_variadic_option(argv: list[str], option: str) -> list[str]:
8893
while i < len(argv) and not argv[i].startswith("-"):
8994
values.append(argv[i])
9095
i += 1
96+
ended_at_eos = i >= len(argv)
97+
trailing: str | None = None
98+
if ended_at_eos and not saw_positional and len(values) >= 2:
99+
trailing = values.pop()
91100
if not values:
92101
out.append(option)
93102
else:
94103
for value in values:
95104
out.extend([option, value])
105+
if trailing is not None:
106+
out.append(trailing)
107+
saw_positional = True
96108
continue
97109
if arg.startswith(prefix):
98110
value = arg[len(prefix) :]
99111
out.extend([option, value] if value else [option])
100112
i += 1
101113
continue
114+
if i > 0 and not arg.startswith("-"):
115+
saw_positional = True
102116
out.append(arg)
103117
i += 1
104118
return out
105119

106120

107121
def _preprocess_cli_argv(argv: list[str] | None = None) -> list[str]:
108122
"""Apply argv rewrites needed before Typer parses the CLI."""
109-
out = _preprocess_fixture_argv(argv)
123+
raw = list(sys.argv if argv is None else argv)
124+
# Pass an explicit list so fixture preprocess does not mutate sys.argv early.
125+
out = _preprocess_fixture_argv(raw)
110126
out = _expand_variadic_option(out, "--addons")
111127
out = _expand_variadic_option(out, "--extend")
112128
if argv is None:

packages/create-awesome-python-app/tests/test_cli.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,59 @@ def test_expand_variadic_addons_and_extend() -> None:
320320
]
321321

322322

323+
def test_expand_preserves_trailing_project_directory() -> None:
324+
"""Repeated ``--addons`` with directory last must not swallow the path."""
325+
from create_awesome_python_app.cli import _preprocess_cli_argv
326+
327+
assert _preprocess_cli_argv(
328+
[
329+
"cpa",
330+
"--addons",
331+
"github-setup",
332+
"--addons",
333+
"fastapi-docker",
334+
"/tmp/app",
335+
]
336+
) == [
337+
"cpa",
338+
"--addons",
339+
"github-setup",
340+
"--addons",
341+
"fastapi-docker",
342+
"/tmp/app",
343+
]
344+
# CI-shaped argv: flags between last addon and directory.
345+
assert (
346+
_preprocess_cli_argv(
347+
[
348+
"cpa",
349+
"--template",
350+
"fastapi-starter",
351+
"--addons",
352+
"fastapi-docker",
353+
"--addons",
354+
"github-setup",
355+
"--no-interactive",
356+
"--no-install",
357+
"--force",
358+
"/tmp/app",
359+
]
360+
)[-1]
361+
== "/tmp/app"
362+
)
363+
# Space-separated addons with directory last (no prior positional).
364+
assert _preprocess_cli_argv(
365+
["cpa", "--addons", "fastapi-docker", "github-setup", "/tmp/app"]
366+
) == [
367+
"cpa",
368+
"--addons",
369+
"fastapi-docker",
370+
"--addons",
371+
"github-setup",
372+
"/tmp/app",
373+
]
374+
375+
323376
def test_space_separated_addons_after_project_directory(
324377
tmp_path: Path, monkeypatch
325378
) -> None:
@@ -373,6 +426,57 @@ async def fake_create_python_app(project_directory, options, *_args, **_kwargs):
373426
assert any("github-setup" in a for a in addons)
374427

375428

429+
def test_repeated_addons_with_directory_last(tmp_path: Path, monkeypatch) -> None:
430+
"""``--addons a --addons b <dir>`` must keep ``<dir>`` as project_directory."""
431+
from create_awesome_python_app.cli import _preprocess_cli_argv
432+
433+
tpl = tmp_path / "tpl"
434+
tpl.mkdir()
435+
target = tmp_path / "app"
436+
captured: dict[str, object] = {}
437+
438+
async def fake_check_for_latest_version(_package_name):
439+
return None
440+
441+
async def fake_create_python_app(project_directory, options, *_args, **_kwargs):
442+
captured["project_directory"] = project_directory
443+
captured["options"] = options
444+
445+
monkeypatch.setattr(
446+
"create_awesome_python_app.cli.check_for_latest_version",
447+
fake_check_for_latest_version,
448+
)
449+
monkeypatch.setattr(
450+
"create_awesome_python_app.cli.create_python_app",
451+
fake_create_python_app,
452+
)
453+
454+
argv = _preprocess_cli_argv(
455+
[
456+
"cpa",
457+
"--template",
458+
f"file://{tpl}",
459+
"--addons",
460+
"fastapi-docker",
461+
"--addons",
462+
"github-setup",
463+
"--no-install",
464+
"--no-interactive",
465+
str(target),
466+
]
467+
)
468+
result = runner.invoke(app, argv[1:])
469+
text = (result.stdout or "") + (result.stderr or "")
470+
assert result.exit_code == 0, text
471+
assert captured["project_directory"] == str(target)
472+
options = captured["options"]
473+
assert isinstance(options, dict)
474+
addons = options["addons"]
475+
assert isinstance(addons, list)
476+
assert len(addons) == 2
477+
assert not any(str(target) in a for a in addons)
478+
479+
376480
def test_preprocess_fixture_argv_bare_and_with_dir() -> None:
377481
from create_awesome_python_app.cli import _FIXTURE_AUTO, _preprocess_fixture_argv
378482

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)