summaryrefslogtreecommitdiffstats
path: root/sphinx/builders/changes.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/builders/changes.py')
-rw-r--r--sphinx/builders/changes.py161
1 files changed, 161 insertions, 0 deletions
diff --git a/sphinx/builders/changes.py b/sphinx/builders/changes.py
new file mode 100644
index 0000000..3e24e7d
--- /dev/null
+++ b/sphinx/builders/changes.py
@@ -0,0 +1,161 @@
+"""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 # type: ignore[attr-defined]
+from sphinx.util.fileutil import copy_asset_file
+from sphinx.util.osutil import ensuredir, os_path
+
+if TYPE_CHECKING:
+ from sphinx.application import Sphinx
+
+logger = logging.getLogger(__name__)
+
+
+class ChangesBuilder(Builder):
+ """
+ Write a summary with all versionadded/changed 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',
+ }
+
+ 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]
+
+ 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'):
+ text = text.replace(f'.. {directive}:: {version}',
+ f'<b>.. {directive}:: {version}</b>')
+ return text
+
+ def finish(self) -> None:
+ pass
+
+
+def setup(app: Sphinx) -> dict[str, Any]:
+ app.add_builder(ChangesBuilder)
+
+ return {
+ 'version': 'builtin',
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }