Skip to content

Commit

Permalink
Allow to handle extra data while loading, saving, and linting (#181)
Browse files Browse the repository at this point in the history
* Allow to load and store extra data.

* Allow to remove extra things before linting.
  • Loading branch information
felixfontein authored Oct 19, 2024
1 parent c3b3b27 commit 5f6ad77
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 27 deletions.
3 changes: 3 additions & 0 deletions changelogs/fragments/181-extra-data.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
minor_changes:
- "Python API: allow to extract extra data when loading changelog files, and allow to insert extra data when saving (https://github.com/ansible-community/antsibull-changelog/pull/181)."
- "Python API: allow to preprocess changelog.yaml before linting (https://github.com/ansible-community/antsibull-changelog/pull/181)."
51 changes: 41 additions & 10 deletions src/antsibull_changelog/changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,22 @@ class ChangesData: # pylint: disable=too-many-public-methods
ancestor: str | None

def __init__(
self, config: ChangelogConfig, path: str, data_override: dict | None = None
self,
config: ChangelogConfig,
path: str,
data_override: dict | None = None,
*,
extra_data_extractor: Callable[[dict], None] | None = None,
):
"""
Create modern change metadata.
:arg path: The path of the changelog file. If exists and ``data_override`` is not
provided, the file's contents will be read. An empty string will always be
treated as a non-existing file.
:arg data_override: Allows to load data from dictionary instead from disk
:kwarg extra_data_extractor: Callback that will obtain the raw data before sanitization.
Can be used to extract extra data and store it somewhere else for later use.
"""
self.config = config
self.path = path
Expand All @@ -77,7 +87,9 @@ def __init__(
self.known_objects = set()
self.ancestor = None
self.config = config
self.load(data_override=data_override)
self.load(
data_override=data_override, extra_data_extractor=extra_data_extractor
)

@staticmethod
def empty() -> dict:
Expand All @@ -89,18 +101,28 @@ def empty() -> dict:
"releases": {},
}

def load(self, data_override: dict | None = None) -> None:
def load(
self,
*,
data_override: dict | None = None,
extra_data_extractor: Callable[[dict], None] | None = None,
) -> None:
"""
Load the change metadata from disk.
:arg data_override: If provided, will use this as loaded data instead of reading self.path
:kwarg data_override: If provided, will use this as loaded data instead of reading self.path
:kwarg extra_data_extractor: Callback that will obtain the raw data before sanitization.
Can be used to extract extra data and store it somewhere else for later use.
"""
if data_override is not None:
self.data = sanitize_changes(data_override, config=self.config)
elif os.path.exists(self.path):
self.data = sanitize_changes(load_yaml_file(self.path), config=self.config)
data = data_override
elif self.path and os.path.exists(self.path):
data = load_yaml_file(self.path)
else:
self.data = self.empty()
data = self.empty()
if extra_data_extractor:
extra_data_extractor(data)
self.data = sanitize_changes(data, config=self.config)
self.ancestor = self.data.get("ancestor")

for _, config in self.releases.items():
Expand All @@ -123,16 +145,25 @@ def load(self, data_override: dict | None = None) -> None:

self.known_fragments |= set(config.get("fragments", []))

def save(self) -> None:
def save(self, *, extra_data: dict | None = None) -> None:
"""
Save the change metadata to disk.
:kwarg extra_data: Additional data to store into the file.
"""
if not self.path:
raise ValueError(
"Cannot save since path was not provided on ChangesData object creation"
)
self.sort()
self.data["ancestor"] = self.ancestor
sort_keys = self.config.changelog_sort == "alphanumerical"
data = self.data.copy()
if extra_data:
data.update(extra_data)
store_yaml_file(
self.path,
self.data,
data,
nice=self.config.changelog_nice_yaml,
sort_keys=sort_keys,
)
Expand Down
70 changes: 53 additions & 17 deletions src/antsibull_changelog/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from __future__ import annotations

import re
from collections.abc import Callable
from typing import Any, cast

import packaging.version
Expand All @@ -33,12 +34,19 @@ class ChangelogYamlLinter:
errors: list[tuple[str, int, int, str]]
path: str

def __init__(self, path: str, no_semantic_versioning: bool = False):
def __init__(
self,
path: str,
*,
no_semantic_versioning: bool = False,
preprocess_data: Callable[[dict], None] | None = None,
):
self.errors = []
self.path = path
self.valid_plugin_types = set(get_documentable_plugins())
self.valid_plugin_types.update(OTHER_PLUGIN_TYPES)
self.no_semantic_versioning = no_semantic_versioning
self.preprocess_data = preprocess_data

def check_version(self, version: Any, message: str) -> Any | None:
"""
Expand Down Expand Up @@ -360,23 +368,10 @@ def lint_releases_entry(
fragment, (str,), ["releases", version_str, "fragments", idx]
)

def lint(self) -> list[tuple[str, int, int, str]]:
def lint_content(self, changelog_yaml: dict) -> None:
"""
Load and lint the changelog.yaml file.
Lint the contents of a changelog.yaml file, provided it is a global mapping.
"""
try:
changelog_yaml = load_yaml_file(self.path)
except Exception as exc: # pylint: disable=broad-except
self.errors.append(
(
self.path,
0,
0,
"error while parsing YAML: {0}".format(exc).replace("\n", " "),
)
)
return self.errors

ancestor_str = changelog_yaml.get("ancestor")
if ancestor_str is not None:
ancestor = self.check_version(ancestor_str, "Invalid ancestor version")
Expand Down Expand Up @@ -407,16 +402,57 @@ def lint(self) -> list[tuple[str, int, int, str]]:
if self.verify_type(entry, (dict,), ["releases", version_str]):
self.lint_releases_entry(fragment_linter, version_str, entry)

def lint(self) -> list[tuple[str, int, int, str]]:
"""
Load and lint the changelog.yaml file.
"""
try:
changelog_yaml = load_yaml_file(self.path)
except Exception as exc: # pylint: disable=broad-except
self.errors.append(
(
self.path,
0,
0,
"error while parsing YAML: {0}".format(exc).replace("\n", " "),
)
)
return self.errors

if not isinstance(changelog_yaml, dict):
self.errors.append(
(
self.path,
0,
0,
"YAML file is not a global mapping",
)
)
return self.errors

if self.preprocess_data:
self.preprocess_data(changelog_yaml)

self.lint_content(changelog_yaml)
return self.errors


def lint_changelog_yaml(
path: str,
*,
no_semantic_versioning: bool = False,
preprocess_data: Callable[[dict], None] | None = None,
) -> list[tuple[str, int, int, str]]:
"""
Lint a changelogs/changelog.yaml file.
:kwarg no_semantic_versioning: Set to ``True`` if the file does not use
semantic versioning, but Python version numbers.
:kwarg preprocess_data: If provided, will be called on the data loaded before
it is checked. This can be used to remove extra data before validation.
"""
return ChangelogYamlLinter(
path, no_semantic_versioning=no_semantic_versioning
path,
no_semantic_versioning=no_semantic_versioning,
preprocess_data=preprocess_data,
).lint()

0 comments on commit 5f6ad77

Please sign in to comment.