diff --git a/poetry.lock b/poetry.lock index 1429f37..58b4daa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "annotated-types" @@ -843,68 +843,6 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] -[[package]] -name = "pyyaml" -version = "6.0.2" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - [[package]] name = "requests" version = "2.32.3" @@ -961,6 +899,21 @@ requests = ">=2.0.0" [package.extras] rsa = ["oauthlib[signedtoken] (>=3.0.0)"] +[[package]] +name = "ruamel-yaml" +version = "0.18.6" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636"}, + {file = "ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b"}, +] + +[package.extras] +docs = ["mercurial (>5.7)", "ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + [[package]] name = "setuptools" version = "75.5.0" @@ -1132,4 +1085,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.13" -content-hash = "7cc5624c0ca0e90e61bc9e78905c348a878dc35272a93c59f8929fb2e39d8c7d" +content-hash = "c36823b0513614f214581956c3d0d2859a8149b6aa1c335a29fceac96f843582" diff --git a/pyproject.toml b/pyproject.toml index e442f9a..a381fce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,9 +21,9 @@ opentelemetry-exporter-otlp-proto-http = "^1.26.0" requests = "^2.32.0" urllib3 = "^2.2.3" pydantic = "^2.9.2" -pyyaml = "^6.0.1" atlassian-python-api = "^3.41.16" Jinja2 = "^3.1.4" +ruamel-yaml = "^0.18.6" [tool.poetry.group.dev.dependencies] requests-mock = "^1.12.1" diff --git a/src/retasc/models/config.py b/src/retasc/models/config.py index 7f9b3b2..e1af0ee 100644 --- a/src/retasc/models/config.py +++ b/src/retasc/models/config.py @@ -1,9 +1,10 @@ # SPDX-License-Identifier: GPL-3.0-or-later from pathlib import Path -import yaml from pydantic import BaseModel, Field +from retasc.yaml import yaml + class Config(BaseModel): rules_path: str = Field(description="Path to rules (processed recursively)") @@ -29,6 +30,6 @@ class Config(BaseModel): def parse_config(config_path: str) -> Config: with open(config_path) as f: - config_data = yaml.safe_load(f) + config_data = yaml().load(f) return Config(**config_data) diff --git a/src/retasc/models/generate_schema.py b/src/retasc/models/generate_schema.py index 765109e..4f3591e 100644 --- a/src/retasc/models/generate_schema.py +++ b/src/retasc/models/generate_schema.py @@ -3,24 +3,27 @@ import sys from typing import TextIO -import yaml - from retasc.models.config import Config from retasc.models.rule import Rule +from retasc.yaml import yaml -def _generate_schema(cls, file: TextIO, generator) -> None: - schema = cls.model_json_schema() - generator.dump(schema, file, indent=2, sort_keys=False) +def json_dump(schema, file: TextIO): + json.dump(schema, file, indent=2, sort_keys=False) + + +def yaml_dump(schema, file: TextIO): + yaml().dump(schema, file) def generate_schema( output_path: str | None, *, output_json: bool, config: bool = False ) -> None: - generator = json if output_json else yaml + generator = json_dump if output_json else yaml_dump cls = Config if config else Rule + schema = cls.model_json_schema() if output_path is None: - _generate_schema(cls, sys.stdout, generator) + generator(schema, sys.stdout) else: with open(output_path, "w") as schema_file: - _generate_schema(cls, schema_file, generator) + generator(schema, schema_file) diff --git a/src/retasc/models/parse_rules.py b/src/retasc/models/parse_rules.py index 96fbf50..d2492d8 100644 --- a/src/retasc/models/parse_rules.py +++ b/src/retasc/models/parse_rules.py @@ -6,12 +6,13 @@ from glob import iglob from itertools import chain -import yaml from pydantic import ValidationError +from ruamel.yaml.error import YAMLError from retasc.models.config import Config from retasc.models.rule import Rule from retasc.utils import to_comma_separated +from retasc.yaml import yaml logger = logging.getLogger(__name__) @@ -31,7 +32,7 @@ def iterate_yaml_files(path: str): def parse_yaml_objects(rule_file: str) -> list[dict]: with open(rule_file) as file: - data = yaml.safe_load(file) + data = yaml().load(file) if isinstance(data, list): return data return [data] @@ -53,7 +54,7 @@ def parse_rules(self, rule_file: str) -> None: try: rule_data_list = parse_yaml_objects(rule_file) - except yaml.YAMLError as e: + except YAMLError as e: self.errors.append(f"Invalid YAML file {rule_file!r}: {e}") return diff --git a/src/retasc/yaml.py b/src/retasc/yaml.py new file mode 100644 index 0000000..97bb327 --- /dev/null +++ b/src/retasc/yaml.py @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +from functools import cache + +from ruamel.yaml import YAML + + +def yaml_str_representer(dumper, data): + """Display nicer multi-line strings in YAML.""" + style = "|" if "\n" in data else None + return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=style) + + +@cache +def yaml() -> YAML: + yaml = YAML(typ="safe") + yaml.default_flow_style = False + yaml.indent(sequence=4, offset=2) + yaml.representer.add_representer(str, yaml_str_representer) + return yaml diff --git a/tests/conftest.py b/tests/conftest.py index 8be3620..70f0407 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,10 +2,10 @@ from datetime import date from unittest.mock import ANY, patch -import yaml from pytest import fixture from retasc.product_pages_api import ProductPagesScheduleTask +from retasc.yaml import yaml @fixture @@ -30,7 +30,7 @@ def rule_path(tmp_path): @fixture def valid_rule_file(rule_path, rule_dict): file = rule_path / "rule.yaml" - file.write_text(yaml.dump(rule_dict, sort_keys=False)) + yaml().dump(rule_dict, file) yield str(file) @@ -38,7 +38,7 @@ def valid_rule_file(rule_path, rule_dict): def invalid_rule_file(rule_path, rule_dict): del rule_dict["version"] file = rule_path / "rule.yaml" - file.write_text(yaml.dump(rule_dict, sort_keys=False)) + yaml().dump(rule_dict, file) yield str(file) diff --git a/tests/test_generate_schema.py b/tests/test_generate_schema.py index 485e9be..3e109a0 100644 --- a/tests/test_generate_schema.py +++ b/tests/test_generate_schema.py @@ -1,10 +1,9 @@ # SPDX-License-Identifier: GPL-3.0-or-later import json -import yaml - from retasc.models.generate_schema import generate_schema from retasc.models.rule import Rule +from retasc.yaml import yaml def test_generate_json_schema(tmp_path): @@ -32,7 +31,7 @@ def test_generate_yaml_schema(tmp_path): schema_file = tmp_path / "rules_schema.yaml" generate_schema(output_path=schema_file, output_json=False) with open(schema_file) as f: - schema = yaml.safe_load(f) + schema = yaml().load(f) expected_schema = Rule.model_json_schema() assert schema == expected_schema diff --git a/tests/test_parse_rules.py b/tests/test_parse_rules.py index 1113743..ff99991 100644 --- a/tests/test_parse_rules.py +++ b/tests/test_parse_rules.py @@ -1,11 +1,11 @@ # SPDX-License-Identifier: GPL-3.0-or-later import re -import yaml from pytest import fixture, mark, raises from retasc.models.config import parse_config from retasc.models.parse_rules import RuleParsingError, parse_rules +from retasc.yaml import yaml JIRA_TEMPLATES = [ "main.yaml", @@ -66,12 +66,12 @@ def templates_root(tmp_path, monkeypatch): def create_jira_templates(path): for template in JIRA_TEMPLATES: file = path / template - file.write_text(yaml.dump({}, sort_keys=False)) + yaml().dump({}, file) def create_dependent_rules(rule_path): file = rule_path / "other_rules.yml" - file.write_text(yaml.dump(DEPENDENT_RULES_DATA, sort_keys=False)) + yaml().dump(DEPENDENT_RULES_DATA, file) def test_parse_no_rules(rule_path): @@ -86,7 +86,7 @@ def test_parse_rule_valid_simple(rule_path): def test_parse_rule_valid(templates_root, rule_path): file = rule_path / "rule.yaml" - file.write_text(yaml.dump(RULE_DATA)) + yaml().dump(RULE_DATA, file) create_dependent_rules(rule_path) create_jira_templates(templates_root) call_parse_rules(str(rule_path)) @@ -99,7 +99,7 @@ def test_parse_rule_invalid(invalid_rule_file): def test_parse_rule_missing_dependent_rules(templates_root, rule_path): file = rule_path / "rule.yaml" - file.write_text(yaml.dump(RULE_DATA)) + yaml().dump(RULE_DATA, file) create_jira_templates(templates_root) expected_error = re.escape( f"Invalid rule 'Example Rule' (file {str(file)!r}):" @@ -112,7 +112,7 @@ def test_parse_rule_missing_dependent_rules(templates_root, rule_path): def test_parse_rule_missing_jira_templates(rule_path, templates_root): file = rule_path / "rule.yaml" - file.write_text(yaml.dump(RULE_DATA, sort_keys=False)) + yaml().dump(RULE_DATA, file) create_dependent_rules(rule_path) expected_error = ( re.escape( @@ -143,7 +143,7 @@ def test_parse_rule_duplicate_jira_ids(rule_path, templates_root): ], } rules_data = [RULE_DATA, rule2] - file.write_text(yaml.dump(rules_data)) + yaml().dump(rules_data, file) create_dependent_rules(rule_path) create_jira_templates(templates_root) expected_error = re.escape( @@ -159,7 +159,7 @@ def test_parse_rule_duplicate_name(rule_path, rule_dict): rule_dict["name"] = "DUPLICATE" for filename in ("rule1.yml", "rule2.yml"): file = rule_path / filename - file.write_text(yaml.dump(rule_dict)) + yaml().dump(rule_dict, file) expected_error = ( "Duplicate rule name 'DUPLICATE' in files: '[^']*/rule1.yml', '[^']*/rule2.yml'"