Skip to content

Commit 3f582b7

Browse files
committed
Use YAML 1.2 by default for reformatting
Fixes: #3790
1 parent 7b9041b commit 3f582b7

File tree

9 files changed

+87
-21
lines changed

9 files changed

+87
-21
lines changed

.config/dictionary.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ matchtasks
208208
matchvar
209209
matchyaml
210210
maxdepth
211+
maxsplit
211212
minversion
212213
mkdir
213214
mkdocs

.config/requirements-lock.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ cffi==1.16.0
1414
charset-normalizer==3.3.1
1515
click==8.1.7
1616
cryptography==41.0.5
17-
filelock==3.12.4
17+
filelock==3.13.0
1818
idna==3.4
1919
importlib-resources==5.0.7
2020
jinja2==3.1.2
@@ -34,7 +34,7 @@ referencing==0.30.2
3434
requests==2.31.0
3535
rich==13.6.0
3636
rpds-py==0.10.6
37-
ruamel-yaml==0.18.2
37+
ruamel-yaml==0.18.3
3838
subprocess-tee==0.4.1
3939
tomli==2.0.1
4040
typing-extensions==4.8.0

.config/requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ defusedxml==0.7.1
2929
dill==0.3.7
3030
exceptiongroup==1.1.3
3131
execnet==2.0.2
32-
filelock==3.12.4
32+
filelock==3.13.0
3333
ghp-import==2.1.0
3434
griffe==0.36.4
3535
htmlmin2==0.1.13
@@ -91,7 +91,7 @@ regex==2023.8.8
9191
requests==2.31.0
9292
rich==13.6.0
9393
rpds-py==0.10.6
94-
ruamel-yaml==0.18.2
94+
ruamel-yaml==0.18.3
9595
six==1.16.0
9696
soupsieve==2.5
9797
subprocess-tee==0.4.1

