Skip to content

Commit

Permalink
Support rendering changelog as MarkDown files (#139)
Browse files Browse the repository at this point in the history
* Add basic MarkDown writer.

* Implement document rendering and use it for changelog generation.

* Avoid code duplication.

* Make pylint shut up about some more duplications.

* Allow multiple output formats, and improve MarkDown output.

* Add changelog fragment.

* Add some fixes and improvements.

* Add rendering tests.

* Improve error/warnings handling.

* Improve rendering and code coverage.

* Extend tests.

* Deprecate some internal code.

The code is used by antsibull, and potentially also other tools.

* Improve generated MD.

* Support tables in MD by rendering them as HTML.

* Fix symbol name.
  • Loading branch information
felixfontein committed Feb 9, 2024
1 parent 4d8e445 commit b1a9862
Show file tree
Hide file tree
Showing 28 changed files with 3,332 additions and 124 deletions.
434 changes: 434 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions CHANGELOG.md.license
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
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-FileCopyrightText: Ansible Project
SPDX-License-Identifier: GPL-3.0-or-later
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
5 changes: 4 additions & 1 deletion changelogs/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# 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

changelog_filename_template: ../CHANGELOG.rst
changelog_filename_template: ../CHANGELOG
changelog_filename_version_depth: 0
changes_file: changelog.yaml
changes_format: combined
Expand Down Expand Up @@ -33,3 +33,6 @@ sections:
- Known Issues
use_semantic_versioning: true
title: Ansible Changelog Tool
output_formats:
- rst
- md
6 changes: 6 additions & 0 deletions changelogs/fragments/139-output-formats.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
minor_changes:
- "Allow to render changelogs as MarkDown. The output formats written can be controlled with the ``output_formats`` option in the config file (https://github.com/ansible-community/antsibull-changelog/pull/139)."
deprecated_features:
- "Some code in ``antsibull_changelog.changelog_entry`` has been deprecated, and the ``antsibull_changelog.rst`` module has been deprecated completely.
If you use them in your own code, please take a look at the `PR deprecating them <https://github.com/ansible-community/antsibull-changelog/pull/139>`__
for information on how to stop using them (https://github.com/ansible-community/antsibull-changelog/pull/139)."
8 changes: 8 additions & 0 deletions docs/changelog-configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,14 @@ The default value is ``false`` for existing configurations, and ``true`` for new
When set to ``true``, uses FQCN (Fully Qualified Collection Names) when mentioning new plugins and modules. This means that `namespace.name.` is prepended to the plugin resp. module names.


``output_format`` (list of strings)
-----------------------------------

The default is ``["rst"]``.

A list of output formats to write the changelog as. Supported formats are ``rst`` for ReStructuredText and ``md`` for MarkDown.


Deprecated options
==================

Expand Down
9 changes: 8 additions & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,14 @@ def formatters(session: nox.Session):
def codeqa(session: nox.Session):
install(session, ".[codeqa]", editable=True)
session.run("flake8", "src/antsibull_changelog", *session.posargs)
session.run("pylint", "--rcfile", ".pylintrc.automated", "src/antsibull_changelog")
session.run(
"pylint",
"--rcfile",
".pylintrc.automated",
"src/antsibull_changelog",
"--ignore-imports",
"yes",
)
session.run("reuse", "lint")
session.run("antsibull-changelog", "lint")

Expand Down
174 changes: 121 additions & 53 deletions src/antsibull_changelog/changelog_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@

from __future__ import annotations

import abc
import collections
import os
from collections.abc import MutableMapping
from typing import Any, cast

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

version: str
text_format: TextFormat

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

def __init__(self, version: str):
self.version = version
self.text_format = TextFormat.RESTRUCTURED_TEXT
self.modules = []
self.plugins = {}
self.objects = {}
Expand Down Expand Up @@ -72,6 +75,8 @@ def empty(self) -> bool:
def add_section_content(self, builder: RstBuilder, section_name: str) -> None:
"""
Add a section's content of fragments to the changelog.
This function is DEPRECATED! It will be removed in a future version.
"""
if section_name not in self.changes:
return
Expand All @@ -85,13 +90,23 @@ def add_section_content(self, builder: RstBuilder, section_name: str) -> None:
builder.add_raw_rst(content)


class ChangelogGenerator:
def get_entry_config(
release_entries: MutableMapping[str, ChangelogEntry], entry_version: str
) -> ChangelogEntry:
"""
Generate changelog as reStructuredText.
Create (if not existing) and return release entry for a given version.
"""
if entry_version not in release_entries:
release_entries[entry_version] = ChangelogEntry(entry_version)

This class can be both used to create a full changelog, or to append a
changelog to an existing RstBuilder. This is for example useful to create
a combined ACD changelog.
return release_entries[entry_version]


class ChangelogGeneratorBase(abc.ABC):
"""
Abstract base class for changelog generators.
Provides some useful helpers.
"""

config: ChangelogConfig
Expand All @@ -103,6 +118,7 @@ def __init__( # pylint: disable=too-many-arguments
self,
config: ChangelogConfig,
changes: ChangesBase,
/,
plugins: list[PluginDescription] | None = None,
fragments: list[ChangelogFragment] | None = None,
flatmap: bool = True,
Expand All @@ -118,18 +134,6 @@ def __init__( # pylint: disable=too-many-arguments
self.object_resolver = changes.get_object_resolver()
self.fragment_resolver = changes.get_fragment_resolver(fragments)

@staticmethod
def _get_entry_config(
release_entries: MutableMapping[str, ChangelogEntry], entry_version: str
) -> ChangelogEntry:
"""
Create (if not existing) and return release entry for a given version.
"""
if entry_version not in release_entries:
release_entries[entry_version] = ChangelogEntry(entry_version)

return release_entries[entry_version]

def _update_modules_plugins_objects(
self, entry_config: ChangelogEntry, release: dict
) -> None:
Expand Down Expand Up @@ -217,11 +221,95 @@ def collect(
until_version=until_version,
squash=squash,
):
entry_config = self._get_entry_config(release_entries, entry_version)
entry_config = get_entry_config(release_entries, entry_version)
self._collect_entry(entry_config, entry_version, versions)

return list(release_entries.values())

def get_fqcn_prefix(self) -> str | None:
"""
Returns the FQCN prefix (collection name) for plugins/modules.
"""
fqcn_prefix = None
if self.config.use_fqcn:
if self.config.paths.is_collection:
fqcn_prefix = "%s.%s" % (
self.config.collection_details.get_namespace(),
self.config.collection_details.get_name(),
)
else:
fqcn_prefix = "ansible.builtin"
return fqcn_prefix

def get_title(self) -> str:
"""
Return changelog's title.
"""
latest_version = self.changes.latest_version
codename = self.changes.releases[latest_version].get("codename")
major_minor_version = ".".join(
latest_version.split(".")[: self.config.changelog_filename_version_depth]
)

title = self.config.title or "Ansible"
if major_minor_version:
title = "%s %s" % (title, major_minor_version)
if codename:
title = '%s "%s"' % (title, codename)
return "%s Release Notes" % (title,)


def get_plugin_name(
name: str,
/,
fqcn_prefix: str | None = None,
namespace: str | None = None,
flatmap: bool = False,
) -> str:
"""
Given a module or plugin name, prepends FQCN prefix (collection name) and/or namespace,
if appropriate.
"""
if not flatmap and namespace:
name = "%s.%s" % (namespace, name)
if fqcn_prefix:
name = "%s.%s" % (fqcn_prefix, name)
return name


class ChangelogGenerator(ChangelogGeneratorBase):
# antsibull_changelog.rendering.changelog.ChangelogGenerator is a modified
# copy of this class adjust to the document rendering framework in
# antsibull_changelog.rendering. To avoid pylint complaining too much, we
# disable 'duplicate-code' for this class.

# pylint: disable=duplicate-code

"""
Generate changelog as reStructuredText.
This class can be both used to create a full changelog, or to append a
changelog to an existing RstBuilder. This is for example useful to create
a combined ACD changelog.
This class is DEPRECATED! It will be removed in a future version.
"""

def __init__( # pylint: disable=too-many-arguments
self,
config: ChangelogConfig,
changes: ChangesBase,
plugins: list[PluginDescription] | None = None,
fragments: list[ChangelogFragment] | None = None,
flatmap: bool = True,
):
"""
Create a changelog generator.
"""
super().__init__(
config, changes, plugins=plugins, fragments=fragments, flatmap=flatmap
)

def append_changelog_entry(
self,
builder: RstBuilder,
Expand All @@ -242,16 +330,7 @@ def append_changelog_entry(
builder, changelog_entry, section_name, start_level=start_level
)

fqcn_prefix = None
if self.config.use_fqcn:
if self.config.paths.is_collection:
fqcn_prefix = "%s.%s" % (
self.config.collection_details.get_namespace(),
self.config.collection_details.get_name(),
)
else:
fqcn_prefix = "ansible.builtin"

fqcn_prefix = self.get_fqcn_prefix()
self._add_plugins(
builder,
changelog_entry.plugins,
Expand Down Expand Up @@ -308,21 +387,10 @@ def generate(self, only_latest: bool = False) -> str:
"""
Generate the changelog as reStructuredText.
"""
latest_version = self.changes.latest_version
codename = self.changes.releases[latest_version].get("codename")
major_minor_version = ".".join(
latest_version.split(".")[: self.config.changelog_filename_version_depth]
)

builder = RstBuilder()

if not only_latest:
title = self.config.title or "Ansible"
if major_minor_version:
title = "%s %s" % (title, major_minor_version)
if codename:
title = '%s "%s"' % (title, codename)
builder.set_title("%s Release Notes" % (title,))
builder.set_title(self.get_title())
builder.add_raw_rst(".. contents:: Topics\n")

if self.changes.ancestor and self.config.mention_ancestor:
Expand Down Expand Up @@ -396,9 +464,7 @@ def add_plugins(
Add new plugins of one type to the changelog.
"""
for plugin in sorted(plugins, key=lambda plugin: plugin["name"]):
plugin_name = plugin["name"]
if fqcn_prefix:
plugin_name = "%s.%s" % (fqcn_prefix, plugin_name)
plugin_name = get_plugin_name(plugin["name"], fqcn_prefix=fqcn_prefix)
builder.add_raw_rst("- %s - %s" % (plugin_name, plugin["description"]))

@staticmethod
Expand Down Expand Up @@ -452,12 +518,12 @@ def add_modules(
builder.add_section(subsection, level + 1)

for module in modules_by_namespace[namespace]:
module_name = module["name"]
if not flatmap and namespace:
module_name = "%s.%s" % (namespace, module_name)
if fqcn_prefix:
module_name = "%s.%s" % (fqcn_prefix, module_name)

module_name = get_plugin_name(
module["name"],
fqcn_prefix=fqcn_prefix,
namespace=namespace,
flatmap=flatmap,
)
builder.add_raw_rst("- %s - %s" % (module_name, module["description"]))

builder.add_raw_rst("")
Expand Down Expand Up @@ -496,9 +562,9 @@ def add_objects(
for ansible_object in sorted(
objects, key=lambda ansible_object: ansible_object["name"]
):
object_name = ansible_object["name"]
if fqcn_prefix:
object_name = "%s.%s" % (fqcn_prefix, object_name)
object_name = get_plugin_name(
ansible_object["name"], fqcn_prefix=fqcn_prefix
)
builder.add_raw_rst(
"- %s - %s" % (object_name, ansible_object["description"])
)
Expand All @@ -517,6 +583,8 @@ def generate_changelog( # pylint: disable=too-many-arguments
"""
Generate the changelog as reStructuredText.
This function is DEPRECATED! It will be removed in a future version.
:arg plugins: Will be loaded if necessary. Only provide when you already have them
:arg fragments: Will be loaded if necessary. Only provide when you already have them
:arg flatmap: Whether the collection uses flatmapping or not
Expand Down

0 comments on commit b1a9862

Please sign in to comment.