Skip to content

Commit

Permalink
feat: allow overriding data file with CLI arguments (#1332)
Browse files Browse the repository at this point in the history
Users should be able to pass CLI data arguments and use a `--data-file` at the same time.  Let's just take inspiration from the [helm values file](https://helm.sh/docs/chart_template_guide/values_files/) and give highest priority to data values passed at the command line.
  • Loading branch information
mspiegel31 authored Sep 29, 2023
1 parent 74769bc commit 61cbe79
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 23 deletions.
13 changes: 9 additions & 4 deletions copier/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ class CopierApp(cli.Application):
class _Subcommand(cli.Application):
"""Base class for Copier subcommands."""

data: AnyByStrDict = {}
def __init__(self, executable):
self.data: AnyByStrDict = {}
super().__init__(executable)

answers_file = cli.SwitchAttr(
["-a", "--answers-file"],
Expand Down Expand Up @@ -158,7 +160,6 @@ class _Subcommand(cli.Application):
"VARIABLE=VALUE",
list=True,
help="Make VARIABLE available as VALUE when rendering the template",
excludes=["--data-file"],
)
def data_switch(self, values: StrSeq) -> None:
"""Update [data][] with provided values.
Expand All @@ -175,7 +176,6 @@ def data_switch(self, values: StrSeq) -> None:
["--data-file"],
cli.ExistingFile,
help="Load data from a YAML file",
excludes=["--data"],
)
def data_file_switch(self, path: cli.ExistingFile) -> None:
"""Update [data][] with provided values.
Expand All @@ -184,7 +184,12 @@ def data_file_switch(self, path: cli.ExistingFile) -> None:
path: The path to the YAML file to load.
"""
with open(path) as f:
self.data.update(yaml.safe_load(f))
file_updates: AnyByStrDict = yaml.safe_load(f)

updates_without_cli_overrides = {
k: v for k, v in file_updates.items() if k not in self.data
}
self.data.update(updates_without_cli_overrides)

def _worker(self, src_path: OptStr = None, dst_path: str = ".", **kwargs) -> Worker:
"""
Expand Down
13 changes: 8 additions & 5 deletions docs/configuring.md
Original file line number Diff line number Diff line change
Expand Up @@ -758,10 +758,6 @@ questions with default answers.
copier copy -fd 'user_name=Manuel Calavera' template destination
```

!!! info

The `-d, --data` flags are mutually exclusive with the [`--data-file`][data_file] flag.

### `data_file`

- Format: `str`
Expand Down Expand Up @@ -797,9 +793,16 @@ contains your data.
copier copy -d 'user_name=Manuel Calavera' -d 'age=7' -d 'height=1.83' template destination
```

if you'd like to override some of the answers in the file, `--data` flags always take
precedence

```shell
copier copy -d 'user_name=Bilbo Baggins' --data-file input.yml template destination
```

!!! info

The `--data-file` flag is mutually exclusive with the [`-d, --data`][data] flags.
command line arguments passed via `--data` will always take precedence over the answers file

### `envops`

Expand Down
37 changes: 23 additions & 14 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,18 +170,20 @@ def test_data_file_parsed_by_question_type(
assert answers["question"] == "answer"


def test_data_and_data_file_mutually_exclusive(
def test_data_cli_takes_precedence_over_data_file(
tmp_path_factory: pytest.TempPathFactory,
capsys: pytest.CaptureFixture[str],
):
src, dst, local = map(tmp_path_factory.mktemp, ("src", "dst", "local"))

build_file_tree(
{
(src / "copier.yml"): (
"""\
question:
type: str
another_question:
type: str
yet_another_question:
type: str
"""
),
(src / "{{ _copier_conf.answers_file }}.jinja"): (
Expand All @@ -190,31 +192,38 @@ def test_data_and_data_file_mutually_exclusive(
{{ _copier_answers|to_nice_yaml }}
"""
),
(local / "data.yml"): yaml.dump({"question": "does not matter"}),
(local / "data.yml"): yaml.dump(
{
"question": "does not matter",
"another_question": "another answer!",
"yet_another_question": "sure, one more for you!",
}
),
}
)

answer_override = "fourscore and seven years ago"
run_result = CopierApp.run(
[
"copier",
"copy",
f"--data-file={local / 'data.yml'}",
'--data=question="also does not matter"',
f"--data=question={answer_override}",
str(src),
str(dst),
"--quiet",
],
exit=False,
)
assert run_result[1] == 2 # cli failure
out, _ = capsys.readouterr()
assert any(
[
"Error: Given --data-file, the following are invalid ['--data']" in out,
"Error: Given --data, the following are invalid ['--data-file']" in out,
]
)
assert not (dst / ".copier-answers.yml").exists()
assert run_result[1] == 0
actual_answers = yaml.safe_load((dst / ".copier-answers.yml").read_text())
expected_answers = {
"_src_path": str(src),
"question": answer_override,
"another_question": "another answer!",
"yet_another_question": "sure, one more for you!",
}
assert actual_answers == expected_answers


@pytest.mark.parametrize(
Expand Down

0 comments on commit 61cbe79

Please sign in to comment.