• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Support for documenting platform availability"""
2
3from __future__ import annotations
4
5from typing import TYPE_CHECKING
6
7from docutils import nodes
8from sphinx import addnodes
9from sphinx.util import logging
10from sphinx.util.docutils import SphinxDirective
11
12if TYPE_CHECKING:
13    from sphinx.application import Sphinx
14    from sphinx.util.typing import ExtensionMetadata
15
16logger = logging.getLogger("availability")
17
18# known platform, libc, and threading implementations
19_PLATFORMS = frozenset({
20    "AIX",
21    "Android",
22    "BSD",
23    "DragonFlyBSD",
24    "Emscripten",
25    "FreeBSD",
26    "GNU/kFreeBSD",
27    "iOS",
28    "Linux",
29    "macOS",
30    "NetBSD",
31    "OpenBSD",
32    "POSIX",
33    "Solaris",
34    "Unix",
35    "VxWorks",
36    "WASI",
37    "Windows",
38})
39_LIBC = frozenset({
40    "BSD libc",
41    "glibc",
42    "musl",
43})
44_THREADING = frozenset({
45    # POSIX platforms with pthreads
46    "pthreads",
47})
48KNOWN_PLATFORMS = _PLATFORMS | _LIBC | _THREADING
49
50
51class Availability(SphinxDirective):
52    has_content = True
53    required_arguments = 1
54    optional_arguments = 0
55    final_argument_whitespace = True
56
57    def run(self) -> list[nodes.container]:
58        title = "Availability"
59        refnode = addnodes.pending_xref(
60            title,
61            nodes.inline(title, title, classes=["xref", "std", "std-ref"]),
62            refdoc=self.env.docname,
63            refdomain="std",
64            refexplicit=True,
65            reftarget="availability",
66            reftype="ref",
67            refwarn=True,
68        )
69        sep = nodes.Text(": ")
70        parsed, msgs = self.state.inline_text(self.arguments[0], self.lineno)
71        pnode = nodes.paragraph(title, "", refnode, sep, *parsed, *msgs)
72        self.set_source_info(pnode)
73        cnode = nodes.container("", pnode, classes=["availability"])
74        self.set_source_info(cnode)
75        if self.content:
76            self.state.nested_parse(self.content, self.content_offset, cnode)
77        self.parse_platforms()
78
79        return [cnode]
80
81    def parse_platforms(self) -> dict[str, str | bool]:
82        """Parse platform information from arguments
83
84        Arguments is a comma-separated string of platforms. A platform may
85        be prefixed with "not " to indicate that a feature is not available.
86
87        Example::
88
89           .. availability:: Windows, Linux >= 4.2, not WASI
90
91        Arguments like "Linux >= 3.17 with glibc >= 2.27" are currently not
92        parsed into separate tokens.
93        """
94        platforms = {}
95        for arg in self.arguments[0].rstrip(".").split(","):
96            arg = arg.strip()
97            platform, _, version = arg.partition(" >= ")
98            if platform.startswith("not "):
99                version = False
100                platform = platform.removeprefix("not ")
101            elif not version:
102                version = True
103            platforms[platform] = version
104
105        if unknown := set(platforms).difference(KNOWN_PLATFORMS):
106            logger.warning(
107                "Unknown platform%s or syntax '%s' in '.. availability:: %s', "
108                "see %s:KNOWN_PLATFORMS for a set of known platforms.",
109                "s" if len(platforms) != 1 else "",
110                " ".join(sorted(unknown)),
111                self.arguments[0],
112                __file__,
113            )
114
115        return platforms
116
117
118def setup(app: Sphinx) -> ExtensionMetadata:
119    app.add_directive("availability", Availability)
120
121    return {
122        "version": "1.0",
123        "parallel_read_safe": True,
124        "parallel_write_safe": True,
125    }
126