You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
167 lines
6.5 KiB
167 lines
6.5 KiB
9 months ago
|
"""Changelog builder."""
|
||
|
|
||
|
from __future__ import annotations
|
||
|
|
||
|
import html
|
||
|
from os import path
|
||
|
from typing import TYPE_CHECKING, Any, cast
|
||
|
|
||
|
from sphinx import package_dir
|
||
|
from sphinx.builders import Builder
|
||
|
from sphinx.domains.changeset import ChangeSetDomain
|
||
|
from sphinx.locale import _, __
|
||
|
from sphinx.theming import HTMLThemeFactory
|
||
|
from sphinx.util import logging
|
||
|
from sphinx.util.console import bold
|
||
|
from sphinx.util.fileutil import copy_asset_file
|
||
|
from sphinx.util.osutil import ensuredir, os_path
|
||
|
|
||
|
if TYPE_CHECKING:
|
||
|
from sphinx.application import Sphinx
|
||
|
from sphinx.util.typing import ExtensionMetadata
|
||
|
|
||
|
logger = logging.getLogger(__name__)
|
||
|
|
||
|
|
||
|
class ChangesBuilder(Builder):
|
||
|
"""
|
||
|
Write a summary with all versionadded/changed/deprecated/removed directives.
|
||
|
"""
|
||
|
|
||
|
name = 'changes'
|
||
|
epilog = __('The overview file is in %(outdir)s.')
|
||
|
|
||
|
def init(self) -> None:
|
||
|
self.create_template_bridge()
|
||
|
theme_factory = HTMLThemeFactory(self.app)
|
||
|
self.theme = theme_factory.create('default')
|
||
|
self.templates.init(self, self.theme)
|
||
|
|
||
|
def get_outdated_docs(self) -> str:
|
||
|
return str(self.outdir)
|
||
|
|
||
|
typemap = {
|
||
|
'versionadded': 'added',
|
||
|
'versionchanged': 'changed',
|
||
|
'deprecated': 'deprecated',
|
||
|
'versionremoved': 'removed',
|
||
|
}
|
||
|
|
||
|
def write(self, *ignored: Any) -> None:
|
||
|
version = self.config.version
|
||
|
domain = cast(ChangeSetDomain, self.env.get_domain('changeset'))
|
||
|
libchanges: dict[str, list[tuple[str, str, int]]] = {}
|
||
|
apichanges: list[tuple[str, str, int]] = []
|
||
|
otherchanges: dict[tuple[str, str], list[tuple[str, str, int]]] = {}
|
||
|
|
||
|
changesets = domain.get_changesets_for(version)
|
||
|
if not changesets:
|
||
|
logger.info(bold(__('no changes in version %s.') % version))
|
||
|
return
|
||
|
logger.info(bold(__('writing summary file...')))
|
||
|
for changeset in changesets:
|
||
|
if isinstance(changeset.descname, tuple):
|
||
|
descname = changeset.descname[0]
|
||
|
else:
|
||
|
descname = changeset.descname
|
||
|
ttext = self.typemap[changeset.type]
|
||
|
context = changeset.content.replace('\n', ' ')
|
||
|
if descname and changeset.docname.startswith('c-api'):
|
||
|
if context:
|
||
|
entry = f'<b>{descname}</b>: <i>{ttext}:</i> {context}'
|
||
|
else:
|
||
|
entry = f'<b>{descname}</b>: <i>{ttext}</i>.'
|
||
|
apichanges.append((entry, changeset.docname, changeset.lineno))
|
||
|
elif descname or changeset.module:
|
||
|
module = changeset.module or _('Builtins')
|
||
|
if not descname:
|
||
|
descname = _('Module level')
|
||
|
if context:
|
||
|
entry = f'<b>{descname}</b>: <i>{ttext}:</i> {context}'
|
||
|
else:
|
||
|
entry = f'<b>{descname}</b>: <i>{ttext}</i>.'
|
||
|
libchanges.setdefault(module, []).append((entry, changeset.docname,
|
||
|
changeset.lineno))
|
||
|
else:
|
||
|
if not context:
|
||
|
continue
|
||
|
entry = f'<i>{ttext.capitalize()}:</i> {context}'
|
||
|
title = self.env.titles[changeset.docname].astext()
|
||
|
otherchanges.setdefault((changeset.docname, title), []).append(
|
||
|
(entry, changeset.docname, changeset.lineno))
|
||
|
|
||
|
ctx = {
|
||
|
'project': self.config.project,
|
||
|
'version': version,
|
||
|
'docstitle': self.config.html_title,
|
||
|
'shorttitle': self.config.html_short_title,
|
||
|
'libchanges': sorted(libchanges.items()),
|
||
|
'apichanges': sorted(apichanges),
|
||
|
'otherchanges': sorted(otherchanges.items()),
|
||
|
'show_copyright': self.config.html_show_copyright,
|
||
|
'show_sphinx': self.config.html_show_sphinx,
|
||
|
}
|
||
|
with open(path.join(self.outdir, 'index.html'), 'w', encoding='utf8') as f:
|
||
|
f.write(self.templates.render('changes/frameset.html', ctx))
|
||
|
with open(path.join(self.outdir, 'changes.html'), 'w', encoding='utf8') as f:
|
||
|
f.write(self.templates.render('changes/versionchanges.html', ctx))
|
||
|
|
||
|
hltext = ['.. versionadded:: %s' % version,
|
||
|
'.. versionchanged:: %s' % version,
|
||
|
'.. deprecated:: %s' % version,
|
||
|
'.. versionremoved:: %s' % version,
|
||
|
]
|
||
|
|
||
|
def hl(no: int, line: str) -> str:
|
||
|
line = '<a name="L%s"> </a>' % no + html.escape(line)
|
||
|
for x in hltext:
|
||
|
if x in line:
|
||
|
line = '<span class="hl">%s</span>' % line
|
||
|
break
|
||
|
return line
|
||
|
|
||
|
logger.info(bold(__('copying source files...')))
|
||
|
for docname in self.env.all_docs:
|
||
|
with open(self.env.doc2path(docname),
|
||
|
encoding=self.env.config.source_encoding) as f:
|
||
|
try:
|
||
|
lines = f.readlines()
|
||
|
except UnicodeDecodeError:
|
||
|
logger.warning(__('could not read %r for changelog creation'), docname)
|
||
|
continue
|
||
|
targetfn = path.join(self.outdir, 'rst', os_path(docname)) + '.html'
|
||
|
ensuredir(path.dirname(targetfn))
|
||
|
with open(targetfn, 'w', encoding='utf-8') as f:
|
||
|
text = ''.join(hl(i + 1, line) for (i, line) in enumerate(lines))
|
||
|
ctx = {
|
||
|
'filename': self.env.doc2path(docname, False),
|
||
|
'text': text,
|
||
|
}
|
||
|
f.write(self.templates.render('changes/rstsource.html', ctx))
|
||
|
themectx = {'theme_' + key: val for (key, val) in
|
||
|
self.theme.get_options({}).items()}
|
||
|
copy_asset_file(path.join(package_dir, 'themes', 'default', 'static', 'default.css_t'),
|
||
|
self.outdir, context=themectx, renderer=self.templates)
|
||
|
copy_asset_file(path.join(package_dir, 'themes', 'basic', 'static', 'basic.css'),
|
||
|
self.outdir)
|
||
|
|
||
|
def hl(self, text: str, version: str) -> str:
|
||
|
text = html.escape(text)
|
||
|
for directive in ('versionchanged', 'versionadded', 'deprecated', 'versionremoved'):
|
||
|
text = text.replace(f'.. {directive}:: {version}',
|
||
|
f'<b>.. {directive}:: {version}</b>')
|
||
|
return text
|
||
|
|
||
|
def finish(self) -> None:
|
||
|
pass
|
||
|
|
||
|
|
||
|
def setup(app: Sphinx) -> ExtensionMetadata:
|
||
|
app.add_builder(ChangesBuilder)
|
||
|
|
||
|
return {
|
||
|
'version': 'builtin',
|
||
|
'parallel_read_safe': True,
|
||
|
'parallel_write_safe': True,
|
||
|
}
|