Skip to content

Commit

Permalink
Improve schema format
Browse files Browse the repository at this point in the history
Generates multi-line strings which are easier to read.

Replaces pyyaml with ruamel.yaml.
  • Loading branch information
hluk committed Dec 4, 2024
1 parent 2beaa16 commit 83f1cf2
Show file tree
Hide file tree
Showing 11 changed files with 72 additions and 97 deletions.
2 changes: 0 additions & 2 deletions examples/jira/notify_team.yaml.j2
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
{% include 'common.yaml.j2' %}
# override project from 'common.yaml.j2'
project: {key: TEAM}
summary: Notify Team
81 changes: 17 additions & 64 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 4 additions & 3 deletions src/retasc/models/config.py
Original file line number Diff line number Diff line change
@@ -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)")
Expand All @@ -16,7 +17,7 @@ class Config(BaseModel):
jira_label_templates: list[str] = Field(
description=(
"Label templates for the managed issues in Jira."
' Example: ["retasc-managed", "retasc-managed-{{ release }}"]'
'\nExample: ["retasc-managed", "retasc-managed-{{ release }}"]'
)
)
jira_label_prefix: str = Field(
Expand All @@ -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)
19 changes: 11 additions & 8 deletions src/retasc/models/generate_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
7 changes: 4 additions & 3 deletions src/retasc/models/parse_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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]
Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions src/retasc/models/prerequisites/jira_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
from collections.abc import Iterator
from itertools import takewhile

import yaml
from pydantic import BaseModel, Field

from retasc.models.release_rule_state import ReleaseRuleState
from retasc.utils import to_comma_separated
from retasc.yaml import yaml

from .base import PrerequisiteBase

Expand Down Expand Up @@ -116,7 +116,7 @@ def _update_issue(
template_content = f.read()

content = context.template.render(template_content)
template_data = yaml.safe_load(content)
template_data = yaml().load(content)
fields = _template_to_issue_data(template_data, context, template)

if issue:
Expand Down
20 changes: 20 additions & 0 deletions src/retasc/yaml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 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.sort_base_mapping_type_on_output = False
yaml.default_flow_style = False
yaml.indent(sequence=4, offset=2)
yaml.representer.add_representer(str, yaml_str_representer)
return yaml
6 changes: 3 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,15 +30,15 @@ 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)


@fixture
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)


Expand Down
5 changes: 2 additions & 3 deletions tests/test_generate_schema.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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
16 changes: 8 additions & 8 deletions tests/test_parse_rules.py
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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):
Expand All @@ -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))
Expand All @@ -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}):"
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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'"
Expand Down

0 comments on commit 83f1cf2

Please sign in to comment.