|
1 | 1 | """Sphinx config.""" |
2 | 2 |
|
| 3 | +import importlib |
3 | 4 | import importlib.metadata |
| 5 | +import inspect |
| 6 | +from pathlib import Path |
4 | 7 | from typing import Any |
5 | 8 |
|
| 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 | + |
6 | 16 | project = "array-api-extra" |
7 | 17 | copyright = "Consortium for Python Data API Standards" |
8 | 18 | author = "Consortium for Python Data API Standards" |
|
44 | 54 | "class": "", |
45 | 55 | }, |
46 | 56 | ], |
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, |
49 | 59 | "source_directory": "docs/", |
50 | 60 | } |
51 | 61 |
|
|
72 | 82 |
|
73 | 83 | always_document_param_types = True |
74 | 84 | 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