From 4fbe0984f12e57fe37befbce77ff11e507ea653d Mon Sep 17 00:00:00 2001 From: ulises-jeremias Date: Thu, 16 Jul 2026 12:24:40 -0300 Subject: [PATCH 1/2] fix(ci): make main lint, tests, and MegaLinter green Wrap long lines for Ruff, harden --help flag test under narrow CI terminals, fix markdownlint templates/links, tune MegaLinter to the Ruff/Pyright stack, and fix editorconfig/yamllint noise. --- .github/PULL_REQUEST_TEMPLATE.md | 2 + .github/workflows/smoke-distribution.yml | 38 ++++++++++++------- .markdownlint.json | 3 +- .mega-linter.yml | 17 ++++++++- .yamllint.yml | 5 ++- Dockerfile | 10 ++--- docs/CPA_TEMPLATES_TRACKING.md | 8 ++-- .../src/create_awesome_python_app/catalog.py | 25 ++++++++---- .../src/create_awesome_python_app/cli.py | 4 +- .../tests/test_cache_cmds.py | 3 +- .../tests/test_cache_env.py | 3 +- .../tests/test_catalog.py | 3 +- .../tests/test_catalog_fetch.py | 3 +- .../tests/test_cpa_templates_integration.py | 8 +++- .../tests/test_flags.py | 14 +++++-- .../tests/test_smoke.py | 7 +--- .../src/create_python_app_core/loaders.py | 16 ++------ .../create_python_app_core/pyproject_merge.py | 34 ++++++++++++++--- .../tests/test_loaders.py | 1 - .../tests/test_scaffold_context.py | 1 - uv.lock | 10 ++--- 21 files changed, 135 insertions(+), 80 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1a3a441..577ad42 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,9 @@ ## Summary + - ## Test plan + - [ ] `make test` - [ ] `make lint` - [ ] `make typecheck` diff --git a/.github/workflows/smoke-distribution.yml b/.github/workflows/smoke-distribution.yml index 5583d35..7d43015 100644 --- a/.github/workflows/smoke-distribution.yml +++ b/.github/workflows/smoke-distribution.yml @@ -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" diff --git a/.markdownlint.json b/.markdownlint.json index 1dd8622..8a038d9 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -2,5 +2,6 @@ "default": true, "MD013": false, "MD033": false, - "MD041": false + "MD041": false, + "MD060": false } diff --git a/.mega-linter.yml b/.mega-linter.yml index 5beee75..df700b2 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -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 @@ -12,11 +16,20 @@ 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) + - PYTHON_PYLINT + - PYTHON_FLAKE8 + - PYTHON_BLACK + - PYTHON_ISORT + - PYTHON_MYPY + - PYTHON_BANDIT + # 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/) diff --git a/.yamllint.yml b/.yamllint.yml index 7dc3337..18f8e02 100644 --- a/.yamllint.yml +++ b/.yamllint.yml @@ -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 diff --git a/Dockerfile b/Dockerfile index e63cbe8..f815227 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/docs/CPA_TEMPLATES_TRACKING.md b/docs/CPA_TEMPLATES_TRACKING.md index afd7f11..652d5c3 100644 --- a/docs/CPA_TEMPLATES_TRACKING.md +++ b/docs/CPA_TEMPLATES_TRACKING.md @@ -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 @@ -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 diff --git a/packages/create-awesome-python-app/src/create_awesome_python_app/catalog.py b/packages/create-awesome-python-app/src/create_awesome_python_app/catalog.py index eeafcc8..dc0741f 100644 --- a/packages/create-awesome-python-app/src/create_awesome_python_app/catalog.py +++ b/packages/create-awesome-python-app/src/create_awesome_python_app/catalog.py @@ -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)" @@ -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() @@ -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", "")), diff --git a/packages/create-awesome-python-app/src/create_awesome_python_app/cli.py b/packages/create-awesome-python-app/src/create_awesome_python_app/cli.py index 97d0a18..b90c6c3 100644 --- a/packages/create-awesome-python-app/src/create_awesome_python_app/cli.py +++ b/packages/create-awesome-python-app/src/create_awesome_python_app/cli.py @@ -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 diff --git a/packages/create-awesome-python-app/tests/test_cache_cmds.py b/packages/create-awesome-python-app/tests/test_cache_cmds.py index 8c22903..2393a46 100644 --- a/packages/create-awesome-python-app/tests/test_cache_cmds.py +++ b/packages/create-awesome-python-app/tests/test_cache_cmds.py @@ -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() diff --git a/packages/create-awesome-python-app/tests/test_cache_env.py b/packages/create-awesome-python-app/tests/test_cache_env.py index ddcf981..20d94da 100644 --- a/packages/create-awesome-python-app/tests/test_cache_env.py +++ b/packages/create-awesome-python-app/tests/test_cache_env.py @@ -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() diff --git a/packages/create-awesome-python-app/tests/test_catalog.py b/packages/create-awesome-python-app/tests/test_catalog.py index 96f064f..a882b1b 100644 --- a/packages/create-awesome-python-app/tests/test_catalog.py +++ b/packages/create-awesome-python-app/tests/test_catalog.py @@ -1,6 +1,5 @@ -from typer.testing import CliRunner - from create_awesome_python_app.cli import app +from typer.testing import CliRunner runner = CliRunner() diff --git a/packages/create-awesome-python-app/tests/test_catalog_fetch.py b/packages/create-awesome-python-app/tests/test_catalog_fetch.py index 8433974..6b225f7 100644 --- a/packages/create-awesome-python-app/tests/test_catalog_fetch.py +++ b/packages/create-awesome-python-app/tests/test_catalog_fetch.py @@ -7,7 +7,6 @@ from unittest.mock import patch import pytest - from create_awesome_python_app.catalog import ( DEFAULT_CATALOG_URL, catalog_cache_path, @@ -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: diff --git a/packages/create-awesome-python-app/tests/test_cpa_templates_integration.py b/packages/create-awesome-python-app/tests/test_cpa_templates_integration.py index 9425212..494a363 100644 --- a/packages/create-awesome-python-app/tests/test_cpa_templates_integration.py +++ b/packages/create-awesome-python-app/tests/test_cpa_templates_integration.py @@ -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 diff --git a/packages/create-awesome-python-app/tests/test_flags.py b/packages/create-awesome-python-app/tests/test_flags.py index a250245..08b37b5 100644 --- a/packages/create-awesome-python-app/tests/test_flags.py +++ b/packages/create-awesome-python-app/tests/test_flags.py @@ -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", @@ -16,4 +24,4 @@ def test_help_lists_scaffold_flags() -> None: "--force", "--no-install", ]: - assert flag in result.stdout + assert flag in text diff --git a/packages/create-awesome-python-app/tests/test_smoke.py b/packages/create-awesome-python-app/tests/test_smoke.py index 65ac09d..4cec552 100644 --- a/packages/create-awesome-python-app/tests/test_smoke.py +++ b/packages/create-awesome-python-app/tests/test_smoke.py @@ -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() @@ -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" diff --git a/packages/create-python-app-core/src/create_python_app_core/loaders.py b/packages/create-python-app-core/src/create_python_app_core/loaders.py index f4fbaf2..e7d406c 100644 --- a/packages/create-python-app-core/src/create_python_app_core/loaders.py +++ b/packages/create-python-app-core/src/create_python_app_core/loaders.py @@ -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: @@ -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 @@ -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( @@ -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 diff --git a/packages/create-python-app-core/src/create_python_app_core/pyproject_merge.py b/packages/create-python-app-core/src/create_python_app_core/pyproject_merge.py index 31d8458..488d2ff 100644 --- a/packages/create-python-app-core/src/create_python_app_core/pyproject_merge.py +++ b/packages/create-python-app-core/src/create_python_app_core/pyproject_merge.py @@ -54,7 +54,11 @@ def merge_tables(base: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any result[key] = overlay_value continue base_value = result[key] - if key == "dependencies" and _is_sequence(base_value) and _is_sequence(overlay_value): + if ( + key == "dependencies" + and _is_sequence(base_value) + and _is_sequence(overlay_value) + ): result[key] = merge_dependency_lists(list(base_value), list(overlay_value)) elif ( key == "optional-dependencies" @@ -63,22 +67,40 @@ def merge_tables(base: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any ): merged_opt: dict[str, Any] = dict(base_value) for opt_key, opt_val in overlay_value.items(): - if opt_key in merged_opt and _is_sequence(merged_opt[opt_key]) and _is_sequence(opt_val): + if ( + opt_key in merged_opt + and _is_sequence(merged_opt[opt_key]) + and _is_sequence(opt_val) + ): merged_opt[opt_key] = merge_dependency_lists( list(merged_opt[opt_key]), list(opt_val) ) else: merged_opt[opt_key] = opt_val result[key] = merged_opt - elif key == "dependency-groups" and _is_mapping(base_value) and _is_mapping(overlay_value): + elif ( + key == "dependency-groups" + and _is_mapping(base_value) + and _is_mapping(overlay_value) + ): merged_groups: dict[str, Any] = dict(base_value) for gkey, gval in overlay_value.items(): - if gkey in merged_groups and _is_sequence(merged_groups[gkey]) and _is_sequence(gval): + if ( + gkey in merged_groups + and _is_sequence(merged_groups[gkey]) + and _is_sequence(gval) + ): merged_groups[gkey] = merge_dependency_lists( list(merged_groups[gkey]), list(gval) ) - elif gkey in merged_groups and _is_mapping(merged_groups[gkey]) and _is_mapping(gval): - merged_groups[gkey] = merge_tables(dict(merged_groups[gkey]), dict(gval)) + elif ( + gkey in merged_groups + and _is_mapping(merged_groups[gkey]) + and _is_mapping(gval) + ): + merged_groups[gkey] = merge_tables( + dict(merged_groups[gkey]), dict(gval) + ) else: merged_groups[gkey] = gval result[key] = merged_groups diff --git a/packages/create-python-app-core/tests/test_loaders.py b/packages/create-python-app-core/tests/test_loaders.py index ddf717b..e3e21f8 100644 --- a/packages/create-python-app-core/tests/test_loaders.py +++ b/packages/create-python-app-core/tests/test_loaders.py @@ -1,7 +1,6 @@ from pathlib import Path import pytest - from create_python_app_core.errors import ScaffoldAbortedError from create_python_app_core.loaders import merge_layers, process_file, render_template from create_python_app_core.paths import ResolvedSource diff --git a/packages/create-python-app-core/tests/test_scaffold_context.py b/packages/create-python-app-core/tests/test_scaffold_context.py index c376116..121ca6b 100644 --- a/packages/create-python-app-core/tests/test_scaffold_context.py +++ b/packages/create-python-app-core/tests/test_scaffold_context.py @@ -1,7 +1,6 @@ from pathlib import Path import pytest - from create_python_app_core.config import CpaConfig, CpaCustomOption from create_python_app_core.installer import build_scaffold_context, scaffold_project diff --git a/uv.lock b/uv.lock index b2c2674..eb78d12 100644 --- a/uv.lock +++ b/uv.lock @@ -10,12 +10,12 @@ members = [ [manifest.dependency-groups] dev = [ - { name = "pre-commit", specifier = ">=4.0.0" }, - { name = "pyright", specifier = ">=1.1.400" }, + { name = "pre-commit", specifier = ">=4.6.0" }, + { name = "pyright", specifier = ">=1.1.411" }, { name = "pytest", specifier = ">=8.0.0" }, - { name = "pytest-asyncio", specifier = ">=0.25.0" }, - { name = "pytest-cov", specifier = ">=6.0.0" }, - { name = "ruff", specifier = ">=0.12.0" }, + { name = "pytest-asyncio", specifier = ">=1.4.0" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "ruff", specifier = ">=0.15.21" }, ] [[package]] From e6228526482d5288ae5160a2fb1c9c35ad5168e0 Mon Sep 17 00:00:00 2001 From: ulises-jeremias Date: Thu, 16 Jul 2026 12:27:42 -0300 Subject: [PATCH 2/2] fix(ci): disable MegaLinter Python tools covered by Ruff/Pyright jobs MegaLinter's pyright/ruff run without the uv workspace venv and produce false positives; dedicated Lint and Typecheck workflows remain the gate. --- .mega-linter.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.mega-linter.yml b/.mega-linter.yml index df700b2..cf01644 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -21,13 +21,15 @@ DISABLE_LINTERS: - SPELL_CSPELL - SPELL_LYCHEE - YAML_V8R - # Overlap with Ruff / Pyright (project tooling) + # 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