-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e9406f0
commit a3907ed
Showing
7 changed files
with
355 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,3 +25,4 @@ repos: | |
- pytest | ||
- types-docutils | ||
- legacy-api-wrap | ||
- myst-parser |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
"""A release notes directive. | ||
Given a list of version files matching :attr:`FULL_VERSION_RE`, | ||
render them using the following (where ``.`` is the directory they are in): | ||
.. code:: restructuredtext | ||
.. release-notes:: . | ||
With e.g. the files `1.2.0.md`, `1.2.1.md`, `1.3.0.rst`, and `1.3.2.rst`, | ||
this will render like the following: | ||
.. code:: restructuredtext | ||
_v1.3: | ||
Version 1.3 | ||
=========== | ||
.. include:: 1.3.2.rst | ||
.. include:: 1.3.0.rst | ||
_v1.2: | ||
Version 1.2 | ||
=========== | ||
.. include:: 1.2.1.md | ||
.. include:: 1.2.0.md | ||
""" | ||
|
||
from __future__ import annotations | ||
|
||
import re | ||
import itertools | ||
from typing import TYPE_CHECKING | ||
from pathlib import Path | ||
from dataclasses import dataclass | ||
|
||
from docutils import nodes | ||
from packaging.version import Version | ||
from sphinx.util.parsing import nested_parse_to_nodes | ||
from sphinx.util.docutils import SphinxDirective | ||
|
||
from . import metadata, _setup_sig | ||
|
||
|
||
if TYPE_CHECKING: | ||
from typing import Any, ClassVar | ||
from collections.abc import Iterable, Sequence | ||
|
||
from sphinx.application import Sphinx | ||
from myst_parser.mdit_to_docutils.base import DocutilsRenderer | ||
|
||
|
||
FULL_VERSION_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)(?:\..*)?$") | ||
"""Regex matching a full version number including patch part, maybe with more after.""" | ||
|
||
|
||
@dataclass | ||
class _Backend: | ||
dir: Path | ||
instance: SphinxDirective | ||
|
||
def run(self) -> Sequence[nodes.Node]: | ||
versions = sorted( | ||
( | ||
(Version(f.stem), f) | ||
for f in self.dir.iterdir() | ||
if FULL_VERSION_RE.match(f.stem) | ||
), | ||
reverse=True, # descending | ||
) | ||
version_groups = itertools.groupby( | ||
versions, key=lambda vf: (vf[0].major, vf[0].minor) | ||
) | ||
return [ | ||
node | ||
for (major, minor), versions in version_groups | ||
for node in self.render_version_group(major, minor, versions) | ||
] | ||
|
||
def render_version_group( | ||
self, | ||
major: int, | ||
minor: int, | ||
versions: Iterable[tuple[Version, Path]] = (), | ||
) -> tuple[nodes.target, nodes.section]: | ||
target = nodes.target( | ||
ids=[f"v{major}-{minor}"], | ||
names=[f"v{major}.{minor}"], | ||
) | ||
section = nodes.section( | ||
"", | ||
nodes.title("", f"Version {major}.{minor}"), | ||
ids=[], | ||
names=[f"version {major}.{minor}"], | ||
) | ||
|
||
self.instance.state.document.note_implicit_target(section) | ||
self.instance.state.document.note_explicit_target(target) | ||
|
||
for _, p in versions: | ||
section += self.render_include(p) | ||
return target, section | ||
|
||
def render_include(self, path: Path) -> Sequence[nodes.Node]: | ||
return nested_parse_to_nodes( | ||
self.instance.state, | ||
path.read_text(), | ||
source=str(path), | ||
offset=self.instance.content_offset, | ||
) | ||
|
||
|
||
# TODO(flying-sheep): Remove once MyST-Parser bug is fixed | ||
# https://github.com/executablebooks/MyST-Parser/issues/967 | ||
class _BackendMyst(_Backend): | ||
def run(self) -> Sequence[nodes.Node]: | ||
super().run() | ||
return [] | ||
|
||
def render_version_group( | ||
self, major: int, minor: int, versions: Iterable[tuple[Version, Path]] = () | ||
) -> tuple[nodes.target, nodes.section]: | ||
target, section = super().render_version_group(major, minor) | ||
# append target and section to parent | ||
self._myst_renderer.current_node.append(target) | ||
self._myst_renderer.update_section_level_state(section, 2) | ||
# append children to section | ||
with self._myst_renderer.current_node_context(section): | ||
for _, p in versions: | ||
self.render_include(p) | ||
return target, section # ignored, just to not change the types | ||
|
||
def render_include(self, path: Path) -> Sequence[nodes.Node]: | ||
from myst_parser.mocking import MockIncludeDirective | ||
from docutils.parsers.rst.directives.misc import Include | ||
|
||
srcfile, lineno = self.instance.get_source_info() | ||
parent_dir = Path(srcfile).parent | ||
|
||
d = MockIncludeDirective( | ||
renderer=self._myst_renderer, | ||
name=type(self).__name__, | ||
klass=Include, # type: ignore[arg-type] # wrong type hint | ||
arguments=[str(path.relative_to(parent_dir))], | ||
options={}, | ||
body=[], | ||
lineno=lineno, | ||
) | ||
return d.run() | ||
|
||
@property | ||
def _myst_renderer(self) -> DocutilsRenderer: | ||
rv: DocutilsRenderer = self.instance.state._renderer # type: ignore[attr-defined] # noqa: SLF001 | ||
return rv | ||
|
||
|
||
class ReleaseNotes(SphinxDirective): | ||
"""Directive rendering release notes, grouping them by minor versions.""" | ||
|
||
required_arguments: ClassVar = 1 | ||
|
||
def run(self) -> Sequence[nodes.Node]: | ||
"""Read the release notes and render them.""" | ||
dir_ = Path(self.arguments[0]) | ||
# resolve relative dir | ||
if not dir_.is_absolute(): | ||
src_file = Path(self.get_source_info()[0]) | ||
if not src_file.is_file(): | ||
msg = f"Cannot find relative path to: {src_file}" | ||
raise self.error(msg) | ||
dir_ = src_file.parent / self.arguments[0] | ||
if not dir_.is_dir(): | ||
msg = f"Not a directory: {dir_}" | ||
raise self.error(msg) | ||
|
||
cls = _BackendMyst if hasattr(self.state, "_renderer") else _Backend | ||
return cls(dir_, self).run() | ||
|
||
|
||
@_setup_sig | ||
def setup(app: Sphinx) -> dict[str, Any]: | ||
"""Add the `release-notes` directive.""" | ||
app.add_directive("release-notes", ReleaseNotes) | ||
return metadata |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.