aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/lv2_write_news.py
diff options
context:
space:
mode:
authorDavid Robillard <d@drobilla.net>2022-09-06 19:48:58 -0400
committerDavid Robillard <d@drobilla.net>2022-09-06 21:35:21 -0400
commit4b661ad6f1607afc30df5461756df6940ebd524e (patch)
tree5b934cc7bf189ca088dca9bb8f9abeeda62b9c7b /scripts/lv2_write_news.py
parent453eae1c459af3e6284f4bfcfc65673932e38fb6 (diff)
downloadlv2-4b661ad6f1607afc30df5461756df6940ebd524e.tar.xz
Replace change history data with a plain text NEWS file
Diffstat (limited to 'scripts/lv2_write_news.py')
-rwxr-xr-xscripts/lv2_write_news.py258
1 files changed, 0 insertions, 258 deletions
diff --git a/scripts/lv2_write_news.py b/scripts/lv2_write_news.py
deleted file mode 100755
index 6ce935c..0000000
--- a/scripts/lv2_write_news.py
+++ /dev/null
@@ -1,258 +0,0 @@
-#!/usr/bin/env python3
-
-# Copyright 2020-2022 David Robillard <d@drobilla.net>
-# SPDX-License-Identifier: ISC
-
-"""
-Write a NEWS file from RDF data.
-
-The output is in Debian changelog format, which can be parsed by
-dpkg-parsechangelog, among other things.
-"""
-
-import argparse
-import os
-import sys
-import datetime
-import textwrap
-import urllib
-import re
-
-import rdflib
-
-doap = rdflib.Namespace("http://usefulinc.com/ns/doap#")
-dcs = rdflib.Namespace("http://ontologi.es/doap-changeset#")
-rdfs = rdflib.Namespace("http://www.w3.org/2000/01/rdf-schema#")
-foaf = rdflib.Namespace("http://xmlns.com/foaf/0.1/")
-rdf = rdflib.Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
-
-
-def _is_release_version(version):
- "Return true if `version` is a stable version number."
-
- if len(version) not in [2, 3] or version[0] == 0:
- return False
-
- minor = version[len(version) - 2]
- micro = version[len(version) - 1]
-
- return micro % 2 == 0 and (len(version) == 2 or minor % 2 == 0)
-
-
-def _parse_datetime(string):
- "Parse string as either a datetime or a date."
-
- try:
- return datetime.datetime.strptime(string, "%Y-%m-%dT%H:%M:%S%z")
- except ValueError:
- return datetime.datetime.strptime(string, "%Y-%m-%d")
-
-
-def _release_entry(graph, release):
- "Return a news entry for a release."
-
- revision = graph.value(release, doap.revision, None)
- date = graph.value(release, doap.created, None)
- blamee = graph.value(release, dcs.blame, None)
- changeset = graph.value(release, dcs.changeset, None)
- dist = graph.value(release, doap["file-release"], None)
-
- if not revision or not date or not blamee or not changeset:
- return None
-
- version = tuple(map(int, revision.split(".")))
-
- entry = {
- "version": version,
- "revision": str(revision),
- "date": _parse_datetime(date),
- "status": "stable" if _is_release_version(version) else "unstable",
- "items": [],
- }
-
- if dist is not None:
- entry["dist"] = dist
-
- for j in graph.triples([changeset, dcs.item, None]):
- item = str(graph.value(j[2], rdfs.label, None))
- entry["items"] += [item]
-
- entry["blamee_name"] = str(graph.value(blamee, foaf.name, None))
- entry["blamee_mbox"] = str(graph.value(blamee, foaf.mbox, None))
- return entry
-
-
-def _project_entries(graph, project):
- "Return a map from version to news entries for a project"
-
- entries = {}
- for link in graph.triples([project, doap.release, None]):
- entry = _release_entry(graph, link[2])
- if entry is not None:
- entries[entry["version"]] = entry
- else:
- sys.stderr.write(f"warning: Ignored partial {project} release\n")
-
- return entries
-
-
-def _read_turtle_news(in_files):
- "Read news entries from Turtle."
-
- graph = rdflib.Graph()
-
- # Parse input files
- for i in in_files:
- graph.parse(i)
-
- # Read news for every project in the data
- projects = {t[0] for t in graph.triples([None, rdf.type, doap.Project])}
- entries_by_project = {}
- for project in projects:
- # Load any associated files
- for uri in graph.triples([project, rdfs.seeAlso, None]):
- if uri[2].endswith(".ttl"):
- graph.parse(uri[2])
-
- # Use the symbol from the URI as a name, or failing that, the doap:name
- name = os.path.basename(urllib.parse.urlparse(str(project)).path)
- if not name:
- name = graph.value(project, doap.name, None)
-
- entries = _project_entries(graph, project)
- for _, entry in entries.items():
- entry["name"] = name
-
- entries_by_project[str(project)] = entries
-
- return entries_by_project
-
-
-def _write_news_item(out, item):
- "Write a single item (change) in NEWS format."
-
- out.write("\n * " + "\n ".join(textwrap.wrap(item, width=74)))
-
-
-def _write_news_entry(out, entry):
- "Write an entry (version) to out in NEWS format."
-
- # Summary header
- summary = f'{entry["name"]} ({entry["revision"]}) {entry["status"]}'
- out.write(f"{summary}; urgency=medium\n")
-
- # Individual change items
- for item in sorted(entry["items"]):
- _write_news_item(out, item)
-
- # Trailer line
- mbox = entry["blamee_mbox"].replace("mailto:", "")
- author = f'{entry["blamee_name"]} <{mbox}>'
- date = entry["date"]
- if date.tzinfo is None: # Assume UTC (dpkg-parsechangelog requires it)
- date = date.strftime("%a, %d %b %Y %H:%M:%S +0000")
- else:
- date = date.strftime("%a, %d %b %Y %H:%M:%S %z")
-
- out.write(f"\n\n -- {author} {date}\n")
-
-
-def _write_single_project_news(out, entries):
- "Write a NEWS file for entries of a single project to out."
-
- revisions = sorted(entries.keys(), reverse=True)
- for revision in revisions:
- entry = entries[revision]
- out.write("\n" if revision != revisions[0] else "")
- _write_news_entry(out, entry)
-
-
-def _write_meta_project_news(out, top_project, entries_by_project):
- "Write a NEWS file for a meta-project that contains others."
-
- top_name = os.path.basename(urllib.parse.urlparse(str(top_project)).path)
- release_pattern = rf".*/{top_name}-([0-9\.]*).tar.bz2"
-
- # Pop the entries for the top project
- top_entries = entries_by_project.pop(top_project)
-
- # Add items from the other projects to the corresponding top entry
- for _, entries in entries_by_project.items():
- for version, entry in entries.items():
- if "dist" in entry:
- match = re.match(release_pattern, entry["dist"])
- if match:
- version = tuple(map(int, match.group(1).split(".")))
- for item in entry["items"]:
- top_entries[version]["items"] += [
- f'{entry["name"]}: {item}'
- ]
-
- for version in sorted(top_entries.keys(), reverse=True):
- out.write("\n" if version != max(top_entries.keys()) else "")
- _write_news_entry(out, top_entries[version])
-
-
-def _write_text_news(out, entries_by_project, top_project=None):
- "Write NEWS in standard Debian changelog format."
-
- if len(entries_by_project) > 1:
- if top_project is None:
- sys.stderr.write("error: --top is required for multi-projects\n")
- return 1
-
- _write_meta_project_news(out, top_project, entries_by_project)
- else:
- project = next(iter(entries_by_project))
- _write_single_project_news(out, entries_by_project[project])
-
- return 0
-
-
-if __name__ == "__main__":
- ap = argparse.ArgumentParser(
- usage="%(prog)s [OPTION]... DATA_FILE...",
- description=__doc__,
- formatter_class=argparse.RawDescriptionHelpFormatter,
- )
-
- ap.add_argument(
- "-o",
- "--output",
- metavar="OUTPUT_FILE",
- help="output file path",
- )
-
- ap.add_argument(
- "-t",
- "--top-project",
- metavar="OUTPUT_FILE",
- help="URI of parent meta-project with file releases",
- )
-
- ap.add_argument(
- "DATA_FILE",
- nargs="+",
- help="path to a Turtle file with release data",
- )
-
- args = ap.parse_args(sys.argv[1:])
-
- if not args.output and "MESON_DIST_ROOT" in os.environ:
- args.output = os.path.join(os.getenv("MESON_DIST_ROOT"), "NEWS")
-
- if not args.output:
- sys.exit(
- _write_text_news(
- sys.stdout, _read_turtle_news(args.DATA_FILE), args.top_project
- )
- )
- else:
- with open(args.output, "w", encoding="utf-8") as output_file:
- sys.exit(
- _write_text_news(
- output_file,
- _read_turtle_news(args.DATA_FILE),
- args.top_project,
- )
- )