GH-121970: Extract ``availability`` into a new extension (#125082)

This commit is contained in:
Adam Turner 2024-10-09 21:50:03 +01:00 committed by GitHub
parent f2cb399470
commit cbfd392479
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 126 additions and 76 deletions

View File

@ -21,6 +21,7 @@ from pyspecific import SOURCE_URI
extensions = [
'audit_events',
'availability',
'c_annotations',
'glossary_search',
'lexers',

View File

@ -0,0 +1,125 @@
"""Support for documenting platform availability"""
from __future__ import annotations
from typing import TYPE_CHECKING
from docutils import nodes
from sphinx import addnodes
from sphinx.util import logging
from sphinx.util.docutils import SphinxDirective
if TYPE_CHECKING:
from sphinx.application import Sphinx
from sphinx.util.typing import ExtensionMetadata
logger = logging.getLogger("availability")
# known platform, libc, and threading implementations
_PLATFORMS = frozenset({
"AIX",
"Android",
"BSD",
"DragonFlyBSD",
"Emscripten",
"FreeBSD",
"GNU/kFreeBSD",
"iOS",
"Linux",
"macOS",
"NetBSD",
"OpenBSD",
"POSIX",
"Solaris",
"Unix",
"VxWorks",
"WASI",
"Windows",
})
_LIBC = frozenset({
"BSD libc",
"glibc",
"musl",
})
_THREADING = frozenset({
# POSIX platforms with pthreads
"pthreads",
})
KNOWN_PLATFORMS = _PLATFORMS | _LIBC | _THREADING
class Availability(SphinxDirective):
has_content = True
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = True
def run(self) -> list[nodes.container]:
title = "Availability"
refnode = addnodes.pending_xref(
title,
nodes.inline(title, title, classes=["xref", "std", "std-ref"]),
refdoc=self.env.docname,
refdomain="std",
refexplicit=True,
reftarget="availability",
reftype="ref",
refwarn=True,
)
sep = nodes.Text(": ")
parsed, msgs = self.state.inline_text(self.arguments[0], self.lineno)
pnode = nodes.paragraph(title, "", refnode, sep, *parsed, *msgs)
self.set_source_info(pnode)
cnode = nodes.container("", pnode, classes=["availability"])
self.set_source_info(cnode)
if self.content:
self.state.nested_parse(self.content, self.content_offset, cnode)
self.parse_platforms()
return [cnode]
def parse_platforms(self) -> dict[str, str | bool]:
"""Parse platform information from arguments
Arguments is a comma-separated string of platforms. A platform may
be prefixed with "not " to indicate that a feature is not available.
Example::
.. availability:: Windows, Linux >= 4.2, not WASI
Arguments like "Linux >= 3.17 with glibc >= 2.27" are currently not
parsed into separate tokens.
"""
platforms = {}
for arg in self.arguments[0].rstrip(".").split(","):
arg = arg.strip()
platform, _, version = arg.partition(" >= ")
if platform.startswith("not "):
version = False
platform = platform.removeprefix("not ")
elif not version:
version = True
platforms[platform] = version
if unknown := set(platforms).difference(KNOWN_PLATFORMS):
logger.warning(
"Unknown platform%s or syntax '%s' in '.. availability:: %s', "
"see %s:KNOWN_PLATFORMS for a set of known platforms.",
"s" if len(platforms) != 1 else "",
" ".join(sorted(unknown)),
self.arguments[0],
__file__,
)
return platforms
def setup(app: Sphinx) -> ExtensionMetadata:
app.add_directive("availability", Availability)
return {
"version": "1.0",
"parallel_read_safe": True,
"parallel_write_safe": True,
}

View File

@ -24,7 +24,6 @@ from sphinx.builders import Builder
from sphinx.domains.changeset import VersionChange, versionlabels, versionlabel_classes
from sphinx.domains.python import PyFunction, PyMethod, PyModule
from sphinx.locale import _ as sphinx_gettext
from sphinx.util import logging
from sphinx.util.docutils import SphinxDirective
from sphinx.writers.text import TextWriter, TextTranslator
from sphinx.util.display import status_iterator
@ -108,80 +107,6 @@ class ImplementationDetail(SphinxDirective):
return [pnode]
# Support for documenting platform availability
class Availability(SphinxDirective):
has_content = True
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = True
# known platform, libc, and threading implementations
known_platforms = frozenset({
"AIX", "Android", "BSD", "DragonFlyBSD", "Emscripten", "FreeBSD",
"GNU/kFreeBSD", "Linux", "NetBSD", "OpenBSD", "POSIX", "Solaris",
"Unix", "VxWorks", "WASI", "Windows", "macOS", "iOS",
# libc
"BSD libc", "glibc", "musl",
# POSIX platforms with pthreads
"pthreads",
})
def run(self):
availability_ref = ':ref:`Availability <availability>`: '
avail_nodes, avail_msgs = self.state.inline_text(
availability_ref + self.arguments[0],
self.lineno)
pnode = nodes.paragraph(availability_ref + self.arguments[0],
'', *avail_nodes, *avail_msgs)
self.set_source_info(pnode)
cnode = nodes.container("", pnode, classes=["availability"])
self.set_source_info(cnode)
if self.content:
self.state.nested_parse(self.content, self.content_offset, cnode)
self.parse_platforms()
return [cnode]
def parse_platforms(self):
"""Parse platform information from arguments
Arguments is a comma-separated string of platforms. A platform may
be prefixed with "not " to indicate that a feature is not available.
Example::
.. availability:: Windows, Linux >= 4.2, not WASI
Arguments like "Linux >= 3.17 with glibc >= 2.27" are currently not
parsed into separate tokens.
"""
platforms = {}
for arg in self.arguments[0].rstrip(".").split(","):
arg = arg.strip()
platform, _, version = arg.partition(" >= ")
if platform.startswith("not "):
version = False
platform = platform[4:]
elif not version:
version = True
platforms[platform] = version
unknown = set(platforms).difference(self.known_platforms)
if unknown:
cls = type(self)
logger = logging.getLogger(cls.__qualname__)
logger.warning(
f"Unknown platform(s) or syntax '{' '.join(sorted(unknown))}' "
f"in '.. availability:: {self.arguments[0]}', see "
f"{__file__}:{cls.__qualname__}.known_platforms for a set "
"known platforms."
)
return platforms
# Support for documenting decorators
class PyDecoratorMixin(object):
@ -492,7 +417,6 @@ def setup(app):
app.add_role('issue', issue_role)
app.add_role('gh', gh_issue_role)
app.add_directive('impl-detail', ImplementationDetail)
app.add_directive('availability', Availability)
app.add_directive('versionadded', PyVersionChange, override=True)
app.add_directive('versionchanged', PyVersionChange, override=True)
app.add_directive('versionremoved', PyVersionChange, override=True)