Skip to content

Commit ebdb0cd

Browse files
fix(cli): accept space-separated --addons/--extend (0.2.8) (#242)
* fix(cli): accept space-separated --addons/--extend values Typer only takes one value per flag; expand CNA-style `--addons a b` into repeated flags before parsing so the documented UX works. Co-authored-by: Cursor <cursoragent@cursor.com> * style: fix ruff import wrap and CHANGELOG MD024 Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent dae2f7c commit ebdb0cd

6 files changed

Lines changed: 155 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.8 - 2026-07-22
4+
5+
### CLI / argv
6+
7+
- Accept space-separated `--addons` / `--extend` values (CNA Commander parity): `--addons fastapi-docker github-setup` expands to repeated flags before Typer parses.
8+
39
## 0.2.7 - 2026-07-22
410

511
### CLI

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.7"
3+
version = "0.2.8"
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.7"
3+
__version__ = "0.2.8"

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,49 @@ def _preprocess_fixture_argv(argv: list[str] | None = None) -> list[str]:
7171
return out
7272

7373

74+
def _expand_variadic_option(argv: list[str], option: str) -> list[str]:
75+
"""Expand ``--addons a b`` into ``--addons a --addons b`` (CNA Commander parity).
76+
77+
Typer's ``list[str]`` Option only accepts one value per flag. Commander uses
78+
``--addons [extensions...]``, so users naturally write space-separated lists.
79+
"""
80+
out: list[str] = []
81+
i = 0
82+
prefix = option + "="
83+
while i < len(argv):
84+
arg = argv[i]
85+
if arg == option:
86+
i += 1
87+
values: list[str] = []
88+
while i < len(argv) and not argv[i].startswith("-"):
89+
values.append(argv[i])
90+
i += 1
91+
if not values:
92+
out.append(option)
93+
else:
94+
for value in values:
95+
out.extend([option, value])
96+
continue
97+
if arg.startswith(prefix):
98+
value = arg[len(prefix) :]
99+
out.extend([option, value] if value else [option])
100+
i += 1
101+
continue
102+
out.append(arg)
103+
i += 1
104+
return out
105+
106+
107+
def _preprocess_cli_argv(argv: list[str] | None = None) -> list[str]:
108+
"""Apply argv rewrites needed before Typer parses the CLI."""
109+
out = _preprocess_fixture_argv(argv)
110+
out = _expand_variadic_option(out, "--addons")
111+
out = _expand_variadic_option(out, "--extend")
112+
if argv is None:
113+
sys.argv = out
114+
return out
115+
116+
74117
def apply_fixture_mode(fixture: str | None) -> None:
75118
"""Translate ``--fixture`` into ``CPA_CATALOG_FIXTURE`` / ``CPA_FIXTURE_DIR``."""
76119
if fixture is None and os.environ.get("CPA_CATALOG_FIXTURE") != "1":
@@ -204,7 +247,7 @@ def main() -> None:
204247
treat `cache` as project_directory).
205248
"""
206249
check_python_version(">=3.12", "create-awesome-python-app")
207-
_preprocess_fixture_argv()
250+
_preprocess_cli_argv()
208251
if len(sys.argv) > 1 and sys.argv[1] == "cache":
209252
sys.argv = [sys.argv[0], *sys.argv[2:]]
210253
cache_app(prog_name="create-awesome-python-app cache")

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

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,108 @@ async def fake_create_python_app(project_directory, options, *_args, **_kwargs):
271271
assert options["template"] == f"file://{tpl}"
272272

273273

274+
def test_expand_variadic_addons_and_extend() -> None:
275+
from create_awesome_python_app.cli import (
276+
_expand_variadic_option,
277+
_preprocess_cli_argv,
278+
)
279+
280+
assert _expand_variadic_option(
281+
["cpa", "my-api", "--addons", "fastapi-docker", "github-setup", "--no-install"],
282+
"--addons",
283+
) == [
284+
"cpa",
285+
"my-api",
286+
"--addons",
287+
"fastapi-docker",
288+
"--addons",
289+
"github-setup",
290+
"--no-install",
291+
]
292+
assert _preprocess_cli_argv(
293+
[
294+
"cpa",
295+
"my-api",
296+
"--template",
297+
"fastapi-starter",
298+
"--addons",
299+
"fastapi-docker",
300+
"github-setup",
301+
"--extend",
302+
"a",
303+
"b",
304+
"--no-interactive",
305+
]
306+
) == [
307+
"cpa",
308+
"my-api",
309+
"--template",
310+
"fastapi-starter",
311+
"--addons",
312+
"fastapi-docker",
313+
"--addons",
314+
"github-setup",
315+
"--extend",
316+
"a",
317+
"--extend",
318+
"b",
319+
"--no-interactive",
320+
]
321+
322+
323+
def test_space_separated_addons_after_project_directory(
324+
tmp_path: Path, monkeypatch
325+
) -> None:
326+
"""CNA parity: ``--addons fastapi-docker github-setup`` (one flag, many values)."""
327+
from create_awesome_python_app.cli import _preprocess_cli_argv
328+
329+
tpl = tmp_path / "tpl"
330+
tpl.mkdir()
331+
captured: dict[str, object] = {}
332+
333+
async def fake_check_for_latest_version(_package_name):
334+
return None
335+
336+
async def fake_create_python_app(project_directory, options, *_args, **_kwargs):
337+
captured["project_directory"] = project_directory
338+
captured["options"] = options
339+
340+
monkeypatch.setattr(
341+
"create_awesome_python_app.cli.check_for_latest_version",
342+
fake_check_for_latest_version,
343+
)
344+
monkeypatch.setattr(
345+
"create_awesome_python_app.cli.create_python_app",
346+
fake_create_python_app,
347+
)
348+
349+
argv = _preprocess_cli_argv(
350+
[
351+
"cpa",
352+
"my-api",
353+
"--template",
354+
f"file://{tpl}",
355+
"--addons",
356+
"fastapi-docker",
357+
"github-setup",
358+
"--no-install",
359+
"--no-interactive",
360+
]
361+
)
362+
result = runner.invoke(app, argv[1:])
363+
364+
text = (result.stdout or "") + (result.stderr or "")
365+
assert result.exit_code == 0, text
366+
assert "unexpected extra argument" not in text.lower()
367+
options = captured["options"]
368+
assert isinstance(options, dict)
369+
addons = options["addons"]
370+
assert isinstance(addons, list)
371+
assert len(addons) == 2
372+
assert any("fastapi-docker" in a for a in addons)
373+
assert any("github-setup" in a for a in addons)
374+
375+
274376
def test_preprocess_fixture_argv_bare_and_with_dir() -> None:
275377
from create_awesome_python_app.cli import _FIXTURE_AUTO, _preprocess_fixture_argv
276378

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)