Skip to content

Commit

Permalink
Implement document rendering and use it for changelog generation.
Browse files Browse the repository at this point in the history
  • Loading branch information
felixfontein committed Feb 2, 2024
1 parent 9a02887 commit 62eca25
Show file tree
Hide file tree
Showing 12 changed files with 1,400 additions and 32 deletions.
1 change: 0 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ Ansible Changelog Tool Release Notes

.. contents:: Topics


v0.23.0
=======

Expand Down
4 changes: 3 additions & 1 deletion src/antsibull_changelog/changelog_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from .changes import ChangesBase, FragmentResolver, PluginResolver
from .config import ChangelogConfig, PathsConfig
from .fragment import ChangelogFragment
from .fragment import ChangelogFragment, FragmentFormat
from .logger import LOGGER
from .plugins import PluginDescription
from .rst import RstBuilder
Expand All @@ -31,6 +31,7 @@ class ChangelogEntry:
"""

version: str
text_format: FragmentFormat

modules: list[Any]
plugins: dict[Any, Any]
Expand All @@ -40,6 +41,7 @@ class ChangelogEntry:

def __init__(self, version: str):
self.version = version
self.text_format = FragmentFormat.RESTRUCTURED_TEXT
self.modules = []
self.plugins = {}
self.objects = {}
Expand Down
26 changes: 21 additions & 5 deletions src/antsibull_changelog/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,19 @@
HAS_ARGCOMPLETE = False

from .ansible import get_ansible_release
from .changelog_generator import generate_changelog
from .changes import ChangesBase, add_release, load_changes
from .config import ChangelogConfig, CollectionDetails, PathsConfig
from .errors import ChangelogError
from .fragment import ChangelogFragment, ChangelogFragmentLinter, load_fragments
from .fragment import (
ChangelogFragment,
ChangelogFragmentLinter,
FragmentFormat,
load_fragments,
)
from .lint import lint_changelog_yaml
from .logger import LOGGER, setup_logger
from .plugins import PluginDescription, load_plugins
from .rendering.changelog import generate_changelog
from .toml import has_toml_loader_available, load_toml


Expand Down Expand Up @@ -670,7 +675,16 @@ def command_release(args: Any) -> int:
prev_version=prev_version,
objects=cast(list[PluginDescription], plugins),
)
generate_changelog(paths, config, changes, plugins, fragments, flatmap=flatmap)
document_format = FragmentFormat.RESTRUCTURED_TEXT
generate_changelog(
paths,
config,
changes,
document_format,
plugins=plugins,
fragments=fragments,
flatmap=flatmap,
)

return 0

Expand Down Expand Up @@ -716,12 +730,14 @@ def command_generate(args: Any) -> int:
version=changes.latest_version,
force_reload=args.reload_plugins,
)
document_format = FragmentFormat.RESTRUCTURED_TEXT
generate_changelog(
paths,
config,
changes,
plugins,
fragments,
document_format,
plugins=plugins,
fragments=fragments,
flatmap=flatmap,
changelog_path=output,
only_latest=only_latest,
Expand Down
48 changes: 41 additions & 7 deletions src/antsibull_changelog/fragment.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from __future__ import annotations

import enum
import os
from typing import Any

Expand All @@ -22,6 +23,15 @@
from .yaml import load_yaml


class FragmentFormat(enum.Enum):
"""
Supported fragment formats.
"""

RESTRUCTURED_TEXT = "restructuredtext"
MARKDOWN = "markdown"


class ChangelogFragment:
"""
A changelog fragment.
Expand All @@ -30,14 +40,21 @@ class ChangelogFragment:
content: dict[str, list[str] | str]
path: str
name: str
fragment_format: FragmentFormat

def __init__(self, content: dict[str, list[str] | str], path: str):
def __init__(
self,
content: dict[str, list[str] | str],
path: str,
fragment_format: FragmentFormat = FragmentFormat.RESTRUCTURED_TEXT,
):
"""
Create changelog fragment.
"""
self.content = content
self.path = path
self.name = os.path.basename(path)
self.fragment_format = fragment_format

