create-awesome-python-app 0.2.7__tar.gz → 0.2.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/PKG-INFO +1 -1
  2. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/pyproject.toml +1 -1
  3. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/src/create_awesome_python_app/__init__.py +1 -1
  4. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/src/create_awesome_python_app/cli.py +60 -1
  5. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/tests/test_cli.py +206 -0
  6. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/.gitignore +0 -0
  7. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/README.md +0 -0
  8. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/assets/hero.svg +0 -0
  9. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/src/create_awesome_python_app/cache.py +0 -0
  10. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/src/create_awesome_python_app/catalog.py +0 -0
  11. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/src/create_awesome_python_app/prompt_style.py +0 -0
  12. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/tests/conftest.py +0 -0
  13. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/tests/test_cache_cmds.py +0 -0
  14. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/tests/test_cache_env.py +0 -0
  15. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/tests/test_catalog.py +0 -0
  16. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/tests/test_catalog_fetch.py +0 -0
  17. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/tests/test_catalog_resolve.py +0 -0
  18. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/tests/test_cpa_templates_integration.py +0 -0
  19. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/tests/test_flags.py +0 -0
  20. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/tests/test_interactive.py +0 -0
  21. {create_awesome_python_app-0.2.7 → create_awesome_python_app-0.2.9}/tests/test_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: create-awesome-python-app
3
- Version: 0.2.7
3
+ Version: 0.2.9
4
4
  Summary: Composable scaffolding CLI for production-ready Python apps
5
5
  Project-URL: Homepage, https://github.com/Create-Python-App/create-python-app
6
6
  Project-URL: Repository, https://github.com/Create-Python-App/create-python-app
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "create-awesome-python-app"
3
- version = "0.2.7"
3
+ version = "0.2.9"
4
4
  description = "Composable scaffolding CLI for production-ready Python apps"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,3 +1,3 @@
1
1
  """Create Awesome Python App CLI."""
2
2
 
3
- __version__ = "0.2.7"
3
+ __version__ = "0.2.9"
@@ -71,6 +71,65 @@ def _preprocess_fixture_argv(argv: list[str] | None = None) -> list[str]:
71
71
  return out
72
72
 
73
73
 
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
+ 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.
83
+ """
84
+ out: list[str] = []
85
+ i = 0
86
+ prefix = option + "="
87
+ saw_positional = False
88
+ while i < len(argv):
89
+ arg = argv[i]
90
+ if arg == option:
91
+ i += 1
92
+ values: list[str] = []
93
+ while i < len(argv) and not argv[i].startswith("-"):
94
+ values.append(argv[i])
95
+ 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()
100
+ if not values:
101
+ out.append(option)
102
+ else:
103
+ for value in values:
104
+ out.extend([option, value])
105
+ if trailing is not None:
106
+ out.append(trailing)
107
+ saw_positional = True
108
+ continue
109
+ if arg.startswith(prefix):
110
+ value = arg[len(prefix) :]
111
+ out.extend([option, value] if value else [option])
112
+ i += 1
113
+ continue
114
+ if i > 0 and not arg.startswith("-"):
115
+ saw_positional = True
116
+ out.append(arg)
117
+ i += 1
118
+ return out
119
+
120
+
121
+ def _preprocess_cli_argv(argv: list[str] | None = None) -> list[str]:
122
+ """Apply argv rewrites needed before Typer parses the CLI."""
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)
126
+ out = _expand_variadic_option(out, "--addons")
127
+ out = _expand_variadic_option(out, "--extend")
128
+ if argv is None:
129
+ sys.argv = out
130
+ return out
131
+
132
+
74
133
  def apply_fixture_mode(fixture: str | None) -> None:
75
134
  """Translate ``--fixture`` into ``CPA_CATALOG_FIXTURE`` / ``CPA_FIXTURE_DIR``."""
76
135
  if fixture is None and os.environ.get("CPA_CATALOG_FIXTURE") != "1":
@@ -204,7 +263,7 @@ def main() -> None:
204
263
  treat `cache` as project_directory).
205
264
  """
206
265
  check_python_version(">=3.12", "create-awesome-python-app")
207
- _preprocess_fixture_argv()
266
+ _preprocess_cli_argv()
208
267
  if len(sys.argv) > 1 and sys.argv[1] == "cache":
209
268
  sys.argv = [sys.argv[0], *sys.argv[2:]]
210
269
  cache_app(prog_name="create-awesome-python-app cache")
@@ -271,6 +271,212 @@ def test_options_after_project_directory(tmp_path: Path, monkeypatch) -> None:
271
271
  assert options["template"] == f"file://{tpl}"
272
272
 
273
273
 
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_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
+
376
+ def test_space_separated_addons_after_project_directory(
377
+ tmp_path: Path, monkeypatch
378
+ ) -> None:
379
+ """CNA parity: ``--addons fastapi-docker github-setup`` (one flag, many values)."""
380
+ from create_awesome_python_app.cli import _preprocess_cli_argv
381
+
382
+ tpl = tmp_path / "tpl"
383
+ tpl.mkdir()
384
+ captured: dict[str, object] = {}
385
+
386
+ async def fake_check_for_latest_version(_package_name):
387
+ return None
388
+
389
+ async def fake_create_python_app(project_directory, options, *_args, **_kwargs):
390
+ captured["project_directory"] = project_directory
391
+ captured["options"] = options
392
+
393
+ monkeypatch.setattr(
394
+ "create_awesome_python_app.cli.check_for_latest_version",
395
+ fake_check_for_latest_version,
396
+ )
397
+ monkeypatch.setattr(
398
+ "create_awesome_python_app.cli.create_python_app",
399
+ fake_create_python_app,
400
+ )
401
+
402
+ argv = _preprocess_cli_argv(
403
+ [
404
+ "cpa",
405
+ "my-api",
406
+ "--template",
407
+ f"file://{tpl}",
408
+ "--addons",
409
+ "fastapi-docker",
410
+ "github-setup",
411
+ "--no-install",
412
+ "--no-interactive",
413
+ ]
414
+ )
415
+ result = runner.invoke(app, argv[1:])
416
+
417
+ text = (result.stdout or "") + (result.stderr or "")
418
+ assert result.exit_code == 0, text
419
+ assert "unexpected extra argument" not in text.lower()
420
+ options = captured["options"]
421
+ assert isinstance(options, dict)
422
+ addons = options["addons"]
423
+ assert isinstance(addons, list)
424
+ assert len(addons) == 2
425
+ assert any("fastapi-docker" in a for a in addons)
426
+ assert any("github-setup" in a for a in addons)
427
+
428
+
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
+
274
480
  def test_preprocess_fixture_argv_bare_and_with_dir() -> None:
275
481
  from create_awesome_python_app.cli import _FIXTURE_AUTO, _preprocess_fixture_argv
276
482