diff --git a/copier.yml b/copier.yml index 71f5cf4..8c09a78 100644 --- a/copier.yml +++ b/copier.yml @@ -231,13 +231,7 @@ _tasks: - command: uv run pre-commit install --hook-type pre-push - command: uv run ruff check --fix --unsafe-fixes . - command: uv run basedpyright - - command: rm -f cliff.toml - when: "{{ not include_git_cliff }}" - - command: rm -f src/{{ package_name }}/cli.py - when: "{{ not include_cli }}" - - command: rm -f mkdocs.yml - when: "{{ not include_docs }}" - - command: sh -c 'rm -f docs/index.md docs/ci.md 2>/dev/null; rmdir docs 2>/dev/null || true' + - command: sh -c 'rmdir docs 2>/dev/null || true' when: "{{ not include_docs }}" - command: >- sh -c 'find . -type f -empty ! -path "./.git/*" ! -path "./.venv/*" -delete' diff --git a/justfile b/justfile index d8dce07..936b44f 100644 --- a/justfile +++ b/justfile @@ -91,7 +91,7 @@ precommit: # Dependency audit matching .github/workflows/security.yml (pip-audit) audit: - @uv export --frozen --format requirements-txt --extra dev | uvx pip-audit --requirement /dev/stdin + @uv export --frozen --format requirements-txt --extra dev | uv tool run pip-audit --requirement /dev/stdin # ------------------------------------------------------------------------- # Dependency management diff --git a/scripts/sync_skip_if_exists.py b/scripts/sync_skip_if_exists.py index 082d602..f93c55b 100644 --- a/scripts/sync_skip_if_exists.py +++ b/scripts/sync_skip_if_exists.py @@ -27,9 +27,7 @@ "template/.github/CODE_OF_CONDUCT.md.jinja": ".github/CODE_OF_CONDUCT.md", "template/.github/ISSUE_TEMPLATE/bug_report.md.jinja": ".github/ISSUE_TEMPLATE/bug_report.md", "template/.github/ISSUE_TEMPLATE/feature_request.md.jinja": ".github/ISSUE_TEMPLATE/feature_request.md", - "template/src/{{ package_name }}/common/bump_version.py.jinja": ( - "src/{{ package_name }}/common/bump_version.py" - ), + "template/src/{{ package_name }}/common/bump_version.py.jinja": "src/{{ package_name }}/common/bump_version.py", } # Always include these (user customization hotspots even if not in the map above). diff --git a/template/docs/ci.md.jinja b/template/docs/{% if include_docs %}ci.md{% endif %}.jinja similarity index 100% rename from template/docs/ci.md.jinja rename to template/docs/{% if include_docs %}ci.md{% endif %}.jinja diff --git a/template/docs/index.md.jinja b/template/docs/{% if include_docs %}index.md{% endif %}.jinja similarity index 100% rename from template/docs/index.md.jinja rename to template/docs/{% if include_docs %}index.md{% endif %}.jinja diff --git a/template/src/{{ package_name }}/cli.py.jinja b/template/src/{{ package_name }}/{% if include_cli %}cli.py{% endif %}.jinja similarity index 91% rename from template/src/{{ package_name }}/cli.py.jinja rename to template/src/{{ package_name }}/{% if include_cli %}cli.py{% endif %}.jinja index ba704b9..b425f25 100644 --- a/template/src/{{ package_name }}/cli.py.jinja +++ b/template/src/{{ package_name }}/{% if include_cli %}cli.py{% endif %}.jinja @@ -1,4 +1,3 @@ -{% if include_cli %} """Command-line interface for **{{ project_name }}**.""" from __future__ import annotations @@ -16,4 +15,3 @@ def hello() -> None: if __name__ == "__main__": app() -{% endif %} diff --git a/template/mkdocs.yml.jinja b/template/{% if include_docs %}mkdocs.yml{% endif %}.jinja similarity index 92% rename from template/mkdocs.yml.jinja rename to template/{% if include_docs %}mkdocs.yml{% endif %}.jinja index 4936538..46052d1 100644 --- a/template/mkdocs.yml.jinja +++ b/template/{% if include_docs %}mkdocs.yml{% endif %}.jinja @@ -1,4 +1,3 @@ -{% if include_docs %} site_name: "{{ project_name }}" site_description: "{{ project_description }}" repo_url: "https://github.com/{{ github_username }}/{{ project_slug }}" @@ -18,4 +17,3 @@ plugins: python: options: docstring_style: google -{% endif %} diff --git a/template/cliff.toml.jinja b/template/{% if include_git_cliff %}cliff.toml{% endif %}.jinja similarity index 100% rename from template/cliff.toml.jinja rename to template/{% if include_git_cliff %}cliff.toml{% endif %}.jinja diff --git a/tests/test_template.py b/tests/test_template.py index 5aa3b1b..cb4b72e 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -19,6 +19,8 @@ import yaml from copier import run_copy +TEMPLATE_GIT_SRC = f"git+{Path('.').resolve().as_uri()}" + def run_command( cmd: list[str], @@ -59,7 +61,7 @@ def get_default_command_list(test_dir: Path) -> list[str]: return [ "copier", "copy", - ".", + TEMPLATE_GIT_SRC, str(test_dir), "--data", "project_name=Test Project", @@ -113,13 +115,9 @@ def _remove_empty_optional_artifacts(dest: Path, data: dict[str, str | bool]) -> pkg = data.get("package_name") if not isinstance(pkg, str): return - pairs: list[tuple[bool, Path]] = [ - (not bool(data.get("include_cli", False)), dest / "src" / pkg / "cli.py"), - (not bool(data.get("include_git_cliff", False)), dest / "cliff.toml"), - ] - for should_drop, path in pairs: - if should_drop and path.is_file() and path.stat().st_size == 0: - path.unlink() + # Optional artifacts are now conditionally named in the template, so Copier won't + # emit empty files for disabled features. Keep this hook as a no-op for now + # (it remains useful if we add any future optional whole-file templates). def copy_with_data( @@ -143,7 +141,7 @@ def copy_with_data( "copy", "--vcs-ref", "HEAD", - ".", + TEMPLATE_GIT_SRC, str(dest), "--trust", "--defaults", @@ -266,9 +264,7 @@ def test_generate_defaults_only_cli(tmp_path: Path) -> None: [ "copier", "copy", - "--vcs-ref", - "HEAD", - ".", + TEMPLATE_GIT_SRC, str(test_dir), "--trust", "--defaults", @@ -313,7 +309,7 @@ def test_package_name_validator_rejects_leading_digit(tmp_path: Path) -> None: "copy", "--vcs-ref", "HEAD", - ".", + TEMPLATE_GIT_SRC, str(test_dir), "--trust", "--defaults", @@ -329,13 +325,23 @@ def test_package_name_validator_rejects_leading_digit(tmp_path: Path) -> None: def test_computed_values_not_recorded_in_answers_file(tmp_path: Path) -> None: """Questions with ``when: false`` must not be stored in the answers file.""" test_dir = tmp_path / "computed_answers" - _ = run_command(["copier", "copy", ".", str(test_dir), "--trust", "--defaults", "--skip-tasks"]) + _ = run_command( + [ + "copier", + "copy", + TEMPLATE_GIT_SRC, + str(test_dir), + "--trust", + "--defaults", + "--skip-tasks", + ] + ) _remove_empty_optional_artifacts( test_dir, { "package_name": "my_library", "include_cli": False, - "include_git_cliff": True, + "include_git_cliff": False, }, ) answers_text = (test_dir / ".copier-answers.yml").read_text(encoding="utf-8") @@ -346,7 +352,17 @@ def test_computed_values_not_recorded_in_answers_file(tmp_path: Path) -> None: def test_answers_file_warns_never_edit_manually(tmp_path: Path) -> None: """Generated answers file should match Copier docs banner text.""" test_dir = tmp_path / "answers_banner" - _ = run_command(["copier", "copy", ".", str(test_dir), "--trust", "--defaults", "--skip-tasks"]) + _ = run_command( + [ + "copier", + "copy", + TEMPLATE_GIT_SRC, + str(test_dir), + "--trust", + "--defaults", + "--skip-tasks", + ] + ) _remove_empty_optional_artifacts( test_dir, { @@ -363,7 +379,7 @@ def test_generate_programmatic_run_copy_local(tmp_path: Path) -> None: """Render programmatically with :func:`copier.run_copy` from a local path.""" test_dir = tmp_path / "programmatic_local" _worker = run_copy( - ".", + TEMPLATE_GIT_SRC, test_dir, defaults=True, unsafe=True, @@ -375,7 +391,7 @@ def test_generate_programmatic_run_copy_local(tmp_path: Path) -> None: { "package_name": "my_library", "include_cli": False, - "include_git_cliff": True, + "include_git_cliff": False, }, ) @@ -854,7 +870,10 @@ def test_docs_ci_page_when_docs_enabled(tmp_path: Path) -> None: def test_generated_pyproject_basedpyright_standard_mode(tmp_path: Path) -> None: """Generated projects should configure basedpyright in standard mode (per template contract).""" test_dir = tmp_path / "bp_std" - copy_with_data(test_dir, {"project_name": "BP Test", "include_docs": False}) + copy_with_data( + test_dir, + {"project_name": "BP Test", "include_docs": False, "include_git_cliff": False}, + ) raw = (test_dir / "pyproject.toml").read_text(encoding="utf-8") assert 'typeCheckingMode = "standard"' in raw assert "reportMissingImports = true" in raw @@ -863,7 +882,10 @@ def test_generated_pyproject_basedpyright_standard_mode(tmp_path: Path) -> None: def test_generated_pre_commit_includes_detect_secrets(tmp_path: Path) -> None: """Pre-commit config in generated projects should run detect-secrets with a baseline.""" test_dir = tmp_path / "secrets_hook" - copy_with_data(test_dir, {"project_name": "Secrets Hook", "include_docs": False}) + copy_with_data( + test_dir, + {"project_name": "Secrets Hook", "include_docs": False, "include_git_cliff": False}, + ) cfg = (test_dir / ".pre-commit-config.yaml").read_text(encoding="utf-8") assert "detect-secrets" in cfg assert ".secrets.baseline" in cfg @@ -873,7 +895,10 @@ def test_generated_pre_commit_includes_detect_secrets(tmp_path: Path) -> None: def test_generated_renovate_enables_pre_commit(tmp_path: Path) -> None: """Renovate should manage pre-commit hook revisions in generated projects.""" test_dir = tmp_path / "renovate_pc" - copy_with_data(test_dir, {"project_name": "Renovate Test", "include_docs": False}) + copy_with_data( + test_dir, + {"project_name": "Renovate Test", "include_docs": False, "include_git_cliff": False}, + ) import json data = json.loads((test_dir / ".github" / "renovate.json").read_text(encoding="utf-8")) @@ -954,7 +979,9 @@ def test_release_workflow_generated_by_default(tmp_path: Path) -> None: assert release_yml.is_file(), "release.yml must exist when include_release_workflow=true" content = release_yml.read_text(encoding="utf-8") assert "${{ true }}" in content, "release job must be enabled" - assert "common/bump_version.py" in content, "release.yml must reference common/bump_version.py" + assert "src/release_default/common/bump_version.py" in content, ( + "release.yml must reference src//common/bump_version.py" + ) assert "--generate-notes" in content, ( "release must use gh --generate-notes (no CHANGELOG.md required)" )