diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b5e327..6a3b48d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,7 +76,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] include: - os: macos-latest python-version: "3.12" @@ -90,6 +90,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + # Needed for 3.15 while it is still pre-release on the runners. + allow-prereleases: true - name: Install package (dev extras) run: python -m pip install -e ".[dev]" diff --git a/CHANGELOG.md b/CHANGELOG.md index 289e27a..6ed555e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,26 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **Publish** workflow: GitHub Release (`vX.Y.Z`) builds sdist/wheel and uploads - to PyPI via Trusted Publishing (OIDC). Manual `workflow_dispatch` can target - TestPyPI. Release tag must match `src/cppboot/_version.py`. -- Ruff + mypy in CI; distribution build + `twine check` job. -- Dev extras: `ruff`, `mypy`, `build`, `twine`. -- Opt-out flags for default-on behavior: `--no-git`, `--no-fmt`, `--no-community-docs` - (alongside existing `--no-vim`, `--no-ctags`, `--no-vscode`, `--no-github-actions`, - `--no-codespaces`). -- Offline license mode for deterministic tests (`ProjectOptions.offline_license`). -- pytest unit and integration suite with multi-Python CI. +- Official support for **Python 3.9+** (`requires-python = ">=3.9"`). +- CI matrix covers **3.9–3.15** (3.15 via `allow-prereleases` while pre-release). ### Changed -- Package version single-sourced from `src/cppboot/_version.py` (setuptools - dynamic version). -- Split the monolithic generator into a `cppboot.generate` package (templates, - tooling, orchestration) while keeping `cppboot.generator` as a stable facade. -- Added package `LICENSE`, `CHANGELOG.md`, and `py.typed` for a PyPI-ready layout. +- Opinionated defaults are **opt-out only**: help/CLI expose `--no-vim`, + `--no-ctags`, `--no-vscode`, etc. Positive mirrors (`--vim`, `--git`, …) removed + so the CLI matches “always on unless you disable it.” -## [0.1.0] - unreleased on PyPI +## [0.1.0] - 2026-07-27 -Initial public development line: CMake C++20 scaffolds, VERSION single source, -Makefile/`build.bat`, VS Code, Codespaces, GitHub Actions for generated projects. +Initial public release on PyPI: CMake C++20 scaffolds, VERSION single source, +Makefile/`build.bat`, VS Code, Codespaces, GitHub Actions for generated projects, +multi-Python CI, GitHub Release → PyPI Trusted Publishing. diff --git a/README.md b/README.md index e37186b..45b707e 100644 --- a/README.md +++ b/README.md @@ -24,28 +24,33 @@ file (single source of truth) and CLI **`--version` / `-V`**. ### Options +Requires **Python 3.9+** (CI: 3.9–3.15; Amazon Linux’s system 3.9 is supported). + | Flag | Default | Description | |------|---------|-------------| | `-n`, `--name` | (prompt) | Project name | | `--license` | `apache-2.0` | License id | | `--build-system` | `cmake` | Only `cmake` for now | -| `--with-modules` | off | C++20 modules scaffold | -| `--shared` | off | Shared library instead of static | -| `--vim` / `--no-vim` | **on** | Project-local `.vimrc` | -| `--ctags` / `--no-ctags` | **on** | Universal Ctags `.ctags` + `make tags` | -| `--vscode` / `--no-vscode` | **on** | VS Code + `CMakePresets.json` | -| `--github-actions` / `--no-github-actions` | **on** | Multi-OS CI / sanitizers / release workflows | -| `--codespaces` / `--no-codespaces` | **on** | GitHub Codespaces / Dev Container (`.devcontainer/`) | -| `--community-docs` / `--no-community-docs` | **on** | `CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`, `SECURITY.md` | -| `--git` / `--no-git` | **on** | `git init` + initial commit | -| `--fmt` / `--no-fmt` | **on** | Run `make fmt` after scaffolding | -| `--github` | **off** | Create GitHub remote with `gh` (opt-in) | +| `--with-modules` | off | C++20 modules scaffold (opt-in) | +| `--shared` | off | Shared library instead of static (opt-in) | +| `--github` | off | Create GitHub remote with `gh` (opt-in) | | `--output-dir` | `.` | Parent directory for the new project | | `-v`, `--verbose` | off | Verbose logging | | `--version` | | Print cppboot version | | `-h`, `--help` | | Help | -All default-on features have matching **`--no-*`** opt-outs. +**Opinionated defaults (always on).** Disable only with `--no-*`: + +| Opt-out | Skips | +|---------|--------| +| `--no-vim` | Project-local `.vimrc` | +| `--no-ctags` | Universal Ctags `.ctags` + `make tags` | +| `--no-vscode` | VS Code + `CMakePresets.json` | +| `--no-github-actions` | CI / sanitizers / release workflows | +| `--no-codespaces` | Codespaces / Dev Container | +| `--no-community-docs` | CoC / CONTRIBUTING / SECURITY | +| `--no-git` | `git init` + initial commit | +| `--no-fmt` | `make fmt` after scaffolding | ## What you get diff --git a/pyproject.toml b/pyproject.toml index ecb646b..94b1644 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "cppboot" dynamic = ["version"] description = "Bootstrap a professional-grade C++ project environment" readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.9" license = { file = "LICENSE" } authors = [{ name = "cppboot contributors" }] keywords = ["c++", "cmake", "bootstrap", "scaffold", "project-generator"] @@ -18,10 +18,13 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Code Generators", "Typing :: Typed", @@ -71,7 +74,7 @@ source = ["cppboot"] branch = true [tool.ruff] -target-version = "py310" +target-version = "py39" line-length = 100 src = ["src", "tests"] @@ -99,6 +102,7 @@ quote-style = "double" indent-style = "space" [tool.mypy] +# Type-check against 3.10+ (current mypy floor). Runtime support includes 3.9 (see CI). python_version = "3.10" mypy_path = "src" packages = ["cppboot"] diff --git a/src/cppboot/cli.py b/src/cppboot/cli.py index 1bc91ec..261afb8 100644 --- a/src/cppboot/cli.py +++ b/src/cppboot/cli.py @@ -12,6 +12,23 @@ from cppboot.licenses import DEFAULT_LICENSE, LICENSE_CHOICES +def _add_opt_out( + parser: argparse.ArgumentParser, + *, + flag: str, + dest: str, + help_text: str, +) -> None: + """Register a default-on feature that is disabled only via ``--no-*``.""" + parser.add_argument( + flag, + action="store_false", + dest=dest, + default=True, + help=help_text, + ) + + def build_parser() -> argparse.ArgumentParser: """Create the argument parser.""" parser = argparse.ArgumentParser( @@ -51,43 +68,54 @@ def build_parser() -> argparse.ArgumentParser: action="store_true", help="Build the library as a shared library (default: static).", ) - parser.add_argument( - "--vim", - action=argparse.BooleanOptionalAction, - default=True, - help="Write a minimal project-local .vimrc (default: on). Use --no-vim to skip.", - ) - parser.add_argument( - "--ctags", - action=argparse.BooleanOptionalAction, - default=True, - help=( - "Write Universal Ctags config and make tags target (default: on). " - "Use --no-ctags to skip." - ), - ) - parser.add_argument( - "--vscode", - action=argparse.BooleanOptionalAction, - default=True, - help=("Write VS Code config + CMakePresets (default: on). Use --no-vscode to skip."), - ) - parser.add_argument( - "--github-actions", - action=argparse.BooleanOptionalAction, - default=True, - help=( - "Write cross-platform GitHub Actions CI (default: on). Use --no-github-actions to skip." - ), - ) - parser.add_argument( - "--codespaces", - action=argparse.BooleanOptionalAction, - default=True, - help=( - "Write GitHub Codespaces / Dev Container config (default: on). " - "Use --no-codespaces to skip." - ), + # Opinionated defaults: always on unless the user passes the matching --no-* flag. + _add_opt_out( + parser, + flag="--no-vim", + dest="vim", + help_text="Do not write a project-local .vimrc.", + ) + _add_opt_out( + parser, + flag="--no-ctags", + dest="ctags", + help_text="Do not write Universal Ctags config / make tags target.", + ) + _add_opt_out( + parser, + flag="--no-vscode", + dest="vscode", + help_text="Do not write VS Code config + CMakePresets.", + ) + _add_opt_out( + parser, + flag="--no-github-actions", + dest="github_actions", + help_text="Do not write GitHub Actions CI / sanitizers / release workflows.", + ) + _add_opt_out( + parser, + flag="--no-codespaces", + dest="codespaces", + help_text="Do not write GitHub Codespaces / Dev Container config.", + ) + _add_opt_out( + parser, + flag="--no-git", + dest="git", + help_text="Do not run git init or create an initial commit.", + ) + _add_opt_out( + parser, + flag="--no-fmt", + dest="fmt", + help_text="Do not run make fmt after scaffolding.", + ) + _add_opt_out( + parser, + flag="--no-community-docs", + dest="community_docs", + help_text="Do not write CODE_OF_CONDUCT.md, CONTRIBUTING.md, or SECURITY.md.", ) parser.add_argument( "--github", @@ -98,27 +126,6 @@ def build_parser() -> argparse.ArgumentParser: "(default: off; opt-in only)." ), ) - parser.add_argument( - "--git", - action=argparse.BooleanOptionalAction, - default=True, - help=("Run git init and create an initial commit (default: on). Use --no-git to skip."), - ) - parser.add_argument( - "--fmt", - action=argparse.BooleanOptionalAction, - default=True, - help=("Run make fmt after scaffolding (default: on). Use --no-fmt to skip."), - ) - parser.add_argument( - "--community-docs", - action=argparse.BooleanOptionalAction, - default=True, - help=( - "Write CODE_OF_CONDUCT.md, CONTRIBUTING.md, and SECURITY.md " - "(default: on). Use --no-community-docs to skip." - ), - ) parser.add_argument( "-v", "--verbose", diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 07b61e7..e12300b 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -52,6 +52,38 @@ def test_parser_opt_outs() -> None: assert args.community_docs is False +def test_help_lists_opt_outs_not_positive_toggles() -> None: + """Opinionated features are default-on; only --no-* should appear in help.""" + parser = build_parser() + option_strings = {opt for action in parser._actions for opt in action.option_strings} + for opt_out in ( + "--no-vim", + "--no-ctags", + "--no-vscode", + "--no-github-actions", + "--no-codespaces", + "--no-git", + "--no-fmt", + "--no-community-docs", + ): + assert opt_out in option_strings + # Positive mirrors must not exist (defaults are always on). + for positive in ("--vim", "--ctags", "--vscode", "--git", "--fmt", "--community-docs"): + assert positive not in option_strings + # Opt-in flags remain. + assert "--github" in option_strings + assert "--with-modules" in option_strings + + +def test_positive_toggle_flags_are_rejected() -> None: + with pytest.raises(SystemExit): + build_parser().parse_args(["-n", "demo", "--vim"]) + with pytest.raises(SystemExit): + build_parser().parse_args(["-n", "demo", "--fmt"]) + # Note: argparse may treat "--git" as an abbreviation of "--github" when + # allow_abbrev is true; the explicit option is only "--no-git". + + def test_parser_opt_in_features() -> None: args = build_parser().parse_args(["-n", "demo", "--with-modules", "--shared", "--github"]) assert args.with_modules is True