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
2 changes: 2 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
## Summary

-

## Test plan

- [ ] `make test`
- [ ] `make lint`
- [ ] `make typecheck`
Expand Down
38 changes: 24 additions & 14 deletions .github/workflows/smoke-distribution.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -121,20 +121,30 @@ jobs:
run: |
set -euo pipefail

PYPI=$(curl -sfL https://pypi.org/pypi/create-awesome-python-app/json | \
python3 -c "import json,sys; print(json.load(sys.stdin)['info']['version'])")

DOCKER=$(docker run --rm ulisesjeremias/create-awesome-python-app:latest --version 2>&1 \
|| echo "unavailable")

HOMEBREW=$(curl -sfL \
https://raw.githubusercontent.com/Create-Python-App/homebrew-tap/main/Formula/create-awesome-python-app.rb | \
grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unavailable")

AUR=$(curl -sfL \
"https://aur.archlinux.org/rpc/v5/info?arg=create-awesome-python-app" | \
python3 -c "import json,sys; d=json.load(sys.stdin); r=d.get('results') or []; print(r[0]['Version'].split('-')[0] if r else 'unavailable')" \
|| echo "unavailable")
PYPI=$(
curl -sfL https://pypi.org/pypi/create-awesome-python-app/json \
| python3 -c "import json,sys; print(json.load(sys.stdin)['info']['version'])"
)

DOCKER=$(
docker run --rm ulisesjeremias/create-awesome-python-app:latest --version 2>&1 \
|| echo "unavailable"
)

FORMULA_URL="https://raw.githubusercontent.com/Create-Python-App/homebrew-tap/main/Formula/create-awesome-python-app.rb"
HOMEBREW=$(
curl -sfL "$FORMULA_URL" \
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' \
| head -1 \
|| echo "unavailable"
)

AUR_RPC="https://aur.archlinux.org/rpc/v5/info?arg=create-awesome-python-app"
AUR=$(
curl -sfL "$AUR_RPC" \
| python3 -c "import json,sys; d=json.load(sys.stdin); r=d.get('results') or []; print(r[0]['Version'].split('-')[0] if r else 'unavailable')" \
|| echo "unavailable"
)

echo "Channel versions:"
echo " pypi: $PYPI"
Expand Down
3 changes: 2 additions & 1 deletion .markdownlint.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,6 @@
"default": true,
"MD013": false,
"MD033": false,
"MD041": false
"MD041": false,
"MD060": false
}
19 changes: 17 additions & 2 deletions .mega-linter.yml
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
# MegaLinter — Python-first config for create-python-app
# Adapted from Create-Node-App/create-node-app .mega-linter.yml
#
# Project standards (AGENTS.md): Ruff + Pyright. Disable overlapping
# MegaLinter Python tools that duplicate those checks or fail without
# the workspace venv (pylint import-error, etc.).

APPLY_FIXES: all

Expand All@@ -12,11 +16,22 @@ DISABLE_LINTERS:
- JSON_V8R
- REPOSITORY_CHECKOV
- REPOSITORY_GRYPE
- REPOSITORY_OSV_SCANNER # Handled later by dedicated workflow (#51)
- REPOSITORY_OSV_SCANNER # Handled by dedicated workflow
- REPOSITORY_TRIVY
- SPELL_CSPELL
- SPELL_LYCHEE
- YAML_V8R
# Overlap with Ruff / Pyright (project tooling + dedicated type-check.yml)
- PYTHON_PYLINT
- PYTHON_FLAKE8
- PYTHON_BLACK
- PYTHON_ISORT
- PYTHON_MYPY
- PYTHON_PYRIGHT
- PYTHON_BANDIT
- PYTHON_RUFF
# Badge / package pages can 404 briefly after first publish
- MARKDOWN_MARKDOWN_LINK_CHECK

SHOW_ELAPSED_TIME: true
FILTER_REGEX_EXCLUDE: (uv\.lock)
FILTER_REGEX_EXCLUDE: (uv\.lock|megalinter-reports/)
5 changes: 4 additions & 1 deletion .yamllint.yml
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
extends: default
rules:
line-length:
max: 120
max: 180
allow-non-breakable-inline-mappings: true
document-start: disable
truthy:
allowed-values: ["true", "false", "on", "off"]
check-keys: false
10 changes: 5 additions & 5 deletions Dockerfile
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,11 +8,11 @@ ARG VERSION=latest

