sphinx-mintlify-output 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sphinx_mintlify_output/__init__.py +31 -0
- sphinx_mintlify_output/assets.py +109 -0
- sphinx_mintlify_output/autodoc.py +350 -0
- sphinx_mintlify_output/builder.py +231 -0
- sphinx_mintlify_output/components.py +166 -0
- sphinx_mintlify_output/escaping.py +150 -0
- sphinx_mintlify_output/frontmatter.py +98 -0
- sphinx_mintlify_output/labels.py +21 -0
- sphinx_mintlify_output/navigation.py +160 -0
- sphinx_mintlify_output/nodes/__init__.py +166 -0
- sphinx_mintlify_output/nodes/admonitions.py +103 -0
- sphinx_mintlify_output/nodes/autodoc.py +558 -0
- sphinx_mintlify_output/nodes/base.py +196 -0
- sphinx_mintlify_output/nodes/block.py +91 -0
- sphinx_mintlify_output/nodes/definitions.py +55 -0
- sphinx_mintlify_output/nodes/footnotes.py +71 -0
- sphinx_mintlify_output/nodes/images.py +116 -0
- sphinx_mintlify_output/nodes/inline.py +75 -0
- sphinx_mintlify_output/nodes/links.py +41 -0
- sphinx_mintlify_output/nodes/lists.py +61 -0
- sphinx_mintlify_output/nodes/math.py +19 -0
- sphinx_mintlify_output/nodes/raw.py +44 -0
- sphinx_mintlify_output/nodes/sphinx_design.py +128 -0
- sphinx_mintlify_output/nodes/sphinx_inline_tabs.py +93 -0
- sphinx_mintlify_output/nodes/tables.py +60 -0
- sphinx_mintlify_output/nodes/toctree.py +96 -0
- sphinx_mintlify_output/py.typed +0 -0
- sphinx_mintlify_output/state.py +45 -0
- sphinx_mintlify_output/tables.py +124 -0
- sphinx_mintlify_output/toctree.py +80 -0
- sphinx_mintlify_output/urls.py +55 -0
- sphinx_mintlify_output-0.1.0.dist-info/METADATA +325 -0
- sphinx_mintlify_output-0.1.0.dist-info/RECORD +35 -0
- sphinx_mintlify_output-0.1.0.dist-info/WHEEL +4 -0
- sphinx_mintlify_output-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Sphinx builder that emits Mintlify-compatible MDX output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from sphinx_mintlify_output.builder import MintlifyBuilder
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from sphinx.application import Sphinx
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
|
|
14
|
+
__all__ = ["MintlifyBuilder", "__version__", "setup"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def setup(app: Sphinx) -> dict[str, Any]:
|
|
18
|
+
app.add_builder(MintlifyBuilder)
|
|
19
|
+
app.add_config_value("mintlify_docs_json", {}, "env")
|
|
20
|
+
app.add_config_value("mintlify_static_path", [], "env")
|
|
21
|
+
app.add_config_value("mintlify_image_dir", "images", "env")
|
|
22
|
+
app.add_config_value("mintlify_frontmatter", {}, "env")
|
|
23
|
+
app.add_config_value("mintlify_component_map", {}, "env")
|
|
24
|
+
app.add_config_value("mintlify_emit_anchors", True, "env")
|
|
25
|
+
app.add_config_value("mintlify_externalize_assets", True, "env")
|
|
26
|
+
app.add_config_value("mintlify_base_path", "", "env")
|
|
27
|
+
return {
|
|
28
|
+
"version": __version__,
|
|
29
|
+
"parallel_read_safe": True,
|
|
30
|
+
"parallel_write_safe": True,
|
|
31
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Externalize and write image assets referenced from raw HTML blocks.
|
|
2
|
+
|
|
3
|
+
Sphinx output can embed images two ways the translator has to handle:
|
|
4
|
+
|
|
5
|
+
* Inline ``<svg>...</svg>`` markup (e.g. from sphinxcontrib-svgbob).
|
|
6
|
+
* Base64 ``data:image/<mime>;base64,...`` URIs.
|
|
7
|
+
|
|
8
|
+
Both forms inflate the page and can break MDX parsing. The functions
|
|
9
|
+
here write the payload to ``<outdir>/<image_dir>/raw-<sha>.<ext>`` and
|
|
10
|
+
rewrite the HTML to reference the file instead. Asset writes are
|
|
11
|
+
idempotent: identical payloads share a single file.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import base64
|
|
17
|
+
import binascii
|
|
18
|
+
import hashlib
|
|
19
|
+
import os
|
|
20
|
+
import posixpath
|
|
21
|
+
import re
|
|
22
|
+
|
|
23
|
+
from sphinx_mintlify_output.urls import url_for
|
|
24
|
+
|
|
25
|
+
SVG_BLOCK_RE = re.compile(r"<svg\b[^>]*>.*?</svg>", re.DOTALL | re.IGNORECASE)
|
|
26
|
+
DATA_URI_RE = re.compile(
|
|
27
|
+
r"data:image/(?P<mime>png|jpeg|jpg|gif|svg\+xml|webp);base64,"
|
|
28
|
+
r"(?P<data>[A-Za-z0-9+/=\s]+?)(?=[\"')\s])",
|
|
29
|
+
re.IGNORECASE,
|
|
30
|
+
)
|
|
31
|
+
MIME_EXT: dict[str, str] = {
|
|
32
|
+
"png": "png",
|
|
33
|
+
"jpeg": "jpg",
|
|
34
|
+
"jpg": "jpg",
|
|
35
|
+
"gif": "gif",
|
|
36
|
+
"svg+xml": "svg",
|
|
37
|
+
"webp": "webp",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def externalize_inline_assets(
|
|
42
|
+
html: str,
|
|
43
|
+
*,
|
|
44
|
+
outdir: str,
|
|
45
|
+
image_dir: str,
|
|
46
|
+
from_doc: str,
|
|
47
|
+
) -> str:
|
|
48
|
+
"""Replace inline SVG and base64 data: URIs with file references.
|
|
49
|
+
|
|
50
|
+
Standalone ``<svg>…</svg>`` blocks are written as ``.svg`` files and
|
|
51
|
+
replaced with ``<img>`` tags. ``data:image/<mime>;base64,…`` payloads
|
|
52
|
+
inside larger HTML (typically inside ``<img src="…">``) are written
|
|
53
|
+
using the inferred extension and only the URI is replaced. Other HTML
|
|
54
|
+
passes through untouched. URLs are produced via
|
|
55
|
+
:func:`~sphinx_mintlify_output.urls.url_for` so they respect the
|
|
56
|
+
relative/absolute mode for the current build.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def replace_svg_block(match: re.Match[str]) -> str:
|
|
60
|
+
svg = match.group(0)
|
|
61
|
+
rel = write_asset_file(
|
|
62
|
+
svg.encode("utf-8"),
|
|
63
|
+
ext="svg",
|
|
64
|
+
outdir=outdir,
|
|
65
|
+
image_dir=image_dir,
|
|
66
|
+
from_doc=from_doc,
|
|
67
|
+
)
|
|
68
|
+
return f'<img src="{rel}" />'
|
|
69
|
+
|
|
70
|
+
def replace_data_uri(match: re.Match[str]) -> str:
|
|
71
|
+
mime = match.group("mime").lower()
|
|
72
|
+
raw = re.sub(r"\s+", "", match.group("data"))
|
|
73
|
+
ext = MIME_EXT.get(mime, "bin")
|
|
74
|
+
try:
|
|
75
|
+
payload = base64.b64decode(raw, validate=True)
|
|
76
|
+
except (binascii.Error, ValueError):
|
|
77
|
+
return match.group(0)
|
|
78
|
+
rel = write_asset_file(
|
|
79
|
+
payload,
|
|
80
|
+
ext=ext,
|
|
81
|
+
outdir=outdir,
|
|
82
|
+
image_dir=image_dir,
|
|
83
|
+
from_doc=from_doc,
|
|
84
|
+
)
|
|
85
|
+
return rel
|
|
86
|
+
|
|
87
|
+
html = SVG_BLOCK_RE.sub(replace_svg_block, html)
|
|
88
|
+
html = DATA_URI_RE.sub(replace_data_uri, html)
|
|
89
|
+
return html
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def write_asset_file(
|
|
93
|
+
content: bytes,
|
|
94
|
+
*,
|
|
95
|
+
ext: str,
|
|
96
|
+
outdir: str,
|
|
97
|
+
image_dir: str,
|
|
98
|
+
from_doc: str,
|
|
99
|
+
) -> str:
|
|
100
|
+
"""Write ``content`` to ``<outdir>/<image_dir>/raw-<hash>.<ext>``."""
|
|
101
|
+
digest = hashlib.sha256(content).hexdigest()[:16]
|
|
102
|
+
filename = f"raw-{digest}.{ext}"
|
|
103
|
+
target_dir = os.path.join(outdir, image_dir)
|
|
104
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
105
|
+
target = os.path.join(target_dir, filename)
|
|
106
|
+
if not os.path.exists(target):
|
|
107
|
+
with open(target, "wb") as fp:
|
|
108
|
+
fp.write(content)
|
|
109
|
+
return url_for(from_doc, posixpath.join(image_dir, filename))
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""Helpers for rendering Sphinx ``desc`` (autodoc) nodes as MDX.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
|
|
5
|
+
* Signature reconstruction (:func:`build_clean_signature`,
|
|
6
|
+
:func:`decorate_signature`).
|
|
7
|
+
* Heading and label formatting (:func:`format_desc_label`).
|
|
8
|
+
* Parameter parsing for ``Parameters`` field lists
|
|
9
|
+
(:func:`parse_param_item`, :func:`parse_param_head`,
|
|
10
|
+
:func:`extract_param_info`).
|
|
11
|
+
* Type-aware cross-referencing of strings like ``list[int] | None``
|
|
12
|
+
(:func:`link_types_in_string`, :func:`lookup_python_object`).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from docutils import nodes
|
|
21
|
+
|
|
22
|
+
from sphinx_mintlify_output.state import ParamInfo
|
|
23
|
+
from sphinx_mintlify_output.urls import url_for
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def desc_short_name(signature: nodes.Element) -> str:
|
|
27
|
+
"""Return the leaf name of a desc_signature (last ``desc_name`` found)."""
|
|
28
|
+
from sphinx import addnodes
|
|
29
|
+
|
|
30
|
+
short = ""
|
|
31
|
+
for descendant in signature.findall(condition=addnodes.desc_name):
|
|
32
|
+
short = descendant.astext().strip()
|
|
33
|
+
return short
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def decorate_signature(sig: str, desctype: str, return_type: str = "") -> str:
|
|
37
|
+
"""Massage a captured desc signature into a Python-like declaration."""
|
|
38
|
+
sig = sig.strip()
|
|
39
|
+
if desctype in {"function", "method", "staticmethod", "classmethod"}:
|
|
40
|
+
if sig.startswith("async "):
|
|
41
|
+
sig = "async def " + sig[len("async ") :]
|
|
42
|
+
elif sig.startswith("classmethod "):
|
|
43
|
+
sig = "@classmethod def " + sig[len("classmethod ") :]
|
|
44
|
+
elif sig.startswith("staticmethod "):
|
|
45
|
+
sig = "@staticmethod def " + sig[len("staticmethod ") :]
|
|
46
|
+
elif sig.startswith("abstractmethod "):
|
|
47
|
+
sig = "@abstractmethod def " + sig[len("abstractmethod ") :]
|
|
48
|
+
else:
|
|
49
|
+
sig = "def " + sig
|
|
50
|
+
if return_type and " -> " not in sig:
|
|
51
|
+
sig = f"{sig} -> {return_type}"
|
|
52
|
+
return sig
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _collect_signature_metadata(
|
|
56
|
+
signature: nodes.Element,
|
|
57
|
+
short_name: str,
|
|
58
|
+
) -> tuple[str, str, bool, str]:
|
|
59
|
+
"""Pull (full_name, annotation, has_paramlist, inline_return) from a sig."""
|
|
60
|
+
from sphinx import addnodes
|
|
61
|
+
|
|
62
|
+
full_name = short_name
|
|
63
|
+
annotation = ""
|
|
64
|
+
has_paramlist = False
|
|
65
|
+
inline_return = ""
|
|
66
|
+
for child in signature.children:
|
|
67
|
+
if isinstance(child, addnodes.desc_addname):
|
|
68
|
+
full_name = child.astext().strip() + short_name
|
|
69
|
+
elif isinstance(child, addnodes.desc_annotation):
|
|
70
|
+
text = child.astext()
|
|
71
|
+
if text.startswith(": "):
|
|
72
|
+
continue
|
|
73
|
+
annotation = text.strip()
|
|
74
|
+
elif isinstance(child, addnodes.desc_parameterlist):
|
|
75
|
+
has_paramlist = True
|
|
76
|
+
elif isinstance(child, addnodes.desc_returns):
|
|
77
|
+
text = child.astext().strip()
|
|
78
|
+
if text.startswith("->"):
|
|
79
|
+
text = text[2:].strip()
|
|
80
|
+
elif text.startswith("→"):
|
|
81
|
+
text = text[1:].strip()
|
|
82
|
+
inline_return = text
|
|
83
|
+
return full_name, annotation, has_paramlist, inline_return
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _collect_param_names(signature: nodes.Element) -> list[str]:
|
|
87
|
+
"""Extract parameter names from the first parameterlist of ``signature``."""
|
|
88
|
+
from sphinx import addnodes
|
|
89
|
+
|
|
90
|
+
param_names: list[str] = []
|
|
91
|
+
for plist in signature.findall(condition=addnodes.desc_parameterlist):
|
|
92
|
+
for param in plist.findall(condition=addnodes.desc_parameter):
|
|
93
|
+
text = param.astext().strip()
|
|
94
|
+
if text in {"/", "*"}:
|
|
95
|
+
param_names.append(text)
|
|
96
|
+
continue
|
|
97
|
+
match = SIGNATURE_PARAM_RE.match(text)
|
|
98
|
+
if match:
|
|
99
|
+
param_names.append(match.group("name"))
|
|
100
|
+
else:
|
|
101
|
+
param_names.append(text.split(":", 1)[0].split("=", 1)[0])
|
|
102
|
+
break
|
|
103
|
+
return param_names
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _build_class_signature(
|
|
107
|
+
desctype: str, full_name: str, param_names: list[str]
|
|
108
|
+
) -> str:
|
|
109
|
+
return f"{desctype} {full_name}({', '.join(param_names)})"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _build_callable_signature(
|
|
113
|
+
desctype: str,
|
|
114
|
+
full_name: str,
|
|
115
|
+
annotation: str,
|
|
116
|
+
param_names: list[str],
|
|
117
|
+
return_type: str,
|
|
118
|
+
) -> str:
|
|
119
|
+
prefix = ""
|
|
120
|
+
async_kw = ""
|
|
121
|
+
if annotation == "async":
|
|
122
|
+
async_kw = "async "
|
|
123
|
+
elif annotation == "classmethod":
|
|
124
|
+
prefix = "@classmethod\n"
|
|
125
|
+
elif annotation == "staticmethod":
|
|
126
|
+
prefix = "@staticmethod\n"
|
|
127
|
+
elif annotation == "abstractmethod":
|
|
128
|
+
prefix = "@abstractmethod\n"
|
|
129
|
+
if desctype == "classmethod" and "@classmethod" not in prefix:
|
|
130
|
+
prefix = "@classmethod\n"
|
|
131
|
+
elif desctype == "staticmethod" and "@staticmethod" not in prefix:
|
|
132
|
+
prefix = "@staticmethod\n"
|
|
133
|
+
body = f"{async_kw}def {full_name}({', '.join(param_names)})"
|
|
134
|
+
if return_type:
|
|
135
|
+
body = f"{body} -> {return_type}"
|
|
136
|
+
return prefix + body
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _build_property_signature(full_name: str, return_type: str) -> str:
|
|
140
|
+
body = f"@property\ndef {full_name}()"
|
|
141
|
+
if return_type:
|
|
142
|
+
body = f"{body} -> {return_type}"
|
|
143
|
+
return body
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def build_clean_signature(
|
|
147
|
+
signature: nodes.Element,
|
|
148
|
+
desctype: str,
|
|
149
|
+
short_name: str,
|
|
150
|
+
return_type: str = "",
|
|
151
|
+
) -> str:
|
|
152
|
+
"""Reconstruct a parameter-name-only signature with decorators."""
|
|
153
|
+
full_name, annotation, has_paramlist, inline_return = _collect_signature_metadata(
|
|
154
|
+
signature, short_name
|
|
155
|
+
)
|
|
156
|
+
if inline_return and not return_type:
|
|
157
|
+
return_type = inline_return
|
|
158
|
+
param_names = _collect_param_names(signature) if has_paramlist else []
|
|
159
|
+
|
|
160
|
+
if desctype in {"class", "exception"}:
|
|
161
|
+
return _build_class_signature(desctype, full_name, param_names)
|
|
162
|
+
if desctype in {"function", "method", "staticmethod", "classmethod"}:
|
|
163
|
+
return _build_callable_signature(
|
|
164
|
+
desctype, full_name, annotation, param_names, return_type
|
|
165
|
+
)
|
|
166
|
+
if desctype == "property":
|
|
167
|
+
return _build_property_signature(full_name, return_type)
|
|
168
|
+
return full_name
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def format_desc_label(desctype: str, short_name: str) -> str:
|
|
172
|
+
"""Return the heading label for a desc node, with type prefix when useful."""
|
|
173
|
+
if not short_name:
|
|
174
|
+
return desctype or "object"
|
|
175
|
+
if desctype == "class":
|
|
176
|
+
return f"`class {short_name}`"
|
|
177
|
+
if desctype == "exception":
|
|
178
|
+
return f"`exception {short_name}`"
|
|
179
|
+
if desctype in {"function", "method", "staticmethod", "classmethod"}:
|
|
180
|
+
return f"`{short_name}()`"
|
|
181
|
+
return f"`{short_name}`"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# Matches a single token from a Sphinx ``desc_parameterlist`` —
|
|
185
|
+
# ``name``, ``name: type``, ``name = default``, ``name: type = default``,
|
|
186
|
+
# ``*args`` / ``**kwargs``.
|
|
187
|
+
SIGNATURE_PARAM_RE = re.compile(
|
|
188
|
+
r"^(?P<name>\*{0,2}\w+)"
|
|
189
|
+
r"\s*(?::\s*(?P<type>[^=]+?))?"
|
|
190
|
+
r"\s*(?:=\s*(?P<default>.+))?$",
|
|
191
|
+
re.DOTALL,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def extract_param_info(desc_node: nodes.Element) -> dict[str, ParamInfo]:
|
|
196
|
+
"""Pull per-parameter type/default/required info from a desc node."""
|
|
197
|
+
from sphinx import addnodes
|
|
198
|
+
|
|
199
|
+
info: dict[str, ParamInfo] = {}
|
|
200
|
+
for plist in desc_node.findall(condition=addnodes.desc_parameterlist):
|
|
201
|
+
for param in plist.findall(condition=addnodes.desc_parameter):
|
|
202
|
+
text = param.astext().strip()
|
|
203
|
+
if text in {"/", "*"}:
|
|
204
|
+
continue
|
|
205
|
+
match = SIGNATURE_PARAM_RE.match(text)
|
|
206
|
+
if match is None:
|
|
207
|
+
continue
|
|
208
|
+
name = (match.group("name") or "").strip()
|
|
209
|
+
if not name:
|
|
210
|
+
continue
|
|
211
|
+
type_str = (match.group("type") or "").strip()
|
|
212
|
+
default = (match.group("default") or "").strip()
|
|
213
|
+
is_varargs = name.startswith("*")
|
|
214
|
+
info[name] = {
|
|
215
|
+
"type": type_str,
|
|
216
|
+
"default": default,
|
|
217
|
+
"required": not default and not is_varargs,
|
|
218
|
+
}
|
|
219
|
+
break
|
|
220
|
+
return info
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
EN_DASH = chr(0x2013)
|
|
224
|
+
EM_DASH = chr(0x2014)
|
|
225
|
+
PARAM_DASH_SEPARATORS = (f" {EN_DASH} ", f" {EM_DASH} ", " -- ")
|
|
226
|
+
PARAM_DASH_CLASS = "[-" + EN_DASH + EM_DASH + "]+"
|
|
227
|
+
# Matches one rendered docstring param line — ``name(type) -- description``
|
|
228
|
+
# or any of the dash variants. Used by :func:`parse_param_item` to peel
|
|
229
|
+
# ``:param X: text`` items rendered by Sphinx into the body field list.
|
|
230
|
+
DOCSTRING_PARAM_RE = re.compile(
|
|
231
|
+
"^\\s*(?P<name>\\S+?)\\s*(?:\\((?P<type>[^)]*)\\))?\\s*"
|
|
232
|
+
+ PARAM_DASH_CLASS
|
|
233
|
+
+ "\\s*(?P<desc>.*)$",
|
|
234
|
+
re.DOTALL,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
TYPE_TOKEN_RE = re.compile(r"([A-Za-z_][\w.]*)")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def lookup_python_object(env: Any, name: str) -> tuple[str, str] | None:
|
|
242
|
+
"""Find a Python-domain class/exception/function by short or full name.
|
|
243
|
+
|
|
244
|
+
Returns ``(docname, anchor)`` or ``None`` when nothing matches.
|
|
245
|
+
"""
|
|
246
|
+
try:
|
|
247
|
+
domain = env.get_domain("py")
|
|
248
|
+
except Exception:
|
|
249
|
+
return None
|
|
250
|
+
objects = getattr(domain, "objects", None)
|
|
251
|
+
if not objects:
|
|
252
|
+
return None
|
|
253
|
+
entry = objects.get(name)
|
|
254
|
+
if entry is not None:
|
|
255
|
+
objtype = getattr(entry, "objtype", "")
|
|
256
|
+
if objtype in {"class", "exception", "function", "method"}:
|
|
257
|
+
return getattr(entry, "docname", ""), getattr(entry, "node_id", "")
|
|
258
|
+
suffix = "." + name
|
|
259
|
+
for fullname, found in objects.items():
|
|
260
|
+
if fullname == name or fullname.endswith(suffix):
|
|
261
|
+
objtype = getattr(found, "objtype", "")
|
|
262
|
+
if objtype in {"class", "exception", "function", "method"}:
|
|
263
|
+
return getattr(found, "docname", ""), getattr(found, "node_id", "")
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def link_types_in_string(type_str: str, from_doc: str, env: Any) -> str:
|
|
268
|
+
"""Wrap recognised class names inside a type expression with markdown links."""
|
|
269
|
+
if not type_str:
|
|
270
|
+
return ""
|
|
271
|
+
|
|
272
|
+
def replace(match: re.Match[str]) -> str:
|
|
273
|
+
token = match.group(1)
|
|
274
|
+
if token in BUILTIN_TYPE_NAMES:
|
|
275
|
+
return token
|
|
276
|
+
ref = lookup_python_object(env, token)
|
|
277
|
+
if ref is None:
|
|
278
|
+
return token
|
|
279
|
+
docname, anchor = ref
|
|
280
|
+
href = url_for(from_doc, docname)
|
|
281
|
+
if anchor:
|
|
282
|
+
href = href + "#" + anchor
|
|
283
|
+
return f"[`{token}`]({href})"
|
|
284
|
+
|
|
285
|
+
return TYPE_TOKEN_RE.sub(replace, type_str)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
BUILTIN_TYPE_NAMES: frozenset[str] = frozenset(
|
|
289
|
+
{
|
|
290
|
+
"Any",
|
|
291
|
+
"Awaitable",
|
|
292
|
+
"Callable",
|
|
293
|
+
"ClassVar",
|
|
294
|
+
"Dict",
|
|
295
|
+
"Final",
|
|
296
|
+
"Generator",
|
|
297
|
+
"Iterable",
|
|
298
|
+
"Iterator",
|
|
299
|
+
"List",
|
|
300
|
+
"Literal",
|
|
301
|
+
"Mapping",
|
|
302
|
+
"None",
|
|
303
|
+
"Optional",
|
|
304
|
+
"Path",
|
|
305
|
+
"PurePosixPath",
|
|
306
|
+
"Sequence",
|
|
307
|
+
"Set",
|
|
308
|
+
"Tuple",
|
|
309
|
+
"Type",
|
|
310
|
+
"TypeVar",
|
|
311
|
+
"Union",
|
|
312
|
+
"UUID",
|
|
313
|
+
"bool",
|
|
314
|
+
"bytes",
|
|
315
|
+
"datetime",
|
|
316
|
+
"dict",
|
|
317
|
+
"float",
|
|
318
|
+
"frozenset",
|
|
319
|
+
"int",
|
|
320
|
+
"list",
|
|
321
|
+
"object",
|
|
322
|
+
"set",
|
|
323
|
+
"str",
|
|
324
|
+
"timedelta",
|
|
325
|
+
"tuple",
|
|
326
|
+
"type",
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def parse_param_item(item: nodes.Element) -> tuple[str, str, str]:
|
|
332
|
+
"""Extract (name, type, description) from a Sphinx autodoc param item."""
|
|
333
|
+
text = item.astext().strip()
|
|
334
|
+
match = DOCSTRING_PARAM_RE.match(text)
|
|
335
|
+
if match:
|
|
336
|
+
return (
|
|
337
|
+
match.group("name"),
|
|
338
|
+
(match.group("type") or "").strip(),
|
|
339
|
+
(match.group("desc") or "").strip(),
|
|
340
|
+
)
|
|
341
|
+
parts = text.split(None, 1)
|
|
342
|
+
if len(parts) == 2:
|
|
343
|
+
return parts[0], "", parts[1]
|
|
344
|
+
return text, "", ""
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def parse_param_head(item: nodes.Element) -> tuple[str, str]:
|
|
348
|
+
"""Extract only (name, type) from a Sphinx autodoc param item."""
|
|
349
|
+
name, type_str, _ = parse_param_item(item)
|
|
350
|
+
return name, type_str
|