diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index ea39f95..c7ea994 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -19,10 +19,17 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Checkout copier-templates + uses: actions/checkout@v2 + with: + repository: plone/copier-templates + path: copier-templates - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} + - name: Set up uv + uses: astral-sh/setup-uv@v5 - name: Install dependencies run: | python -m pip install --upgrade pip @@ -32,9 +39,11 @@ jobs: - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 . --exclude=copier-templates --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + flake8 . --exclude=copier-templates --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest + env: + PLONECLI_TEMPLATES_DIR: ${{ github.workspace }}/copier-templates run: | pytest diff --git a/CHANGES.md b/CHANGES.md index 4696764..306c4cc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,11 @@ ## 7.0.0b10 (unreleased) +- Fix non-interactive composite `create`/`setup`: layered steps now overwrite + files generated by earlier steps instead of raising a copier conflict in + `--defaults` mode. + [MrTango] + - Skill: list field names reserved by Plone's default behaviors so naming conflicts are caught early and an alternative is recommended; keeping a conflicting name is allowed when the type does not enable that behavior. diff --git a/plonecli/cli.py b/plonecli/cli.py index 7c20543..8442b31 100644 --- a/plonecli/cli.py +++ b/plonecli/cli.py @@ -287,11 +287,11 @@ def create(context, template, name, data, data_file, defaults, no_git): steps = reg.get_composite_steps(resolved) if steps: echo(f"\nCreating {resolved} project: {name}", fg="green", reverse=True) - for step in steps: + for index, step in enumerate(steps): echo(f"\n Applying template: {step}", fg="green") committed = run_create( step, name, config, data=answers, defaults=defaults, - git_commit=git_commit, + git_commit=git_commit, overwrite=index > 0, ) if committed: echo(f" Committed: {committed}", fg="green") @@ -386,7 +386,7 @@ def setup(context): config = context.obj["config"] echo("\nRunning zope-setup...", fg="green", reverse=True) - run_create("zope-setup", str(project.root_folder), config) + run_create("zope-setup", str(project.root_folder), config, overwrite=True) @cli.command("serve") diff --git a/plonecli/templates.py b/plonecli/templates.py index 71d20eb..372a66d 100644 --- a/plonecli/templates.py +++ b/plonecli/templates.py @@ -137,6 +137,7 @@ def run_create( data: dict | None = None, defaults: bool = False, git_commit: bool = True, + overwrite: bool = False, ) -> str | None: """Run copier to create a new project from a main template. @@ -148,6 +149,10 @@ def run_create( defaults: Use template defaults for unanswered questions instead of prompting (non-interactive mode). git_commit: Initialise git (if needed) and commit the result. + overwrite: Overwrite existing files without prompting. Needed when a + template is layered onto an existing project (composite steps after + the first, ``plonecli setup``); copier otherwise raises an + interactive-conflict error in non-interactive mode. Returns: The commit message if a commit was made, otherwise ``None``. @@ -161,6 +166,7 @@ def run_create( data=data or {}, user_defaults=_build_user_defaults(config), defaults=defaults, + overwrite=overwrite, unsafe=True, ) diff --git a/tests/test_all_templates_data.py b/tests/test_all_templates_data.py index fe73c67..8300cea 100644 --- a/tests/test_all_templates_data.py +++ b/tests/test_all_templates_data.py @@ -19,6 +19,7 @@ from __future__ import annotations +import os import shutil from pathlib import Path @@ -46,6 +47,9 @@ def _find_templates_dir() -> Path | None: + env_dir = os.environ.get("PLONECLI_TEMPLATES_DIR") + if env_dir and Path(env_dir).exists(): + return Path(env_dir) if DEV_TEMPLATES_DIR.exists(): return DEV_TEMPLATES_DIR if FALLBACK_TEMPLATES_DIR.exists(): diff --git a/tests/test_config.py b/tests/test_config.py index 1001254..7f6c5ac 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -126,6 +126,7 @@ def test_templates_path_reloads_under_different_home(tmp_path, monkeypatch): config_file = config_dir / "config.toml" monkeypatch.setattr("plonecli.config.CONFIG_DIR", config_dir) monkeypatch.setattr("plonecli.config.CONFIG_FILE", config_file) + monkeypatch.delenv("PLONECLI_TEMPLATES_DIR", raising=False) home_a = tmp_path / "home_a" monkeypatch.setenv("HOME", str(home_a)) diff --git a/tests/test_plonecli.py b/tests/test_plonecli.py index 8092b9d..2877057 100644 --- a/tests/test_plonecli.py +++ b/tests/test_plonecli.py @@ -125,6 +125,9 @@ def test_create_composite_template(mock_ensure, mock_run_create, mock_config, mo assert calls[0][0][1] == "my.addon" assert calls[1][0][0] == "zope-setup" assert calls[1][0][1] == "my.addon" + # First layer creates fresh files; later layers overlay and must overwrite. + assert calls[0].kwargs["overwrite"] is False + assert calls[1].kwargs["overwrite"] is True @patch("plonecli.cli.find_project_root", return_value=None) diff --git a/tests/test_templates.py b/tests/test_templates.py index 2846620..9d7e0c9 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -9,6 +9,7 @@ ensure_templates_cloned, get_template_path, get_templates_info, + run_create, update_templates_clone, ) @@ -130,3 +131,26 @@ def test_get_templates_info_not_cloned(tmp_path): config = PlonecliConfig(templates_dir=str(tmp_path / "nonexistent")) info = get_templates_info(config) assert info == "not cloned" + + +@patch("plonecli.templates.run_copy") +def test_run_create_overwrite_default_false(mock_run_copy, tmp_path): + """run_create does not overwrite by default.""" + (tmp_path / ".git").mkdir() + (tmp_path / "backend_addon").mkdir() + config = PlonecliConfig(templates_dir=str(tmp_path)) + run_create("backend_addon", "out", config, defaults=True, git_commit=False) + assert mock_run_copy.call_args.kwargs["overwrite"] is False + + +@patch("plonecli.templates.run_copy") +def test_run_create_overwrite_forwarded(mock_run_copy, tmp_path): + """overwrite=True is forwarded to copier (layering onto existing files).""" + (tmp_path / ".git").mkdir() + (tmp_path / "zope-setup").mkdir() + config = PlonecliConfig(templates_dir=str(tmp_path)) + run_create( + "zope-setup", "out", config, defaults=True, git_commit=False, + overwrite=True, + ) + assert mock_run_copy.call_args.kwargs["overwrite"] is True diff --git a/tests/test_theme_barceloneta_integration.py b/tests/test_theme_barceloneta_integration.py index 811b307..3088d29 100644 --- a/tests/test_theme_barceloneta_integration.py +++ b/tests/test_theme_barceloneta_integration.py @@ -33,6 +33,9 @@ def _templates_dir() -> Path: + env_dir = os.environ.get("PLONECLI_TEMPLATES_DIR") + if env_dir and Path(env_dir).exists(): + return Path(env_dir) if DEV_TEMPLATES_DIR.exists(): return DEV_TEMPLATES_DIR if FALLBACK_TEMPLATES_DIR.exists():