mirror of
https://github.com/copier-org/copier.git
synced 2025-05-05 15:32:54 +00:00
336 lines
10 KiB
Python
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"
|