"""Changelog builder."""
from __future__ import annotations
import html
from pathlib import Path
from typing import TYPE_CHECKING
from sphinx import package_dir
from sphinx._cli.util.colour import bold
from sphinx.builders import Builder
from sphinx.locale import _, __
from sphinx.theming import HTMLThemeFactory
from sphinx.util import logging
from sphinx.util.fileutil import copy_asset_file
if TYPE_CHECKING:
from collections.abc import Set
from sphinx.application import Sphinx
from sphinx.util.typing import ExtensionMetadata
logger = logging.getLogger(__name__)
[documentos]
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_documents(self, _docnames: Set[str]) -> None:
version = self.config.version
domain = self.env.domains.changeset_domain
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:
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(self.outdir / 'index.html', 'w', encoding='utf8') as f:
f.write(self.templates.render('changes/frameset.html', ctx))
with open(self.outdir / 'changes.html', 'w', encoding='utf8') as f:
f.write(self.templates.render('changes/versionchanges.html', ctx))
hltext = [
f'.. versionadded:: {version}',
f'.. versionchanged:: {version}',
f'.. deprecated:: {version}',
f'.. versionremoved:: {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.config.source_encoding
) as f:
try:
lines = f.readlines()
except UnicodeDecodeError:
logger.warning(
__('could not read %r for changelog creation'), docname
)
continue
text = ''.join(hl(i + 1, line) for (i, line) in enumerate(lines))
ctx = {
'filename': str(self.env.doc2path(docname, False)),
'text': text,
}
rendered = self.templates.render('changes/rstsource.html', ctx)
targetfn = self.outdir / 'rst' / f'{docname}.html'
targetfn.parent.mkdir(parents=True, exist_ok=True)
with open(targetfn, 'w', encoding='utf-8') as f:
f.write(rendered)
themectx = {
'theme_' + key: val for (key, val) in self.theme.get_options({}).items()
}
copy_asset_file(
Path(package_dir, 'themes', 'default', 'static', 'default.css.jinja'),
self.outdir,
context=themectx,
renderer=self.templates,
force=True,
)
copy_asset_file(
Path(package_dir, 'themes', 'basic', 'static', 'basic.css'),
self.outdir / 'basic.css',
force=True,
)
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,
}