def remove(self) -> None:
"""
Expand Down Expand Up @@ -68,21 +85,25 @@ def move_to(self, directory: str) -> None:
)

@staticmethod
def load(path: str) -> "ChangelogFragment":
def load(
path: str, fragment_format: FragmentFormat = FragmentFormat.RESTRUCTURED_TEXT
) -> "ChangelogFragment":
"""
Load a ``ChangelogFragment`` from a file.
"""
content = load_yaml(path)
return ChangelogFragment(content, path)
return ChangelogFragment(content, path, fragment_format=fragment_format)

@staticmethod
def from_dict(
data: dict[str, list[str] | str], path: str = ""
data: dict[str, list[str] | str],
path: str = "",
fragment_format: FragmentFormat = FragmentFormat.RESTRUCTURED_TEXT,
) -> "ChangelogFragment":
"""
Create a ``ChangelogFragment`` from a dictionary.
"""
return ChangelogFragment(data, path)
return ChangelogFragment(data, path, fragment_format=fragment_format)

@staticmethod
def combine(
Expand Down Expand Up @@ -326,6 +347,15 @@ def _lint_section(
):
errors.append((fragment.path, 0, 0, "invalid section: %s" % section))

@staticmethod
def _check_content(
text: str, text_format: FragmentFormat, filename: str
) -> list[str]:
if text_format == FragmentFormat.RESTRUCTURED_TEXT:
results = check_rst_content(text, filename=filename)
return [result[2] for result in results]
raise ValueError("No validation possible for MarkDown fragments")

@staticmethod
def _lint_lines(
errors: list[tuple[str, int, int, str]],
Expand All @@ -352,10 +382,14 @@ def _lint_lines(
)
continue

results = check_rst_content(line, filename=fragment.path)
results = ChangelogFragmentLinter._check_content(
line, fragment.fragment_format, fragment.path
)
errors += [(fragment.path, 0, 0, result[2]) for result in results]
elif isinstance(lines, str):
results = check_rst_content(lines, filename=fragment.path)
results = ChangelogFragmentLinter._check_content(
lines, fragment.fragment_format, fragment.path
)
errors += [(fragment.path, 0, 0, result[2]) for result in results]

def lint(self, fragment: ChangelogFragment) -> list[tuple[str, int, int, str]]:
Expand Down
177 changes: 177 additions & 0 deletions src/antsibull_changelog/rendering/_document.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# Author: Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
# https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2024, Ansible Project

"""
Common code for rendering a document.
"""

from __future__ import annotations

import abc

from ..fragment import FragmentFormat
from .document import AbstractRenderer, DocumentRenderer


class BaseContent(abc.ABC):
"""
Abstract content object.
"""

closed: bool

def __init__(self, already_closed=False):
self.closed = already_closed

def generate(self) -> None:
"""
Generate data for this content (if dynamic).
"""

@abc.abstractmethod
def append_lines(self, lines: list[str], start_level: int = 0) -> None:
"""
Append the lines for this content.
"""


class DocumentRendererEx(DocumentRenderer):
"""
Abstract extended document renderer
"""

title: str | None

def __init__(self):
self.title = None

@abc.abstractmethod
def render_text(self, text: str, text_format: FragmentFormat) -> str:
"""
Render a text as ReStructured Text.
"""

def set_title(self, title: str) -> None:
if self.title is not None:
raise ValueError("Document title already set")
self.title = title


class TextRenderer(BaseContent):
"""
Render text.
"""

text: str
text_format: FragmentFormat
root: DocumentRendererEx
indent_first: str
indent_next: str

# pylint: disable-next=too-many-arguments
def __init__(
self,
text: str,
text_format: FragmentFormat,
/,
root: DocumentRendererEx,
indent_first: str = "",
indent_next: str = "",
):
super().__init__(already_closed=True)
self.text = text
self.text_format = text_format
self.root = root
self.indent_first = indent_first
self.indent_next = indent_next

def append_lines(self, lines: list[str], start_level: int = 0) -> None:
text = self.root.render_text(self.text, self.text_format)
indent = self.indent_first
for line in text.splitlines():
if not line and indent != self.indent_first:
lines.append("")
else:
lines.append(f"{indent}{line}")
indent = self.indent_next


class AbstractRendererEx(BaseContent, AbstractRenderer):
"""
Abstract RST renderer.
"""

content: list[BaseContent]
root: DocumentRendererEx
_fragment_ident: str

def __init__(self, root: DocumentRendererEx, fragment_ident: str):
super().__init__()
self.content = []
self.root = root
self._fragment_ident = fragment_ident

def _check_content_closed(self) -> None:
for content in self.content:
if not content.closed:
raise ValueError(f"Content {content} is not closed")

@abc.abstractmethod
def _get_level(self) -> int:
pass

def _generate_all(self) -> None:
self.generate()
for content in self.content:
content.generate()

def add_text(self, text: str, text_format: FragmentFormat) -> None:
if self.closed:
raise ValueError("{self} is already closed")
self.content.append(TextRenderer(text, text_format, root=self.root))

def add_fragment(self, text: str, text_format: FragmentFormat) -> None:
if self.closed:
raise ValueError("{self} is already closed")
self.content.append(
TextRenderer(
text,
text_format,
root=self.root,
indent_first=self._fragment_ident,
indent_next=" ",
)
)


def render_document(
document_renderer: DocumentRendererEx, abstract_renderer: AbstractRendererEx
) -> str:
"""
Renders the document to a string.
:arg document_renderer: View of the document as an extended document renderer.
:arg document_renderer: View of the document as an extended abstract renderer.
"""
# Check
abstract_renderer._check_content_closed() # pylint: disable=protected-access
if document_renderer.title is None:
raise ValueError("Document title not set")

# Make sure everything is generated
abstract_renderer.generate()
for content in abstract_renderer.content:
content.generate()

# Generate lines
lines: list[str] = []
abstract_renderer.append_lines(lines)

# Return lines
return "\n".join(lines) + "\n" # add trailing newline


__all__ = ("BaseContent", "DocumentRendererEx", "TextRenderer", "AbstractRendererEx")

0 comments on commit 62eca25

Please sign in to comment.