Skip to content

Commit 38caa00

Browse files
Boulea7lucascolley
authored andcommitted
DOC: fix generated API source links
1 parent f13a835 commit 38caa00

1 file changed

Lines changed: 83 additions & 2 deletions

File tree

docs/conf.py

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
"""Sphinx config."""
22

3+
import importlib
34
import importlib.metadata
5+
import inspect
6+
from pathlib import Path
47
from typing import Any
58

9+
from sphinx import addnodes
10+
11+
_SOURCE_REPOSITORY = "https://github.com/data-apis/array-api-extra"
12+
_SOURCE_BRANCH = "main"
13+
_REPOSITORY_ROOT = Path(__file__).resolve().parent.parent
14+
_PACKAGE_SOURCE_ROOT = _REPOSITORY_ROOT / "src"
15+
616
project = "array-api-extra"
717
copyright = "Consortium for Python Data API Standards"
818
author = "Consortium for Python Data API Standards"
@@ -44,8 +54,8 @@
4454
"class": "",
4555
},
4656
],
47-
"source_repository": "https://github.com/data-apis/array-api-extra",
48-
"source_branch": "main",
57+
"source_repository": _SOURCE_REPOSITORY,
58+
"source_branch": _SOURCE_BRANCH,
4959
"source_directory": "docs/",
5060
}
5161

@@ -72,3 +82,74 @@
7282

7383
always_document_param_types = True
7484
typehints_document_overloads = False
85+
86+
87+
def _documented_object(
88+
doctree: Any,
89+
) -> object | None: # numpydoc ignore=PR01,RT01
90+
"""Return the first Python object described by a generated page."""
91+
for signature in doctree.findall(addnodes.desc_signature):
92+
module_name = signature.get("module")
93+
fullname = signature.get("fullname")
94+
if not isinstance(module_name, str) or not isinstance(fullname, str):
95+
continue
96+
97+
try:
98+
obj: object = importlib.import_module(module_name)
99+
for name in fullname.split("."):
100+
obj = getattr(obj, name)
101+
except (AttributeError, ImportError):
102+
continue
103+
return obj
104+
105+
return None
106+
107+
108+
def _repository_source_path(
109+
obj: object,
110+
) -> Path | None: # numpydoc ignore=PR01,RT01
111+
"""Return an object's source path when it belongs to this package."""
112+
if not callable(obj):
113+
return None
114+
115+
try:
116+
filename = inspect.getsourcefile(inspect.unwrap(obj))
117+
if filename is None:
118+
return None
119+
source_path = Path(filename).resolve(strict=True)
120+
return Path("src") / source_path.relative_to(_PACKAGE_SOURCE_ROOT)
121+
except (OSError, RuntimeError, TypeError, ValueError):
122+
return None
123+
124+
125+
def _set_generated_source_links(
126+
app: Any,
127+
pagename: str,
128+
templatename: str,
129+
context: dict[str, Any],
130+
doctree: Any,
131+
) -> None: # numpydoc ignore=PR01
132+
"""Set source links for an autosummary-generated page."""
133+
del app, templatename
134+
if doctree is None or not pagename.startswith("generated/"):
135+
return
136+
137+
obj = _documented_object(doctree)
138+
source_path = _repository_source_path(obj) if obj is not None else None
139+
if source_path is None:
140+
context["page_source_suffix"] = ""
141+
return
142+
143+
source_path_url = source_path.as_posix()
144+
context["theme_source_view_link"] = (
145+
f"{_SOURCE_REPOSITORY}/blob/{_SOURCE_BRANCH}/{source_path_url}?plain=true"
146+
)
147+
context["theme_source_edit_link"] = (
148+
f"{_SOURCE_REPOSITORY}/edit/{_SOURCE_BRANCH}/{source_path_url}"
149+
)
150+
151+
152+
def setup(app: Any) -> dict[str, bool]: # numpydoc ignore=PR01,RT01
153+
"""Register the generated-page source link handler."""
154+
app.connect("html-page-context", _set_generated_source_links, priority=900)
155+
return {"parallel_read_safe": True, "parallel_write_safe": True}

0 commit comments

Comments
 (0)