# hadolint ignore=DL3013
RUN useradd --create-home --uid 1000 --shell /bin/bash app \
&& if [ "$VERSION" = "latest" ]; then \
pip install --no-cache-dir create-awesome-python-app; \
else \
pip install --no-cache-dir "create-awesome-python-app==${VERSION}"; \
fi
&& if [ "$VERSION" = "latest" ]; then \
pip install --no-cache-dir create-awesome-python-app; \
else \
pip install --no-cache-dir "create-awesome-python-app==${VERSION}"; \
fi

USER app
WORKDIR /home/app
Expand Down
8 changes: 5 additions & 3 deletions docs/CPA_TEMPLATES_TRACKING.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
# Tracking: `cpa-templates` + website

Previously tracked by https://github.com/Create-Python-App/create-python-app/issues/44 (closed).
Previously tracked by
[issue #44](https://github.com/Create-Python-App/create-python-app/issues/44)
(closed).

## Status

Expand All@@ -15,8 +17,8 @@ Previously tracked by https://github.com/Create-Python-App/create-python-app/iss

| Repo | URL |
|------|-----|
| CLI monorepo | https://github.com/Create-Python-App/create-python-app |
| Template bank | https://github.com/Create-Python-App/cpa-templates |
| CLI monorepo | [create-python-app](https://github.com/Create-Python-App/create-python-app) |
| Template bank | [cpa-templates](https://github.com/Create-Python-App/cpa-templates) |

## CNA parity reference

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,9 +18,7 @@

console = Console(stderr=True)

DEFAULT_CATALOG_URL = (
"https://raw.githubusercontent.com/Create-Python-App/cpa-templates/main/templates.json"
)
DEFAULT_CATALOG_URL = "https://raw.githubusercontent.com/Create-Python-App/cpa-templates/main/templates.json"
CACHE_TTL_SECONDS = 3600
FETCH_TIMEOUT_SECONDS = 10
USER_AGENT = f"create-awesome-python-app/{__version__} (https://github.com/Create-Python-App/create-python-app)"
Expand DownExpand Up@@ -111,22 +109,31 @@ def get_catalog_data(*, force_refresh: bool = False) -> dict[str, Any]:
try:
data = _fetch_remote(url)
_write_disk_cache(data)
except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError) as err:
except (
urllib.error.URLError,
TimeoutError,
OSError,
json.JSONDecodeError,
) as err:
disk = _read_disk_cache()
if disk is not None:
console.print(
f"[yellow][cpa] Could not refresh catalog ({err}); using disk cache.[/yellow]"
"[yellow][cpa] Could not refresh catalog "
f"({err}); using disk cache.[/yellow]"
)
data = disk
else:
fixture = _read_fixture()
if fixture.get("templates"):
console.print(
f"[yellow][cpa] Could not refresh catalog ({err}); using fixture.[/yellow]"
"[yellow][cpa] Could not refresh catalog "
f"({err}); using fixture.[/yellow]"
)
data = fixture
else:
raise RuntimeError(f"Failed to load template catalog: {err}") from err
raise RuntimeError(
f"Failed to load template catalog: {err}"
) from err

_memory_cache = data
_memory_ts = time.time()
Expand DownExpand Up@@ -173,7 +180,9 @@ def list_addons(template_slug: str | None = None) -> None:
ext_types = [ext_types]
if template_type and template_type not in ext_types:
continue
type_label = ", ".join(ext_types) if isinstance(ext_types, list) else str(ext_types)
type_label = (
", ".join(ext_types) if isinstance(ext_types, list) else str(ext_types)
)
table.add_row(
str(ext.get("slug", "")),
str(ext.get("category", "")),
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,9 +103,7 @@ def scaffold(
if offline:
pass # passed to core

want_interactive = (
interactive if interactive is not None else (not _in_ci())
)
want_interactive = interactive if interactive is not None else (not _in_ci())
if want_interactive and not template:
try:
import questionary
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
from typer.testing import CliRunner

from create_awesome_python_app.cli import cache_app
from typer.testing import CliRunner

runner = CliRunner()

Expand Down
3 changes: 1 addition & 2 deletions packages/create-awesome-python-app/tests/test_cache_env.py
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
from typer.testing import CliRunner

from create_awesome_python_app.cli import cache_app
from typer.testing import CliRunner

runner = CliRunner()

Expand Down
3 changes: 1 addition & 2 deletions packages/create-awesome-python-app/tests/test_catalog.py
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
from typer.testing import CliRunner

from create_awesome_python_app.cli import app
from typer.testing import CliRunner

runner = CliRunner()

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,6 @@
from unittest.mock import patch

import pytest

from create_awesome_python_app.catalog import (
DEFAULT_CATALOG_URL,
catalog_cache_path,
Expand DownExpand Up@@ -41,7 +40,7 @@ class FakeResponse:
def read(self) -> bytes:
return json.dumps(payload).encode("utf-8")

def __enter__(self) -> "FakeResponse":
def __enter__(self) -> FakeResponse:
return self

def __exit__(self, *args: object) -> None:
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,10 +56,14 @@ def test_scaffold_fastapi_starter_from_cpa_templates(
sync = subprocess.run(["uv", "sync"], cwd=dest, capture_output=True, text=True)
assert sync.returncode == 0, sync.stderr

lint = subprocess.run(["uv", "run", "ruff", "check", "."], cwd=dest, capture_output=True, text=True)
lint = subprocess.run(
["uv", "run", "ruff", "check", "."], cwd=dest, capture_output=True, text=True
)
assert lint.returncode == 0, lint.stderr

tests = subprocess.run(["uv", "run", "pytest", "-q"], cwd=dest, capture_output=True, text=True)
tests = subprocess.run(
["uv", "run", "pytest", "-q"], cwd=dest, capture_output=True, text=True
)
assert tests.returncode == 0, tests.stdout + tests.stderr


Expand Down
14 changes: 11 additions & 3 deletions packages/create-awesome-python-app/tests/test_flags.py
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
from typer.testing import CliRunner
import re

from create_awesome_python_app.cli import app
from typer.testing import CliRunner

runner = CliRunner()
_ANSI = re.compile(r"\x1b\[[0-9;]*m")


def test_help_lists_scaffold_flags() -> None:
result = runner.invoke(app, ["--help"])
# Wide COLUMNS avoids Rich truncating option names in CI runners.
result = runner.invoke(
app,
["--help"],
env={"COLUMNS": "120", "TERM": "xterm-256color", "NO_COLOR": "1"},
)
assert result.exit_code == 0
text = _ANSI.sub("", result.stdout or "")
for flag in [
"--template",
"--addons",
Expand All@@ -16,4 +24,4 @@ def test_help_lists_scaffold_flags() -> None:
"--force",
"--no-install",
]:
assert flag in result.stdout
assert flag in text
7 changes: 2 additions & 5 deletions packages/create-awesome-python-app/tests/test_smoke.py
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
from pathlib import Path

import pytest
from typer.testing import CliRunner

from create_awesome_python_app.cli import app
from typer.testing import CliRunner

runner = CliRunner()

Expand All@@ -12,9 +11,7 @@ def test_help_smoke() -> None:
assert runner.invoke(app, ["--help"]).exit_code == 0


def test_scaffold_file(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
def test_scaffold_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("CPA_SKIP_GIT", "1")
monkeypatch.setenv("CI", "1")
tpl = tmp_path / "tpl"
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,9 +45,7 @@ def render_template(content: str, context: dict[str, Any], *, path: str) -> str:
try:
return _JINJA.from_string(content).render(**context)
except TemplateError as exc:
raise ScaffoldAbortedError(
f"Template render failed for {path}: {exc}"
) from exc
raise ScaffoldAbortedError(f"Template render failed for {path}: {exc}") from exc


def _write_bytes(target: Path, data: bytes, *, append: bool) -> None:
Expand DownExpand Up@@ -128,9 +126,7 @@ def copy_tree(
if path.is_dir():
continue
rel = path.relative_to(src)
result = process_file(
path, dest, rel, context=ctx, overwrite=overwrite
)
result = process_file(path, dest, rel, context=ctx, overwrite=overwrite)
if result is not None:
written.append(result)
return written
Expand All@@ -146,9 +142,7 @@ def load_layer(
) -> list[Path]:
"""Load one template/extension layer into dest."""
template_root = get_template_dir_path(source, root)
return copy_tree(
template_root, dest, overwrite=overwrite, context=context
)
return copy_tree(template_root, dest, overwrite=overwrite, context=context)


def merge_layers(
Expand All@@ -160,7 +154,5 @@ def merge_layers(
"""Apply layers in order: template → addons → extend (later wins for copies)."""
written: list[Path] = []
for source, root in layers:
written.extend(
load_layer(source, root, dest, overwrite=True, context=context)
)
written.extend(load_layer(source, root, dest, overwrite=True, context=context))
return written
Loading
Loading