1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
|
# 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
DEFAULT_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], output_dir: Path):
'''
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(output_dir: Path):
"""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)
parser.add_argument("--target", "-t", help="Directory to output the generated files",
type=Path, default=DEFAULT_OUTPUT_DIR)
args = parser.parse_args()
logging.basicConfig(level=args.loglevel)
output_dir = args.target.resolve()
# 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(output_dir)
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, output_dir)
if __name__ == "__main__":
main()
|