aboutsummaryrefslogtreecommitdiffstats
path: root/tools/release_notes/main.py
diff options
context:
space:
mode:
authorShyamnath Premnadh <Shyamnath.Premnadh@qt.io>2024-09-05 10:52:46 +0200
committerShyamnath Premnadh <Shyamnath.Premnadh@qt.io>2024-09-19 10:20:58 +0200
commitc71bbe991fc5faa1327503e06af17fd11d717a76 (patch)
tree5673ee4ba7169678fbfebf4592d04c2cb91e5253 /tools/release_notes/main.py
parentfe5020b7e218078562c6b9b35d005553f53255fb (diff)
Documentation: Release Notes
- Introduces a new section in the navigation pane called "Release Notes" with subpages for PySide6, Shiboken6, PySide2, and Shiboken2. - The .md files are auto-generated by the script `tools/release_notes/main.py` and created in the directory `sources/pyside6/doc/release_notes/`. Fixes: PYSIDE-2853 Change-Id: I6def8b526f11a638581f29798dd6917cd435d19c Reviewed-by: Friedemann Kleint <Friedemann.Kleint@qt.io> Reviewed-by: Cristian Maureira-Fredes <cristian.maureira-fredes@qt.io>
Diffstat (limited to 'tools/release_notes/main.py')
-rw-r--r--tools/release_notes/main.py193
1 files changed, 193 insertions, 0 deletions
diff --git a/tools/release_notes/main.py b/tools/release_notes/main.py
new file mode 100644
index 000000000..b9a86172d
--- /dev/null
+++ b/tools/release_notes/main.py
@@ -0,0 +1,193 @@
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+"""
+This tool reads all the changelogs in doc/changelogs and generates .rst files for each of the
+changelogs. This .rst files are then used to generate the contents of the 'Release Notes' section
+in the navigation pane of the Qt for Python documentation.
+"""
+
+import re
+import logging
+import shutil
+from pathlib import Path
+from argparse import ArgumentParser, RawTextHelpFormatter
+
+SECTION_NAMES = ["PySide6", "Shiboken6", "PySide2", "Shiboken2"]
+DIR = Path(__file__).parent
+OUTPUT_DIR = Path(f"{DIR}/../../sources/pyside6/doc/release_notes").resolve()
+CHANGELOG_DIR = Path(f"{DIR}/../../doc/changelogs").resolve()
+
+BASE_CONTENT = """\
+.. _release_notes:
+
+Release Notes
+=============
+
+This section contains the release notes for different versions of Qt for Python.
+
+.. toctree::
+ :maxdepth: 1
+
+ pyside6_release_notes.md
+ shiboken6_release_notes.md
+ pyside2_release_notes.md
+ shiboken2_release_notes.md
+"""
+
+
+class Changelog:
+ def __init__(self, file_path: Path):
+ self.file_path = file_path
+ self.version = file_path.name.split("-")[-1]
+ self.sections = {section: [] for section in SECTION_NAMES}
+ # for matching lines like * PySide6 * to identify the section
+ self.section_pattern = re.compile(r"\* +(\w+) +\*")
+ # for line that start with ' -' which lists the changes
+ self.line_pattern = re.compile(r"^ -")
+ # for line that contains a bug report like PYSIDE-<bug_number>
+ self.bug_number_pattern = re.compile(r"\[PYSIDE-\d+\]")
+
+ def add_line(self, section, line):
+ self.sections[section].append(line)
+
+ def parsed_sections(self):
+ return self.sections
+
+ def parse(self):
+ current_section = None
+ buffer = []
+
+ with open(self.file_path, 'r', encoding='utf-8') as file:
+ # convert the lines to an iterator for skip the '***' lines
+ lines = iter(file.readlines())
+
+ for line in lines:
+ # skip lines with all characters as '*'
+ if line.strip() == '*' * len(line.strip()):
+ continue
+
+ match = self.section_pattern.match(line)
+ if match:
+ # if buffer has content, add it to the current section
+ if buffer:
+ self.add_line(current_section, ' '.join(buffer).strip())
+ buffer = []
+ current_section = match.group(1)
+ # skip the next line which contains '***'
+ try:
+ next(lines)
+ except StopIteration:
+ break
+ continue
+
+ if current_section:
+ if self.line_pattern.match(line) and buffer:
+ self.add_line(current_section, ' '.join(buffer).strip())
+ buffer = []
+
+ # If the line contains a reference to a bug report like [PYSIDE-<bug_number>]
+ # then insert a link to the reference that conforms with Sphinx syntax
+ bug_number = self.bug_number_pattern.search(line)
+ if bug_number:
+ bug_number = bug_number.group()
+ # remove the square brackets
+ actual_bug_number = bug_number[1:-1]
+ bug_number_replacement = (
+ f"[{actual_bug_number}]"
+ f"(https://bugreports.qt.io/browse/{actual_bug_number})"
+ )
+ line = re.sub(re.escape(bug_number), bug_number_replacement, line)
+
+ # Add the line to the buffer
+ buffer.append(line.strip())
+
+ # Add any remaining content in the buffer to the current section
+ if buffer:
+ self.add_line(current_section, ' '.join(buffer).strip())
+
+
+def parse_changelogs() -> str:
+ '''
+ Parse the changelogs in the CHANGELOG_DIR and return a list of parsed changelogs.
+ '''
+ changelogs = []
+ logging.info(f"[RELEASE_DOC] Processing changelogs in {CHANGELOG_DIR}")
+ for file_path in CHANGELOG_DIR.iterdir():
+ # exclude changes-1.2.3
+ if "changes-1.2.3" in file_path.name:
+ continue
+ logging.info(f"[RELEASE_DOC] Processing file {file_path.name}")
+ changelog = Changelog(file_path)
+ changelog.parse()
+ changelogs.append(changelog)
+ return changelogs
+
+
+def write_md_file(section: str, changelogs: list[Changelog]):
+ '''
+ For each section create a .md file with the following content:
+
+ Section Name
+ ============
+
+ Version
+ -------
+
+ - Change 1
+ - Change 2
+ ....
+ '''
+ file_path = OUTPUT_DIR / f"{section.lower()}_release_notes.md"
+ with open(file_path, 'w', encoding='utf-8') as file:
+ file.write(f"# {section}\n")
+ for changelog in changelogs:
+ section_contents = changelog.parsed_sections()[section]
+ if section_contents:
+ file.write(f"## {changelog.version}\n\n")
+ for lines in section_contents:
+ # separate each line with a newline
+ file.write(f"{lines}\n")
+ file.write("\n")
+
+
+def generate_index_file():
+ """Generate the index RST file."""
+ index_path = OUTPUT_DIR / "index.rst"
+ index_path.write_text(BASE_CONTENT, encoding='utf-8')
+
+
+def main():
+ parser = ArgumentParser(description="Generate release notes from changelog",
+ formatter_class=RawTextHelpFormatter)
+ parser.add_argument("-v", "--verbose", help="run in verbose mode", action="store_const",
+ dest="loglevel", const=logging.INFO)
+ args = parser.parse_args()
+
+ logging.basicConfig(level=args.loglevel)
+
+ # create the output directory if it does not exist
+ # otherwise remove its contents
+ if OUTPUT_DIR.is_dir():
+ shutil.rmtree(OUTPUT_DIR, ignore_errors=True)
+ logging.info(f"[RELEASE_DOC] Removed existing {OUTPUT_DIR}")
+
+ logging.info(f"[RELEASE_DOC] Creating {OUTPUT_DIR}")
+ OUTPUT_DIR.mkdir(exist_ok=True)
+
+ logging.info("[RELEASE_DOC] Generating index.md file")
+ generate_index_file()
+
+ logging.info("[RELEASE_DOC] Parsing changelogs")
+ changelogs = parse_changelogs()
+
+ # sort changelogs by version number in descending order
+ changelogs.sort(key=lambda x: x.version, reverse=True)
+
+ for section in SECTION_NAMES:
+ logging.info(f"[RELEASE_DOC] Generating {section.lower()}_release_notes.md file")
+ write_md_file(section, changelogs)
+
+
+if __name__ == "__main__":
+ main()