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.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
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.8"
version = "0.2.9"
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.8"
__version__ = "0.2.9"
Original file line numberDiff line numberDiff line change
Expand Up@@ -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:
Expand All@@ -88,25 +93,36 @@ 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


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:
Expand Down
104 changes: 104 additions & 0 deletions packages/create-awesome-python-app/tests/test_cli.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -320,6 +320,59 @@ 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:
Expand DownExpand Up@@ -373,6 +426,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 <dir>`` must keep ``<dir>`` 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

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