#!/usr/bin/env python3
#
# peps.py
"""
Sphinx extension which modifies the :rst:role:`sphinx:pep` role to use normal (i.e. not bold) text for custom titles.
Also adds the :rst:role:`pep621` role for referencing sections within :pep:`621`,
and the :rst:role:`core-meta` role for referencing sections in Python's core metadata`.
"""
# Based on https://github.com/sphinx-doc/sphinx/blob/3.x/sphinx/roles.py
#
# Copyright (c) 2007-2021 by the Sphinx team.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# stdlib
import re
from typing import Any, Dict, List, Optional, Tuple
# 3rd party
from docutils import nodes
from docutils.nodes import Node, system_message, unescape
from docutils.parsers.rst.states import Inliner
from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.locale import _
from sphinx.util.docutils import ReferenceRole, SphinxRole
__all__ = ["PEP", "PEP621Section", "CoreMetadata", "setup"]
[docs]class PEP(ReferenceRole):
"""
Sphinx role for referencing a PEP or a section thereof.
"""
title: str
target: str
has_explicit_title: bool
[docs] def run(self) -> Tuple[List[Node], List[system_message]]:
"""
Process the role.
"""
assert self.inliner is not None
target_id = f"index-{self.env.new_serialno('index')}"
entries = [("single", _("Python Enhancement Proposals; PEP %s") % self.target, target_id, '', None)]
index = addnodes.index(entries=entries)
target = nodes.target('', '', ids=[target_id])
self.inliner.document.note_explicit_target(target) # type: ignore[attr-defined]
try:
refuri = self.build_uri()
reference = nodes.reference('', '', internal=False, refuri=refuri, classes=["pep"])
if self.has_explicit_title:
reference += nodes.inline(self.title, self.title)
else:
title = f"PEP {self.title}"
reference += nodes.strong(title, title)
except ValueError:
msg = self.inliner.reporter.error( # type: ignore[attr-defined]
f"invalid PEP number {self.target}",
line=self.lineno,
)
prb = self.inliner.problematic(self.rawtext, self.rawtext, msg) # type: ignore[attr-defined]
return [prb], [msg]
return [index, target, reference], []
[docs] def build_uri(self) -> str:
"""
Constrict the target URI for the reference node.
"""
assert self.inliner is not None
base_url: str = self.inliner.document.settings.pep_base_url # type: ignore[attr-defined]
if base_url == "https://www.python.org/dev/peps/": # pragma: no cover
# Update URL
base_url = "https://peps.python.org/"
ret = self.target.split('#', 1)
if len(ret) == 2:
return f"{base_url}pep-{int(ret[0]):04d}#{ret[1]}"
else:
return f"{base_url}pep-{int(ret[0]):04d}"
[docs]class PEP621Section(PEP):
"""
Sphinx role for referencing a section within :pep:`621`.
"""
[docs] def __call__( # noqa: 117
self,
name: str,
rawtext: str,
text: str,
lineno: int,
inliner: Inliner,
options: Dict = {},
content: List[str] = []
) -> Tuple[List[Node], List[system_message]]:
matched = self.explicit_title_re.match(text)
if matched:
self.has_explicit_title = True
self.title = unescape(matched.group(1))
self.target = f"621#{unescape(matched.group(2))}"
else:
self.has_explicit_title = True
self.title = unescape(text)
self.target = f"621#{unescape(text)}"
if self.target in {
f"621#dependencies",
f"621#optional-dependencies",
f"621#dependencies/optional-dependencies",
}:
self.target = f"621#dependencies-optional-dependencies"
elif self.target in {f"621#authors", f"621#maintainers", f"621#authors/maintainers"}:
self.target = f"621#authors-maintainers"
return SphinxRole.__call__(self, name, rawtext, text, lineno, inliner, options, content)
[docs]def setup(app: Sphinx) -> Dict[str, Any]:
"""
Setup :mod:`sphinx_packaging.peps`.
:param app: The Sphinx application.
"""
# this package
from sphinx_packaging import __version__
app.add_role("pep", PEP(), override=True)
app.add_role("pep621", PEP621Section())
app.add_role("core-meta", CoreMetadata())
return {
"version": __version__,
"parallel_read_safe": True,
"parallel_write_safe": True,
}