diff options
Diffstat (limited to 'tools/changes.py')
| -rw-r--r-- | tools/changes.py | 65 |
1 files changed, 65 insertions, 0 deletions
diff --git a/tools/changes.py b/tools/changes.py new file mode 100644 index 0000000..0c3e0bf --- /dev/null +++ b/tools/changes.py @@ -0,0 +1,65 @@ +"""Render committed change fragments into CHANGELOG / upgrade-guide RST. + +Stdlib only, no external changelog tooling. Reads every ``changes/*.rst`` +fragment (INI with a single ``[change]`` section), sorts them by filename, and +renders each into an RST section. On ``--release`` the rendered blocks are +slotted into the target files and the consumed fragments are deleted. +""" + +from __future__ import annotations + +import configparser +import sys +from pathlib import Path + +_REPO_ROOT = Path(__file__).resolve().parent.parent +_CHANGES_DIR = _REPO_ROOT / "changes" + + +def collect_fragments(changes_dir: Path) -> list[Path]: + """Return the fragment files under ``changes_dir`` in fragment-id order.""" + fragments = [path for path in changes_dir.glob("*.rst") if path.name != "README.rst"] + return sorted(fragments, key=lambda path: path.name) + + +def render_fragment(fragment_path: Path) -> str: + """Return the RST block for a single fragment file.""" + parser = configparser.ConfigParser() + parser.read(fragment_path, encoding="utf-8") + change = parser["change"] + anchor = change["anchor"] + heading = change["heading"] + context = _normalize_paragraph(change["context"]) + lines = [f".. _{anchor}:", "", heading, "-" * len(heading), "", context, ""] + before = change.get("before") + after = change.get("after") + if before is not None and after is not None: + lines.extend(["", ".. code-block:: text", "", f" {before}", f" {after}", ""]) + return "\n".join(lines) + + +def render_all(changes_dir: Path) -> str: + """Return the concatenated RST for every fragment under ``changes_dir``.""" + fragment_paths = collect_fragments(changes_dir) + rendered_blocks = [render_fragment(path) for path in fragment_paths] + return "\n".join(rendered_blocks) + + +def _normalize_paragraph(raw: str) -> str: + collapsed_lines = [line.strip() for line in raw.splitlines()] + non_empty = [line for line in collapsed_lines if line] + return " ".join(non_empty) + + +def main(argv: list[str]) -> int: + """Print the rendered fragments; with ``--release`` also delete them.""" + rendered = render_all(_CHANGES_DIR) + sys.stdout.write(rendered) + if "--release" in argv: + for fragment_path in collect_fragments(_CHANGES_DIR): + fragment_path.unlink() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) |
