Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
# Changelog

## 0.2.8 - 2026-07-22

### CLI / argv

- Accept space-separated `--addons` / `--extend` values (CNA Commander parity): `--addons fastapi-docker github-setup` expands to repeated flags before Typer parses.

## 0.2.7 - 2026-07-22

### CLI
Expand Down
2 changes: 1 addition & 1 deletion packages/create-awesome-python-app/pyproject.toml
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
[project]
name = "create-awesome-python-app"
version = "0.2.7"
version = "0.2.8"
description = "Composable scaffolding CLI for production-ready Python apps"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
"""Create Awesome Python App CLI."""

__version__ = "0.2.7"
__version__ = "0.2.8"
Original file line numberDiff line numberDiff line change
Expand Up@@ -71,6 +71,49 @@ def _preprocess_fixture_argv(argv: list[str] | None = None) -> list[str]:
return out


def _expand_variadic_option(argv: list[str], option: str) -> list[str]:
"""Expand ``--addons a b`` into ``--addons a --addons b`` (CNA Commander parity).

Typer's ``list[str]`` Option only accepts one value per flag. Commander uses
``--addons [extensions...]``, so users naturally write space-separated lists.
"""
out: list[str] = []
i = 0
prefix = option + "="
while i < len(argv):
arg = argv[i]
if arg == option:
i += 1
values: list[str] = []
while i < len(argv) and not argv[i].startswith("-"):
values.append(argv[i])
i += 1
if not values:
out.append(option)
else:
for value in values:
out.extend([option, value])
continue
if arg.startswith(prefix):
value = arg[len(prefix) :]
out.extend([option, value] if value else [option])
i += 1
continue
out.append(arg)
i += 1
return out


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)
out = _expand_variadic_option(out, "--addons")
out = _expand_variadic_option(out, "--extend")
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":
Expand DownExpand Up@@ -204,7 +247,7 @@ def main() -> None:
treat `cache` as project_directory).
"""
check_python_version(">=3.12", "create-awesome-python-app")
_preprocess_fixture_argv()
_preprocess_cli_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")
Expand Down
102 changes: 102 additions & 0 deletions packages/create-awesome-python-app/tests/test_cli.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -271,6 +271,108 @@ async def fake_create_python_app(project_directory, options, *_args, **_kwargs):
assert options["template"] == f"file://{tpl}"


def test_expand_variadic_addons_and_extend() -> None:
from create_awesome_python_app.cli import (
_expand_variadic_option,
_preprocess_cli_argv,
)

assert _expand_variadic_option(
["cpa", "my-api", "--addons", "fastapi-docker", "github-setup", "--no-install"],
"--addons",
) == [
"cpa",
"my-api",
"--addons",
"fastapi-docker",
"--addons",
"github-setup",
"--no-install",
]
assert _preprocess_cli_argv(
[
"cpa",
"my-api",
"--template",
"fastapi-starter",
"--addons",
"fastapi-docker",
"github-setup",
"--extend",
"a",
"b",
"--no-interactive",
]
) == [
"cpa",
"my-api",
"--template",
"fastapi-starter",
"--addons",
"fastapi-docker",
"--addons",
"github-setup",
"--extend",
"a",
"--extend",
"b",
"--no-interactive",
]


def test_space_separated_addons_after_project_directory(
tmp_path: Path, monkeypatch
) -> None:
"""CNA parity: ``--addons fastapi-docker github-setup`` (one flag, many values)."""
from create_awesome_python_app.cli import _preprocess_cli_argv

tpl = tmp_path / "tpl"
tpl.mkdir()
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",
"my-api",
"--template",
f"file://{tpl}",
"--addons",
"fastapi-docker",
"github-setup",
"--no-install",
"--no-interactive",
]
)
result = runner.invoke(app, argv[1:])

text = (result.stdout or "") + (result.stderr or "")
assert result.exit_code == 0, text
assert "unexpected extra argument" not in text.lower()
options = captured["options"]
assert isinstance(options, dict)
addons = options["addons"]
assert isinstance(addons, list)
assert len(addons) == 2
assert any("fastapi-docker" in a for a in addons)
assert any("github-setup" 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

Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading