copier/tests/test_answersfile.py
2025-04-22 13:52:51 +02:00

336 lines
10 KiB
Python

from __future__ import annotations
import json
import sys
from pathlib import Path
from textwrap import dedent
import pytest
import yaml
import copier
from copier._user_data import load_answersfile_data
from .helpers import BRACKET_ENVOPS_JSON, SUFFIX_TMPL, build_file_tree, git_save
@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 ]].tmpl"): (
"""\
# Changes here will be overwritten by Copier
[[ _copier_answers|to_nice_yaml ]]
"""
),
(root / "copier.yml"): (
f"""\
_answers_file: .answers-file-changed-in-template.yml
_templates_suffix: {SUFFIX_TMPL}
_envops: {BRACKET_ENVOPS_JSON}
round: 1st
# password_1 and password_2 must not appear in answers file
_secret_questions:
- password_1
password_1: password one
password_2:
secret: yes
default: password two
"""
),
(root / "round.txt.tmpl"): (
"""\
It's the [[round]] round.
password_1=[[password_1]]
password_2=[[password_2]]
"""
),
}
)
return str(root)
@pytest.mark.parametrize("answers_file", [None, ".changed-by-user.yaml"])
def test_answersfile(
template_path: str, tmp_path: Path, answers_file: str | None
) -> None:
"""Test copier behaves properly when using an answersfile."""
round_file = tmp_path / "round.txt"
# Check 1st round is properly executed and remembered
copier.run_copy(
template_path,
tmp_path,
answers_file=answers_file,
defaults=True,
overwrite=True,
)
answers_file = answers_file or ".answers-file-changed-in-template.yml"
assert (
round_file.read_text()
== dedent(
"""
It's the 1st round.
password_1=password one
password_2=password two
"""
).lstrip()
)
log = load_answersfile_data(tmp_path, answers_file)
assert log["round"] == "1st"
assert "password_1" not in log
assert "password_2" not in log
# Check 2nd round is properly executed and remembered
copier.run_copy(
template_path,
tmp_path,
{"round": "2nd"},
answers_file=answers_file,
defaults=True,
overwrite=True,
)
assert (
round_file.read_text()
== dedent(
"""
It's the 2nd round.
password_1=password one
password_2=password two
"""
).lstrip()
)
log = load_answersfile_data(tmp_path, answers_file)
assert log["round"] == "2nd"
assert "password_1" not in log
assert "password_2" not in log
# Check repeating 2nd is properly executed and remembered
copier.run_copy(
template_path,
tmp_path,
answers_file=answers_file,
defaults=True,
overwrite=True,
)
assert (
round_file.read_text()
== dedent(
"""
It's the 2nd round.
password_1=password one
password_2=password two
"""
).lstrip()
)
log = load_answersfile_data(tmp_path, answers_file)
assert log["round"] == "2nd"
assert "password_1" not in log
assert "password_2" not in log
def test_external_data(tmp_path_factory: pytest.TempPathFactory) -> None:
parent1, parent2, child, dst = map(
tmp_path_factory.mktemp, ("parent1", "parent2", "child", "dst")
)
build_file_tree(
{
(parent1 / "copier.yaml"): "{name: P1, child: C1}",
(parent1 / "parent1.txt.jinja"): "{{ name }}",
(
parent1 / "{{ _copier_conf.answers_file }}.jinja"
): "{{ _copier_answers|to_nice_yaml -}}",
(parent2 / "copier.yaml"): "name: P2",
(parent2 / "parent2.txt.jinja"): "{{ name }}",
(
parent2 / "{{ _copier_conf.answers_file }}.jinja"
): "{{ _copier_answers|to_nice_yaml -}}",
(child / "copier.yml"): (
"""\
_external_data:
parent1: .copier-answers.yml
parent2: "{{ parent2_answers }}"
parent2_answers: .parent2-answers.yml
name: "{{ _external_data.parent2.child | d(_external_data.parent1.child) }}"
"""
),
(child / "combined.json.jinja"): """\
{
"parent1": {{ _external_data.parent1.name | tojson }},
"parent2": {{ _external_data.parent2.name | tojson }},
"child": {{ name | tojson }}
}
""",
(
child / "{{ _copier_conf.answers_file }}.jinja"
): "{{ _copier_answers|to_nice_yaml -}}",
}
)
git_save(parent1, tag="v1.0+parent1")
git_save(parent2, tag="v1.0+parent2")
git_save(child, tag="v1.0+child")
# Apply parent 1. At this point we don't know we'll want more than 1
# template in the same subproject, so we leave the default answers file.
copier.run_copy(str(parent1), dst, defaults=True, overwrite=True)
git_save(dst)
assert (dst / "parent1.txt").read_text() == "P1"
expected_parent1_answers = {
"_src_path": str(parent1),
"_commit": "v1.0+parent1",
"name": "P1",
"child": "C1",
}
assert load_answersfile_data(dst, ".copier-answers.yml") == expected_parent1_answers
# Apply parent 2. It uses a different answers file.
copier.run_copy(
str(parent2),
dst,
defaults=True,
overwrite=True,
answers_file=".parent2-answers.yml",
)
git_save(dst)
assert (dst / "parent2.txt").read_text() == "P2"
expected_parent2_answers = {
"_commit": "v1.0+parent2",
"_src_path": str(parent2),
"name": "P2",
}
assert (
load_answersfile_data(dst, ".parent2-answers.yml") == expected_parent2_answers
)
# Apply child. It can access answers from both parents.
copier.run_copy(
str(child),
dst,
defaults=True,
overwrite=True,
answers_file=".child-answers.yml",
)
git_save(dst)
assert load_answersfile_data(dst, ".child-answers.yml") == {
"_commit": "v1.0+child",
"_src_path": str(child),
"name": "C1",
"parent2_answers": ".parent2-answers.yml",
}
assert json.loads((dst / "combined.json").read_text()) == {
"parent1": "P1",
"parent2": "P2",
"child": "C1",
}
def test_external_data_with_umlaut(
tmp_path_factory: pytest.TempPathFactory,
) -> None:
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
build_file_tree(
{
(src / "copier.yml"): (
"""\
_external_data:
data: data.yml
ext_umlaut: "{{ _external_data.data.umlaut }}"
"""
),
(src / "{{ _copier_conf.answers_file }}.jinja"): (
"{{ _copier_answers|to_nice_yaml }}"
),
}
)
git_save(src, tag="v1")
(dst / "data.yml").write_text("umlaut: äöü", encoding="utf-8")
copier.run_copy(str(src), dst, defaults=True, overwrite=True)
answers = load_answersfile_data(dst, ".copier-answers.yml")
assert answers["ext_umlaut"] == "äöü"
@pytest.mark.parametrize(
"encoding",
["utf-8", "utf-8-sig", "utf-16-le", "utf-16-be"],
)
def test_external_data_with_unicode_characters(
tmp_path_factory: pytest.TempPathFactory,
monkeypatch: pytest.MonkeyPatch,
encoding: str,
) -> None:
def _encode(data: str) -> bytes:
if encoding.startswith("utf-16"):
data = f"\ufeff{data}"
return data.encode(encoding)
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
build_file_tree(
{
(src / "copier.yml"): (
"""\
_external_data:
data: data.yml
foo: "{{ _external_data.data.foo }}"
bar: "{{ _external_data.data.bar }}"
"""
),
(src / "{{ _copier_conf.answers_file }}.jinja"): (
"{{ _copier_answers|to_nice_yaml }}"
),
}
)
git_save(src, tag="v1")
data = {
"foo": "\u3053\u3093\u306b\u3061\u306f", # japanese hiragana
"bar": "\U0001f60e", # smiling face with sunglasses
}
(dst / "data.yml").write_bytes(_encode(yaml.dump(data, allow_unicode=True)))
with monkeypatch.context() as m:
# Override the factor that determines the default encoding when opening files.
if sys.version_info >= (3, 10):
m.setattr("io.text_encoding", lambda *_args: "cp932")
else:
m.setattr("_bootlocale.getpreferredencoding", lambda *_args: "cp932")
copier.run_copy(str(src), dst, defaults=True, overwrite=True)
answers = load_answersfile_data(dst)
assert answers["foo"] == data["foo"]
assert answers["bar"] == data["bar"]
def test_undefined_phase_in_external_data(
tmp_path_factory: pytest.TempPathFactory,
) -> None:
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
build_file_tree(
{
(src / "copier.yml"): (
"""\
_external_data:
data: '{{ _copier_phase }}.yml'
key: "{{ _external_data.data.key }}"
"""
),
(src / "{{ _copier_conf.answers_file }}.jinja"): (
"{{ _copier_answers|to_nice_yaml }}"
),
}
)
git_save(src, tag="v1")
(dst / "undefined.yml").write_text("key: value")
copier.run_copy(str(src), dst, defaults=True, overwrite=True)
answers = load_answersfile_data(dst, ".copier-answers.yml")
assert answers["key"] == "value"