.github/workflows/tox.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ jobs:
7070
env:
7171
# Number of expected test passes, safety measure for accidental skip of
7272
# tests. Update value if you add/remove tests.
73-
PYTEST_REQPASS: 834
73+
PYTEST_REQPASS: 845
7474
steps:
7575
- uses: actions/checkout@v4
7676
with:

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ repos:
131131
types: [file, yaml]
132132
entry: yamllint --strict
133133
- repo: https://github.com/astral-sh/ruff-pre-commit
134-
rev: "v0.1.2"
134+
rev: "v0.1.3"
135135
hooks:
136136
- id: ruff
137137
args: [--fix, --exit-non-zero-on-fix]

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,6 @@ python_files = [
233233
xfail_strict = true
234234

235235
[tool.ruff]
236-
required-version = "0.1.2"
237236
ignore = [
238237
"D203", # incompatible with D211
239238
"D213", # incompatible with D212

src/ansiblelint/transformer.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88

99
from ansiblelint.file_utils import Lintable
1010
from ansiblelint.rules import AnsibleLintRule, TransformMixin
11-
from ansiblelint.yaml_utils import FormattedYAML, get_path_to_play, get_path_to_task
11+
from ansiblelint.yaml_utils import (
12+
FormattedYAML,
13+
get_path_to_play,
14+
get_path_to_task,
15+
)
1216

1317
if TYPE_CHECKING:
1418
from ansiblelint.config import Options
@@ -92,7 +96,9 @@ def run(self) -> None:
9296
# We need a fresh YAML() instance for each load because ruamel.yaml
9397
# stores intermediate state during load which could affect loading
9498
# any other files. (Based on suggestion from ruamel.yaml author)
95-
yaml = FormattedYAML()
99+
yaml = FormattedYAML(
100+
version=(1, 1) if file.is_owned_by_ansible() else None,
101+
)
96102

97103
ruamel_data = yaml.load(data)
98104
if not isinstance(ruamel_data, (CommentedMap, CommentedSeq)):

src/ansiblelint/yaml_utils.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
if TYPE_CHECKING:
3636
# noinspection PyProtectedMember
3737
from ruamel.yaml.comments import LineCol
38-
from ruamel.yaml.compat import StreamTextType
38+
from ruamel.yaml.compat import StreamTextType, VersionType
3939
from ruamel.yaml.nodes import ScalarNode
4040
from ruamel.yaml.representer import RoundTripRepresenter
4141
from ruamel.yaml.tokens import CommentToken
@@ -44,6 +44,7 @@
4444

4545
_logger = logging.getLogger(__name__)
4646

47+
4748
YAMLLINT_CONFIG = """
4849
extends: default
4950
rules:
@@ -750,13 +751,14 @@ def write_version_directive(self, version_text: Any) -> None:
750751
class FormattedYAML(YAML):
751752
"""A YAML loader/dumper that handles ansible content better by default."""
752753

753-
def __init__(
754+
def __init__( # pylint: disable=too-many-arguments
754755
self,
755756
*,
756757
typ: str | None = None,
757758
pure: bool = False,
758759
output: Any = None,
759760
plug_ins: list[str] | None = None,
761+
version: VersionType | None = None,
760762
):
761763
"""Return a configured ``ruamel.yaml.YAML`` instance.
762764
@@ -817,8 +819,12 @@ def __init__(
817819
- name: Task
818820
"""
819821
# Default to reading/dumping YAML 1.1 (ruamel.yaml defaults to 1.2)
820-
self._yaml_version_default: tuple[int, int] = (1, 1)
821-
self._yaml_version: str | tuple[int, int] = self._yaml_version_default
822+
if version:
823+
if isinstance(version, str):
824+
x, y = version.split(".", maxsplit=1)
825+
version = (int(x), int(y))
826+
self._yaml_version_default: VersionType = version
827+
self._yaml_version: VersionType = self._yaml_version_default
822828

823829
super().__init__(typ=typ, pure=pure, output=output, plug_ins=plug_ins)
824830

@@ -874,6 +880,9 @@ def __init__(
874880
# (see https://stackoverflow.com/a/44314840/1134951)
875881
# self.Representer.add_representer(
876882

883+
if version:
884+
self.version = version
885+
877886
@staticmethod
878887
def _defaults_from_yamllint_config() -> dict[str, bool | int | str]:
879888
"""Extract FormattedYAML-relevant settings from yamllint config if possible."""
@@ -920,16 +929,18 @@ def _defaults_from_yamllint_config() -> dict[str, bool | int | str]:
920929

921930
return cast(dict[str, Union[bool, int, str]], config)
922931

923-
@property # type: ignore[override]
924-
def version(self) -> str | tuple[int, int]:
932+
@property
933+
def version(self) -> VersionType | None:
925934
"""Return the YAML version used to parse or dump.
926935
927936
Ansible uses PyYAML which only supports YAML 1.1. ruamel.yaml defaults to 1.2.
928937
So, we have to make sure we dump yaml files using YAML 1.1.
929938
We can relax the version requirement once ansible uses a version of PyYAML
930939
that includes this PR: https://github.com/yaml/pyyaml/pull/555
931940
"""
932-
return self._yaml_version
941+
if hasattr(self, "_yaml_version"):
942+
return self._yaml_version
943+
return None
933944

934945
@version.setter
935946
def version(self, value: str | tuple[int, int] | None) -> None:
@@ -939,7 +950,11 @@ def version(self, value: str | tuple[int, int] | None) -> None:
939950
So, if a file does not include the directive, it sets this to None.
940951
But, None effectively resets the parsing version to YAML 1.2 (ruamel's default).
941952
"""
942-
self._yaml_version = value if value is not None else self._yaml_version_default
953+
if value is not None:
954+
self._yaml_version = value
955+
elif hasattr(self, "_yaml_version_default"):
956+
self._yaml_version = self._yaml_version_default
957+
# We do nothing if the object did not had a previous default version defined
943958

944959
def load(self, stream: Path | StreamTextType) -> Any:
945960
"""Load YAML content from a string while avoiding known ruamel.yaml issues."""
@@ -977,7 +992,11 @@ def dumps(self, data: Any) -> str:
977992
stream.write(preamble_comment)
978993
self.dump(data, stream)
979994
text = stream.getvalue()
980-
return self._post_process_yaml(text)
995+
strip_version_directive = hasattr(self, "_yaml_version_default")
996+
return self._post_process_yaml(
997+
text,
998+
strip_version_directive=strip_version_directive,
999+
)
9811000

9821001
def _prevent_wrapping_flow_style(self, data: Any) -> None:
9831002
if not isinstance(data, (CommentedMap, CommentedSeq)):
@@ -1065,7 +1084,7 @@ def _pre_process_yaml(self, text: str) -> tuple[str, str | None]:
10651084
return text, "".join(preamble_comments) or None
10661085

10671086
@staticmethod
1068-
def _post_process_yaml(text: str) -> str:
1087+
def _post_process_yaml(text: str, *, strip_version_directive: bool = False) -> str:
10691088
"""Handle known issues with ruamel.yaml dumping.
10701089
10711090
Make sure there's only one newline at the end of the file.
@@ -1077,6 +1096,10 @@ def _post_process_yaml(text: str) -> str:
10771096
10781097
Make sure null list items don't end in a space.
10791098
"""
1099+
# remove YAML directive
1100+
if strip_version_directive and text.startswith("%YAML"):
1101+
text = text.split("\n", 1)[1]
1102+
10801103
text = text.rstrip("\n") + "\n"
10811104

10821105
lines = text.splitlines(keepends=True)

test/test_yaml_utils.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
if TYPE_CHECKING:
1717
from ruamel.yaml.comments import CommentedMap, CommentedSeq
18+
from ruamel.yaml.compat import VersionType
1819
from ruamel.yaml.emitter import Emitter
1920

2021
fixtures_dir = Path(__file__).parent / "fixtures"
@@ -219,6 +220,39 @@ def fixture_yaml_formatting_fixtures(fixture_filename: str) -> tuple[str, str, s
219220
return before_content, prettier_content, formatted_content
220221

221222

223+
@pytest.mark.parametrize(
224+
("before", "after", "version"),
225+
(
226+
pytest.param("---\nfoo: bar\n", "---\nfoo: bar\n", None, id="1"),
227+
# verify that 'on' is not translated to bool (1.2 behavior)
228+
pytest.param("---\nfoo: on\n", "---\nfoo: on\n", None, id="2"),
229+
# When version is manually mentioned by us, we expect to output without version directive
230+
pytest.param("---\nfoo: on\n", "---\nfoo: on\n", (1, 2), id="3"),
231+
pytest.param("---\nfoo: on\n", "---\nfoo: true\n", (1, 1), id="4"),
232+
pytest.param("%YAML 1.1\n---\nfoo: on\n", "---\nfoo: true\n", (1, 1), id="5"),
233+
# verify that in-line directive takes precedence but dumping strips if we mention a specific version
234+
pytest.param("%YAML 1.1\n---\nfoo: on\n", "---\nfoo: true\n", (1, 2), id="6"),
235+
# verify that version directive are kept if present
236+
pytest.param("%YAML 1.1\n---\nfoo: on\n", "---\nfoo: true\n", None, id="7"),
237+
pytest.param(
238+
"%YAML 1.2\n---\nfoo: on\n",
239+
"%YAML 1.2\n---\nfoo: on\n",
240+
None,
241+
id="8",
242+
),
243+
pytest.param("---\nfoo: YES\n", "---\nfoo: true\n", (1, 1), id="9"),
244+
pytest.param("---\nfoo: YES\n", "---\nfoo: YES\n", (1, 2), id="10"),
245+
pytest.param("---\nfoo: YES\n", "---\nfoo: YES\n", None, id="11"),
246+
),
247+
)
248+
def test_fmt(before: str, after: str, version: VersionType) -> None:
249+
"""Tests behavior of formatter in regards to different YAML versions, specified or not."""
250+
yaml = ansiblelint.yaml_utils.FormattedYAML(version=version)
251+
data = yaml.load(before)
252+
result = yaml.dumps(data)
253+
assert result == after
254+
255+
222256
@pytest.mark.parametrize(
223257
"fixture_filename",
224258
(
@@ -229,7 +263,7 @@ def fixture_yaml_formatting_fixtures(fixture_filename: str) -> tuple[str, str, s
229263
pytest.param("fmt-5.yml", id="5"),
230264
),
231265
)
232-
def test_formatted_yaml_loader_dumper(
266+
def test_formatted_yaml11_loader_dumper(
233267
yaml_formatting_fixtures: tuple[str, str, str],
234268
fixture_filename: str, # noqa: ARG001
235269
) -> None:
@@ -239,7 +273,7 @@ def test_formatted_yaml_loader_dumper(
239273
assert before_content != prettier_content
240274
assert before_content != after_content
241275

242-
yaml = ansiblelint.yaml_utils.FormattedYAML()
276+
yaml = ansiblelint.yaml_utils.FormattedYAML(version=(1, 1))
243277

244278
data_before = yaml.load(before_content)
245279
dump_from_before = yaml.dumps(data_before)
@@ -263,6 +297,9 @@ def test_formatted_yaml_loader_dumper(
263297
# Running our files through yamllint, after we reformatted them,
264298
# should not yield any problems.
265299
config = ansiblelint.yaml_utils.load_yamllint_config()
300+
# We disable truthy rule as since we switched to default YAML 1.2 loading
301+
# on ruamel.yaml, we expect to not auto-repair these on dumping.
302+
config.rules["truthy"] = False
266303
assert not list(run_yamllint(after_content, config))
267304

268305

0 commit comments

Comments
 (0)