mirror of
https://github.com/copier-org/copier.git
synced 2025-05-05 23:42:55 +00:00
This change addresses a usability issue troubling users when they run 'copier update'. Previously, each time 'copier update' was run, the user had to review all responses given in the answers file. This was a repetitive and laborious process, particularly for users dealing with extensive templates. This PR introduces a new flag --skip-answered to improve the user experience and reduce friction during updates. The new flag allows users to bypass the review of questions already answered in the past. It automatically pulls responses from the answers file and presents only the newly added template questions to the user for their review. `skip-answered` will skip all answered questions and ask user input only for new inputs if they are present. Fixes #1178 Co-authored-by: Jairo Llopis <973709+yajo@users.noreply.github.com>
272 lines
7.6 KiB
Python
272 lines
7.6 KiB
Python
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Callable, Generator
|
|
|
|
import pytest
|
|
import yaml
|
|
from plumbum import local
|
|
from plumbum.cmd import git
|
|
|
|
from copier.cli import CopierApp
|
|
|
|
from .helpers import COPIER_CMD, build_file_tree
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def template_path(tmp_path_factory: pytest.TempPathFactory) -> str:
|
|
root = tmp_path_factory.mktemp("template")
|
|
build_file_tree(
|
|
{
|
|
(root / "{{ _copier_conf.answers_file }}.jinja"): (
|
|
"""\
|
|
# Changes here will be overwritten by Copier
|
|
{{ _copier_answers|to_nice_yaml }}
|
|
"""
|
|
),
|
|
(root / "a.txt"): "EXAMPLE_CONTENT",
|
|
}
|
|
)
|
|
return str(root)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def template_path_with_dot_config(
|
|
tmp_path_factory: pytest.TempPathFactory,
|
|
) -> Generator[Callable[[str], str], str, None]:
|
|
def _template_path_with_dot_config(config_folder: str) -> str:
|
|
root = tmp_path_factory.mktemp("template")
|
|
build_file_tree(
|
|
{
|
|
root
|
|
/ config_folder
|
|
/ "{{ _copier_conf.answers_file }}.jinja": """\
|
|
# Changes here will be overwritten by Copier
|
|
{{ _copier_answers|to_nice_yaml }}
|
|
""",
|
|
root / "a.txt": "EXAMPLE_CONTENT",
|
|
}
|
|
)
|
|
return str(root)
|
|
|
|
yield _template_path_with_dot_config
|
|
|
|
|
|
def test_good_cli_run(template_path: str, tmp_path: Path) -> None:
|
|
run_result = CopierApp.run(
|
|
[
|
|
"copier",
|
|
"copy",
|
|
"--quiet",
|
|
"-a",
|
|
"altered-answers.yml",
|
|
template_path,
|
|
str(tmp_path),
|
|
],
|
|
exit=False,
|
|
)
|
|
a_txt = tmp_path / "a.txt"
|
|
assert run_result[1] == 0
|
|
assert a_txt.exists()
|
|
assert a_txt.is_file()
|
|
assert a_txt.read_text() == "EXAMPLE_CONTENT"
|
|
answers = yaml.safe_load((tmp_path / "altered-answers.yml").read_text())
|
|
assert answers["_src_path"] == template_path
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"type_, answer, expected",
|
|
[
|
|
("str", "string", "string"),
|
|
("str", "1", "1"),
|
|
("str", "1.2", "1.2"),
|
|
("str", "true", "true"),
|
|
("str", "null", "null"),
|
|
("str", '["string", 1, 1.2, true, null]', '["string", 1, 1.2, true, null]'),
|
|
("int", "1", 1),
|
|
("float", "1.2", 1.2),
|
|
("bool", "true", True),
|
|
("bool", "false", False),
|
|
("yaml", '"string"', "string"),
|
|
("yaml", "1", 1),
|
|
("yaml", "1.2", 1.2),
|
|
("yaml", "true", True),
|
|
("yaml", "null", None),
|
|
("yaml", '["string", 1, 1.2, true, null]', ["string", 1, 1.2, True, None]),
|
|
("json", '"string"', "string"),
|
|
("json", "1", 1),
|
|
("json", "1.2", 1.2),
|
|
("json", "true", True),
|
|
("json", "null", None),
|
|
("json", '["string", 1, 1.2, true, null]', ["string", 1, 1.2, True, None]),
|
|
],
|
|
)
|
|
def test_cli_data_parsed_by_question_type(
|
|
tmp_path_factory: pytest.TempPathFactory, type_, answer, expected
|
|
) -> None:
|
|
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
|
|
build_file_tree(
|
|
{
|
|
(src / "copier.yml"): (
|
|
f"""\
|
|
question:
|
|
type: {type_}
|
|
"""
|
|
),
|
|
(src / "{{ _copier_conf.answers_file }}.jinja"): (
|
|
"""\
|
|
# Changes here will be overwritten by Copier
|
|
{{ _copier_answers|to_nice_yaml }}
|
|
"""
|
|
),
|
|
}
|
|
)
|
|
run_result = CopierApp.run(
|
|
["copier", "copy", f"--data=question={answer}", str(src), str(dst), "--quiet"],
|
|
exit=False,
|
|
)
|
|
assert run_result[1] == 0
|
|
answers = yaml.safe_load((dst / ".copier-answers.yml").read_text())
|
|
assert answers["_src_path"] == str(src)
|
|
assert answers["question"] == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"config_folder",
|
|
map(
|
|
Path,
|
|
["", ".config/", ".config/subconfig/", ".config/subconfig/deep_nested_config/"],
|
|
),
|
|
)
|
|
def test_good_cli_run_dot_config(
|
|
tmp_path: Path,
|
|
template_path_with_dot_config: Callable[[str], str],
|
|
config_folder: Path,
|
|
) -> None:
|
|
"""Function to test different locations of the answersfile.
|
|
|
|
Added based on the discussion: https://github.com/copier-org/copier/discussions/859
|
|
"""
|
|
|
|
src = template_path_with_dot_config(str(config_folder))
|
|
|
|
with local.cwd(src):
|
|
git_commands()
|
|
|
|
run_result = CopierApp.run(
|
|
[
|
|
"copier",
|
|
"copy",
|
|
"--quiet",
|
|
"-a",
|
|
str(config_folder / "altered-answers.yml"),
|
|
src,
|
|
str(tmp_path),
|
|
],
|
|
exit=False,
|
|
)
|
|
a_txt = tmp_path / "a.txt"
|
|
assert run_result[1] == 0
|
|
assert a_txt.exists()
|
|
assert a_txt.is_file()
|
|
assert a_txt.read_text() == "EXAMPLE_CONTENT"
|
|
answers = yaml.safe_load(
|
|
(tmp_path / config_folder / "altered-answers.yml").read_text()
|
|
)
|
|
assert answers["_src_path"] == src
|
|
|
|
with local.cwd(str(tmp_path)):
|
|
git_commands()
|
|
|
|
run_update = CopierApp.run(
|
|
[
|
|
"copier",
|
|
"update",
|
|
str(tmp_path),
|
|
"--answers-file",
|
|
str(config_folder / "altered-answers.yml"),
|
|
"--quiet",
|
|
],
|
|
exit=False,
|
|
)
|
|
assert run_update[1] == 0
|
|
assert answers["_src_path"] == src
|
|
|
|
|
|
def git_commands() -> None:
|
|
git("init")
|
|
git("add", "-A")
|
|
git("commit", "-m", "init")
|
|
|
|
|
|
def test_help() -> None:
|
|
_help = COPIER_CMD("--help-all")
|
|
assert "copier copy [SWITCHES] template_src destination_path" in _help
|
|
assert "copier update [SWITCHES] [destination_path=.]" in _help
|
|
|
|
|
|
def test_copy_help() -> None:
|
|
_help = COPIER_CMD("copy", "--help")
|
|
assert "copier copy [SWITCHES] template_src destination_path" in _help
|
|
|
|
|
|
def test_update_help() -> None:
|
|
_help = COPIER_CMD("update", "--help")
|
|
assert "-o, --conflict" in _help
|
|
assert "copier update [SWITCHES] [destination_path=.]" in _help
|
|
assert "--skip-answered" in _help
|
|
|
|
|
|
def test_python_run() -> None:
|
|
cmd = [sys.executable, "-m", "copier", "--help-all"]
|
|
assert subprocess.run(cmd, check=True).returncode == 0
|
|
|
|
|
|
def test_skip_filenotexists(template_path: str, tmp_path: Path) -> None:
|
|
run_result = CopierApp.run(
|
|
[
|
|
"copier",
|
|
"copy",
|
|
"--quiet",
|
|
"-a",
|
|
"altered-answers.yml",
|
|
"--overwrite",
|
|
"--skip=a.txt",
|
|
template_path,
|
|
str(tmp_path),
|
|
],
|
|
exit=False,
|
|
)
|
|
a_txt = tmp_path / "a.txt"
|
|
assert run_result[1] == 0
|
|
assert a_txt.exists()
|
|
assert a_txt.is_file()
|
|
assert a_txt.read_text() == "EXAMPLE_CONTENT"
|
|
answers = yaml.safe_load((tmp_path / "altered-answers.yml").read_text())
|
|
assert answers["_src_path"] == str(template_path)
|
|
|
|
|
|
def test_skip_fileexists(template_path: str, tmp_path: Path) -> None:
|
|
a_txt = tmp_path / "a.txt"
|
|
a_txt.write_text("PREVIOUS_CONTENT")
|
|
run_result = CopierApp.run(
|
|
[
|
|
"copier",
|
|
"copy",
|
|
"--quiet",
|
|
"-a",
|
|
"altered-answers.yml",
|
|
"--overwrite",
|
|
"--skip=a.txt",
|
|
template_path,
|
|
str(tmp_path),
|
|
],
|
|
exit=False,
|
|
)
|
|
assert run_result[1] == 0
|
|
assert a_txt.exists()
|
|
assert a_txt.is_file()
|
|
assert a_txt.read_text() == "PREVIOUS_CONTENT"
|
|
answers = yaml.safe_load((tmp_path / "altered-answers.yml").read_text())
|
|
assert answers["_src_path"] == str(template_path)
|