Skip to content

Commit 99594fd

Browse files
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>
1 parent dae2f7c commit 99594fd

6 files changed

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

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)