Skip to content

Commit 48a39c0

Browse files
feat(cli): scaffold spinner, shell completion docs, Spanish contributing
- Rich progress spinner around the scaffold phase on interactive TTYs only (disabled in CI/pipes/--json) (closes #269) - docs/SHELL_COMPLETION.md: install + per-shell setup for the built-in Typer completion (closes #270) - CONTRIBUTING.es.md: condensed Spanish onboarding translation, English stays source of truth (closes #265)
1 parent 53cc210 commit 48a39c0

4 files changed

Lines changed: 147 additions & 17 deletions

File tree

‎CONTRIBUTING.es.md‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Contribuir a Create Awesome Python App (resumen en español)
2+
3+
> El inglés es la fuente de verdad. Esta es una traducción condensada de las
4+
> secciones de incorporación de `CONTRIBUTING.md`.
5+
6+
Gracias por contribuir. Sigue el
7+
[Código de Conducta](./.github/CODE_OF_CONDUCT.md).
8+
9+
## Desarrollo local
10+
11+
```bash
12+
git clone https://github.com/Create-Python-App/create-python-app.git
13+
cd create-python-app
14+
uv sync --group dev
15+
uv run pre-commit install
16+
make test
17+
make lint
18+
make typecheck
19+
```
20+
21+
## Probar con fixtures (sin red)
22+
23+
El CLI puede funcionar sin conexión con el catálogo de fixtures:
24+
25+
```bash
26+
uv run create-awesome-python-app --fixture . --list-templates
27+
```
28+
29+
## Pull requests
30+
31+
1. Crea tu rama desde `main`.
32+
2. Cambios enfocados; enlaza el issue correspondiente.
33+
3. Usa [Conventional Commits](https://www.conventionalcommits.org/) (mensajes en inglés).
34+
4. Asegúrate de que pasen los tests, el lint (`ruff`) y los tipos (`pyright`).
35+
5. Completa la plantilla del PR (en inglés).
36+
37+
## Dónde vive cada cosa
38+
39+
- Motor del CLI: `packages/`.
40+
- Plantillas y extensiones: [`cpa-templates`](https://github.com/Create-Python-App/cpa-templates).
41+
- Documentación del sitio: [`website`](https://github.com/Create-Python-App/website).

‎docs/SHELL_COMPLETION.md‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Shell completion
2+
3+
`create-awesome-python-app` ships tab completion via Typer (bash, zsh, fish,
4+
PowerShell). Completion covers flags and the `cache` subcommands; catalog
5+
values (template slugs) are intentionally not completed yet.
6+
7+
## Quick install
8+
9+
```bash
10+
# Detect the current shell from $SHELL and install
11+
create-awesome-python-app --install-completion
12+
```
13+
14+
Restart the shell (or `exec "$SHELL" -l`) so the new script loads.
15+
16+
## Manual install per shell
17+
18+
```bash
19+
# bash — append to ~/.bashrc
20+
create-awesome-python-app --show-completion >> ~/.bash_completion
21+
# zsh — needs compinit; save to a fpath entry
22+
create-awesome-python-app --show-completion > ~/.zfunc/_create-awesome-python-app
23+
# fish
24+
create-awesome-python-app --show-completion > ~/.config/fish/completions/create-awesome-python-app.fish
25+
# PowerShell — append to $PROFILE
26+
create-awesome-python-app --show-completion >> $PROFILE
27+
```
28+
29+
If you see `Shell sh not supported`, export a real shell first
30+
(`export SHELL=/bin/bash`) — completion detection reads `$SHELL`.
31+
32+
## uvx users
33+
34+
Completion attaches to the shell, not the installer, so it works the same
35+
under `uvx create-awesome-python-app`. Run `--install-completion` once with
36+
any installed copy.

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

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import asyncio
66
import os
77
import sys
8+
from contextlib import contextmanager
89
from pathlib import Path
910
from typing import Any
1011

@@ -166,6 +167,21 @@ def _in_ci() -> bool:
166167
return os.environ.get("CI", "").lower() in {"1", "true", "yes"}
167168

168169

170+
def _spinner_enabled(*, interactive: bool) -> bool:
171+
"""Progress spinner on interactive TTYs only (never CI/pipes/--json)."""
172+
return bool(interactive) and sys.stderr.isatty() and not _in_ci()
173+
174+
175+
@contextmanager
176+
def _phase_status(message: str, *, enabled: bool):
177+
"""Rich status spinner on stderr while a long phase runs (no-op when disabled)."""
178+
if not enabled:
179+
yield
180+
return
181+
with console.status(f"[cyan]{message}[/cyan]", spinner="dots"):
182+
yield
183+
184+
169185
def _template_config_path(source_subdir: str | None, root: Path) -> Path:
170186
cfg_path = root / "cpa.config.json"
171187
if not cfg_path.is_file() and source_subdir:
@@ -575,25 +591,27 @@ def scaffold(
575591
raise typer.Exit(1)
576592
console.print(f"[yellow]{msg}[/yellow]")
577593

594+
show_progress = _spinner_enabled(interactive=want_interactive)
578595
try:
579-
asyncio.run(
580-
create_python_app(
581-
target_directory,
582-
{
583-
"template": template,
584-
"addons": addons or [],
585-
"extend": extend or [],
586-
"install": not no_install,
587-
"force": force,
588-
"verbose": verbose,
589-
"offline": offline,
590-
"refresh": effective_refresh,
591-
"keep_on_failure": keep_on_failure,
592-
"cache_dir": str(cache_dir) if cache_dir else None,
593-
"set": set_map,
594-
},
596+
with _phase_status("Scaffolding project…", enabled=show_progress):
597+
asyncio.run(
598+
create_python_app(
599+
target_directory,
600+
{
601+
"template": template,
602+
"addons": addons or [],
603+
"extend": extend or [],
604+
"install": not no_install,
605+
"force": force,
606+
"verbose": verbose,
607+
"offline": offline,
608+
"refresh": effective_refresh,
609+
"keep_on_failure": keep_on_failure,
610+
"cache_dir": str(cache_dir) if cache_dir else None,
611+
"set": set_map,
612+
},
613+
)
595614
)
596-
)
597615
except NonEmptyTargetDirectoryError as err:
598616
# Safety net if the target fills up after the early check (e.g. during
599617
# a long interactive session).
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Tests for the scaffold progress spinner (#269)."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
from create_awesome_python_app.cli import _phase_status, _spinner_enabled
7+
8+
9+
def test_spinner_disabled_without_interactive(monkeypatch: pytest.MonkeyPatch) -> None:
10+
monkeypatch.setattr("sys.stderr.isatty", lambda: True)
11+
monkeypatch.delenv("CI", raising=False)
12+
assert _spinner_enabled(interactive=False) is False
13+
14+
15+
def test_spinner_disabled_in_ci(monkeypatch: pytest.MonkeyPatch) -> None:
16+
monkeypatch.setattr("sys.stderr.isatty", lambda: True)
17+
monkeypatch.setenv("CI", "1")
18+
assert _spinner_enabled(interactive=True) is False
19+
20+
21+
def test_spinner_disabled_without_tty(monkeypatch: pytest.MonkeyPatch) -> None:
22+
monkeypatch.setattr("sys.stderr.isatty", lambda: False)
23+
monkeypatch.delenv("CI", raising=False)
24+
assert _spinner_enabled(interactive=True) is False
25+
26+
27+
def test_spinner_enabled_on_interactive_tty(monkeypatch: pytest.MonkeyPatch) -> None:
28+
monkeypatch.setattr("sys.stderr.isatty", lambda: True)
29+
monkeypatch.delenv("CI", raising=False)
30+
assert _spinner_enabled(interactive=True) is True
31+
32+
33+
def test_phase_status_noop_when_disabled() -> None:
34+
with _phase_status("Working…", enabled=False):
35+
pass

0 commit comments

Comments
 (0)