copier icon indicating copy to clipboard operation
copier copied to clipboard

`copier update` reformats YAML from answers file

Open lkubb opened this issue 1 year ago • 0 comments

Describe the problem

One of my templates has a type: yaml question for configuration + defaults of the generated project. Since this input is used to render documentation, its order is somewhat significant (more important keys first).

I wrote a custom yaml filter to keep the input order (+ some other tweaks) when dumping .copier-answers.yml, which works perfectly for recopy and update with --skip-answered. However, when omitting --skip-answered during updates, the dumped YAML for the question prompt is sorted automatically, destroying the order in the answers file.

Template

set -exo pipefail

mkdir repro
cd repro

<< 'EOF' > copier.yml
settings:
  type: yaml
  multiline: true

_jinja_extensions:
  - copier_templates_extensions.TemplateExtensionLoader
  - dumper.py:YamlDumper
EOF

<< 'EOF' > dumper.py
import yaml
from jinja2.ext import Extension


def represent_str(dumper, data):
    """
    Represent multiline strings using "|"
    """
    if len(data.splitlines()) > 1:
        return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
    return dumper.represent_scalar("tag:yaml.org,2002:str", data)


class OpinionatedYamlDumper(yaml.SafeDumper):
    """
    Indent lists by two spaces
    """

    def increase_indent(self, flow=False, indentless=False):
        return super().increase_indent(flow=flow, indentless=False)


OpinionatedYamlDumper.add_representer(str, represent_str)


class YamlDumper(Extension):
    def __init__(self, environment):
        super().__init__(environment)
        environment.filters["yaml"] = dump_yaml


def dump_yaml(data, flow_style=False, indent=0, sort_keys=False):
    ret = yaml.dump(
        data,
        Dumper=OpinionatedYamlDumper,
        indent=indent,
        default_flow_style=flow_style,
        canonical=False,
        sort_keys=sort_keys,
    )
    if ret.endswith("...\n"):
        ret = ret[:-4]
    return ret.strip()
EOF

<< 'EOF' > doc.md.jinja
{%- for key, val in settings.items() %}
* {{ key }}: {{ val }}
{%- endfor %}
EOF

<< 'EOF' > "{{ _copier_conf.answers_file }}.jinja"
# Autogenerated. Do not edit this by hand, use 'copier update'.
---
{{ _copier_answers | yaml }}
EOF

git init
git add .
git commit -m init
git tag 1.0.0

To Reproduce

From where we left off above:

cd ..
copier copy --trust --data settings='foo: foo
bar: bar' $(pwd)/repro foo
cd foo
git init
git add .
git commit -m init
cat doc.md
# * foo: foo
# * bar: bar
copier update --trust
# just accept the default via alt+enter / esc,enter
git diff -p

Even though I accepted the previous value verbatim, this prints:

diff --git a/.copier-answers.yml b/.copier-answers.yml
index a8a6d42..7434558 100644
--- a/.copier-answers.yml
+++ b/.copier-answers.yml
@@ -3,5 +3,5 @@
 _commit: 1.0.0
 _src_path: /Users/jeanluc/tmp/reprotemp/repro
 settings:
-  foo: foo
   bar: bar
+  foo: foo
diff --git a/doc.md b/doc.md
index 98bf876..c64a7c6 100644
--- a/doc.md
+++ b/doc.md
@@ -1,3 +1,3 @@
 
-* foo: foo
 * bar: bar
+* foo: foo

Logs

No response

Expected behavior

The default value in the question should not modify the order that is found in the answers file.

I would prefer if Copier kept the formatting exactly as dumped into the answers file, but this would be a bit more involved than just setting sort_keys=False in yaml.dumps and can be recovered.

Screenshots/screencasts/logs

No response

Operating system

macOS

Operating system distribution and version

Sequoia 15.1.1

Copier version

copier 9.4.1

Python version

CPython 3.13

Installation method

pipx+pypi (actually uv+pypi)

Additional context

As mentioned, this could be solved by just setting sort_keys=False here:

https://github.com/copier-org/copier/blob/2dc1687af389505a708f25b0bc4e37af56179e99/copier/user_data.py#L283-L285

I think this should be the default behavior since the current one encodes information in a lossy way.

lkubb avatar Dec 13 '24 17:12 lkubb