Skip to content

Commit 189a2c5

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

File tree

7 files changed

+83
-17
lines changed

7 files changed

+83
-17
lines changed

.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]

src/ansiblelint/transformer.py

Lines changed: 5 additions & 1 deletion
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

src/ansiblelint/yaml_utils.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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,11 @@ 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+
version = tuple(version.split("."))
825+
self._yaml_version_default: VersionType = version
826+
self._yaml_version: VersionType = self._yaml_version_default
822827

823828
super().__init__(typ=typ, pure=pure, output=output, plug_ins=plug_ins)
824829

@@ -874,6 +879,9 @@ def __init__(
874879
# (see https://stackoverflow.com/a/44314840/1134951)
875880
# self.Representer.add_representer(
876881

882+
if version:
883+
self.version = version
884+
877885
@staticmethod
878886
def _defaults_from_yamllint_config() -> dict[str, bool | int | str]:
879887
"""Extract FormattedYAML-relevant settings from yamllint config if possible."""
@@ -920,16 +928,18 @@ def _defaults_from_yamllint_config() -> dict[str, bool | int | str]:
920928

921929
return cast(dict[str, Union[bool, int, str]], config)
922930

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

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

944958
def load(self, stream: Path | StreamTextType) -> Any:
945959
"""Load YAML content from a string while avoiding known ruamel.yaml issues."""
@@ -958,6 +972,9 @@ def load(self, stream: Path | StreamTextType) -> Any:
958972
except ParserError:
959973
data = None
960974
_logger.error("Invalid yaml, verify the file contents and try again.")
975+
except Exception as exc:
976+
breakpoint()
977+
raise exc
961978
if preamble_comment is not None and isinstance(
962979
data,
963980
(CommentedMap, CommentedSeq),
@@ -977,7 +994,11 @@ def dumps(self, data: Any) -> str:
977994
stream.write(preamble_comment)
978995
self.dump(data, stream)
979996
text = stream.getvalue()
980-
return self._post_process_yaml(text)
997+
strip_version_directive = hasattr(self, "_yaml_version_default")
998+
return self._post_process_yaml(
999+
text,
1000+
strip_version_directive=strip_version_directive,
1001+
)
9811002

9821003
def _prevent_wrapping_flow_style(self, data: Any) -> None:
9831004
if not isinstance(data, (CommentedMap, CommentedSeq)):
@@ -1065,7 +1086,7 @@ def _pre_process_yaml(self, text: str) -> tuple[str, str | None]:
10651086
return text, "".join(preamble_comments) or None
10661087

10671088
@staticmethod
1068-
def _post_process_yaml(text: str) -> str:
1089+
def _post_process_yaml(text: str, *, strip_version_directive: bool = False) -> str:
10691090
"""Handle known issues with ruamel.yaml dumping.
10701091
10711092
Make sure there's only one newline at the end of the file.
@@ -1077,6 +1098,10 @@ def _post_process_yaml(text: str) -> str:
10771098
10781099
Make sure null list items don't end in a space.
10791100
"""
1101+
# remove YAML directive
1102+
if strip_version_directive and text.startswith("%YAML"):
1103+
text = text.split("\n", 1)[1]
1104+
10801105
text = text.rstrip("\n") + "\n"
10811106

10821107
lines = text.splitlines(keepends=True)

test/test_yaml_utils.py

Lines changed: 38 additions & 1 deletion
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
(
@@ -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)