|
1 | 1 | # Configuration file for the Sphinx documentation builder. |
2 | 2 | # https://www.sphinx-doc.org/en/master/usage/configuration.html |
3 | 3 |
|
| 4 | +# ruff: noqa: T201 |
| 5 | + |
| 6 | +import inspect |
| 7 | +import shutil |
| 8 | +import sys |
4 | 9 | from datetime import date |
| 10 | +from functools import cache |
| 11 | +from pathlib import Path |
| 12 | +from subprocess import check_output |
5 | 13 |
|
6 | 14 | import setuptools_scm |
7 | 15 |
|
| 16 | +import docstub |
| 17 | +from docstub._path_utils import walk_source_package |
| 18 | +from docstub._utils import module_name_from_path |
| 19 | + |
8 | 20 | # -- Project information ------------------------------------------------------ |
9 | 21 |
|
10 | 22 | project = "docstub" |
|
22 | 34 |
|
23 | 35 | extensions = [ |
24 | 36 | "sphinx.ext.intersphinx", |
| 37 | + "sphinx.ext.autodoc", |
| 38 | + "sphinx.ext.autosummary", |
| 39 | + "sphinx.ext.linkcode", |
25 | 40 | "sphinx_copybutton", |
26 | | - # https://numpydoc.readthedocs.io/ |
27 | 41 | "myst_parser", |
| 42 | + "numpydoc", |
28 | 43 | ] |
29 | 44 |
|
30 | 45 | intersphinx_mapping = { |
31 | 46 | "python": ("https://docs.python.org/3/", None), |
32 | 47 | "typing": ("https://typing.python.org/en/latest/", None), |
| 48 | + "lark": ("https://lark-parser.readthedocs.io/en/latest/", None), |
| 49 | + "libcst": ("https://libcst.readthedocs.io/en/latest/", None), |
33 | 50 | } |
34 | 51 |
|
35 | 52 | myst_enable_extensions = [ |
|
41 | 58 |
|
42 | 59 | myst_heading_anchors = 3 |
43 | 60 |
|
| 61 | +autosummary_generate = True |
| 62 | + |
| 63 | +autodoc_default_options = {"show-inheritance": True, "private-members": True} |
| 64 | + |
| 65 | +autodoc_member_order = "alphabetical" |
| 66 | + |
| 67 | +numpydoc_show_class_members = False |
| 68 | + |
| 69 | +add_module_names = False |
| 70 | + |
| 71 | + |
| 72 | +@cache |
| 73 | +def get_git_revision_hash(): |
| 74 | + """Extract hash of current revision.""" |
| 75 | + output = check_output(["git", "rev-parse", "HEAD"]) |
| 76 | + full_hash = output.decode().strip() |
| 77 | + return full_hash |
| 78 | + |
| 79 | + |
| 80 | +def linkcode_resolve(domain, info): |
| 81 | + """Resolve link for code objects. |
| 82 | +
|
| 83 | + This function is a hook called by the sphinx extension linkcode [1]_. |
| 84 | + Loosely inspired by a function in NumPy [2]_. |
| 85 | +
|
| 86 | + Parameters |
| 87 | + ---------- |
| 88 | + domain : str |
| 89 | + Specifies the language domain of the given object. |
| 90 | + info : dict |
| 91 | + A dictionary describing the object. |
| 92 | +
|
| 93 | + Returns |
| 94 | + ------- |
| 95 | + url : str |
| 96 | + An URL to the source code. |
| 97 | +
|
| 98 | + References |
| 99 | + --------- |
| 100 | + .. [1] http://www.sphinx-doc.org/en/master/usage/extensions/linkcode.html |
| 101 | + .. [2] https://github.com/numpy/numpy/blob/cbf3a081271a43e980e3c2f76625deb43fd53922/doc/source/conf.py#L303-L357 |
| 102 | + """ |
| 103 | + if domain != "py" or "module" not in info or "fullname" not in info: |
| 104 | + return None |
| 105 | + |
| 106 | + # Get reference to the object, start with the module |
| 107 | + obj = sys.modules.get(info["module"]) |
| 108 | + for part in info["fullname"].split("."): |
| 109 | + try: |
| 110 | + obj = getattr(obj, part) |
| 111 | + except AttributeError: |
| 112 | + # Not all attributes exist, e.g. instance attributes |
| 113 | + return None |
| 114 | + # Strip decorators which would resolve to the source of the decorator |
| 115 | + obj = inspect.unwrap(obj) |
| 116 | + |
| 117 | + file = info["module"].replace(".", "/") |
| 118 | + try: |
| 119 | + source, start_line = inspect.getsourcelines(obj) |
| 120 | + except (TypeError, OSError): |
| 121 | + # Skip unexpected types, occurs when `obj` isn't a module, class, method, |
| 122 | + # function, etc. |
| 123 | + return None |
| 124 | + stop_line = start_line + len(source) - 1 |
| 125 | + |
| 126 | + template = "https://github.com/scientific-python/docstub/blob/{revision}/src/{file}.py#L{start}-L{stop}" |
| 127 | + revision = get_git_revision_hash() |
| 128 | + assert len(revision) == 40 |
| 129 | + url = template.format( |
| 130 | + revision=revision, file=file, start=start_line, stop=stop_line |
| 131 | + ) |
| 132 | + return url |
| 133 | + |
44 | 134 |
|
45 | 135 | # -- HTML output -------------------------------------------------------------- |
46 | 136 |
|
|
79 | 169 | "sidebar/variant-selector.html", |
80 | 170 | ] |
81 | 171 | } |
| 172 | + |
| 173 | + |
| 174 | +# -- Pregenerate doc sources --------------------------------------------------- |
| 175 | + |
| 176 | +PYTHON_API_TEMPLATE = """.. Generated by `conf.py` |
| 177 | +
|
| 178 | +Python API |
| 179 | +========== |
| 180 | +
|
| 181 | +Docstub is primarily intended to be used as a command line tool. |
| 182 | +Nevertheless we document it's Python facing API here. |
| 183 | +
|
| 184 | +Modules |
| 185 | +------- |
| 186 | +
|
| 187 | +.. autosummary:: |
| 188 | + :toctree: |
| 189 | + :recursive: |
| 190 | + :template: module.rst |
| 191 | + :nosignatures: |
| 192 | +
|
| 193 | +""" |
| 194 | + |
| 195 | + |
| 196 | +def is_private_qualname(name): |
| 197 | + parts = name.split(".") |
| 198 | + return any(p.startswith("_") for p in parts) |
| 199 | + |
| 200 | + |
| 201 | +def write_python_api(module, *, exclude_modules=()): |
| 202 | + """Create ``python_api/index.rst`` as an entrypoint for autosummary. |
| 203 | +
|
| 204 | + Parameters |
| 205 | + ---------- |
| 206 | + module : ModuleType |
| 207 | + exclude_modules : tuple[str, ...] |
| 208 | + """ |
| 209 | + package_path = Path(module.__file__).parent |
| 210 | + |
| 211 | + names = sorted( |
| 212 | + module_name_from_path(path) for path in walk_source_package(package_path) |
| 213 | + ) |
| 214 | + names = [n for n in names if n not in exclude_modules] |
| 215 | + |
| 216 | + # Sort private modules after public ones |
| 217 | + public_names = [n for n in names if not is_private_qualname(n)] |
| 218 | + private_names = [n for n in names if is_private_qualname(n)] |
| 219 | + names = public_names + private_names |
| 220 | + |
| 221 | + names = [f" {name}" for name in names] |
| 222 | + content = "\n".join([PYTHON_API_TEMPLATE, *names]) |
| 223 | + |
| 224 | + api_dir = Path(__file__).parent / "python_api" |
| 225 | + if api_dir.exists(): |
| 226 | + shutil.rmtree(api_dir) |
| 227 | + print(f"[conf.py] deleted {api_dir}") |
| 228 | + |
| 229 | + api_dir.mkdir() |
| 230 | + index_path = api_dir / "index.rst" |
| 231 | + index_path.write_text(content) |
| 232 | + print(f"[conf.py] wrote {index_path}") |
| 233 | + |
| 234 | + |
| 235 | +write_python_api( |
| 236 | + docstub, |
| 237 | + exclude_modules=("docstub.__main__", "docstub._version"), |
| 238 | +) |
| 239 | + |
| 240 | + |
| 241 | +# -- Patch Sphinx -------------------------------------------------------------- |
| 242 | + |
| 243 | + |
| 244 | +def autodoc_skip_members(app, obj_type, name, obj, skip, options): |
| 245 | + """Force autosummary to show private members too.""" |
| 246 | + # TODO ensure that click commands are included as well |
| 247 | + if name.startswith("_") and not name.startswith("__"): |
| 248 | + # Force autosummary to show private members to, |
| 249 | + # `autodoc_default_options = {"private-members": True}` |
| 250 | + # doesn't solve this on its own |
| 251 | + return False |
| 252 | + return None # Default behavior |
| 253 | + |
| 254 | + |
| 255 | +def truncate_signature_defaults( |
| 256 | + app, what, name, obj, options, signature, return_annotation |
| 257 | +): |
| 258 | + """Truncate long defaults in function signatures.""" |
| 259 | + if what in {"module", "property", "attribute"}: |
| 260 | + return signature, return_annotation |
| 261 | + |
| 262 | + try: |
| 263 | + signature_object = inspect.signature(obj) |
| 264 | + except (TypeError, ValueError): |
| 265 | + pass |
| 266 | + else: |
| 267 | + for param in signature_object.parameters.values(): |
| 268 | + if param.default is inspect.Parameter.empty: |
| 269 | + continue |
| 270 | + default = repr(param.default) |
| 271 | + if len(default) > 30: |
| 272 | + truncated = default[:12] + "..." + default[-12:] |
| 273 | + signature = signature.replace(default, truncated) |
| 274 | + |
| 275 | + return signature, return_annotation |
| 276 | + |
| 277 | + |
| 278 | +def setup(app): |
| 279 | + app.connect("autodoc-process-signature", truncate_signature_defaults) |
| 280 | + app.connect("autodoc-skip-member", autodoc_skip_members) |
0 commit comments