scverse-doc 0.0.1__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.
- scverse_doc/__init__.py +57 -0
- scverse_doc/_accents.json +12 -0
- scverse_doc/_color.py +145 -0
- scverse_doc/autodoc/__init__.py +70 -0
- scverse_doc/autodoc/templates/autosummary/class.rst +54 -0
- scverse_doc/autodoc/templates/autosummary/module.rst +62 -0
- scverse_doc/config.py +90 -0
- scverse_doc/napoleon.py +54 -0
- scverse_doc/py.typed +0 -0
- scverse_doc/registry.py +249 -0
- scverse_doc/source.py +243 -0
- scverse_doc/theme/__init__.py +121 -0
- scverse_doc/theme/scverse/components/scverse-ecosystem.html +54 -0
- scverse_doc/theme/scverse/components/scverse-footer.html +18 -0
- scverse_doc/theme/scverse/components/scverse-sidebar-nav.html +17 -0
- scverse_doc/theme/scverse/components/sidebar-versions.html +1 -0
- scverse_doc/theme/scverse/components/toggle-primary-sidebar.html +8 -0
- scverse_doc/theme/scverse/components/toggle-secondary-sidebar.html +12 -0
- scverse_doc/theme/scverse/layout.html +5 -0
- scverse_doc/theme/scverse/static/_tokens.css +42 -0
- scverse_doc/theme/scverse/static/scripts/readthedocs-integration.js +66 -0
- scverse_doc/theme/scverse/static/scverse-fa.svg +4 -0
- scverse_doc/theme/scverse/static/styles/scverse.css +346 -0
- scverse_doc/theme/scverse/theme.toml +30 -0
- scverse_doc-0.0.1.dist-info/METADATA +119 -0
- scverse_doc-0.0.1.dist-info/RECORD +29 -0
- scverse_doc-0.0.1.dist-info/WHEEL +4 -0
- scverse_doc-0.0.1.dist-info/entry_points.txt +2 -0
- scverse_doc-0.0.1.dist-info/licenses/LICENSE +373 -0
scverse_doc/__init__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""The scverse Sphinx theme and shared documentation configuration.
|
|
2
|
+
|
|
3
|
+
A package’s `conf.py` needs few lines:
|
|
4
|
+
|
|
5
|
+
.. code:: python
|
|
6
|
+
|
|
7
|
+
extensions = ["scverse_doc"]
|
|
8
|
+
|
|
9
|
+
source_repository = "https://github.com/scverse/pertpy"
|
|
10
|
+
|
|
11
|
+
This extension sets up the subextensions,
|
|
12
|
+
each of which also works on its own:
|
|
13
|
+
|
|
14
|
+
:mod:`scverse_doc.autodoc`
|
|
15
|
+
The layout of the API pages.
|
|
16
|
+
:mod:`scverse_doc.config`
|
|
17
|
+
The extension stack and the shared defaults.
|
|
18
|
+
:mod:`scverse_doc.napoleon`
|
|
19
|
+
Prose return sections in NumPy-style docstrings.
|
|
20
|
+
:mod:`scverse_doc.registry`
|
|
21
|
+
The package registry, usable as an :func:`~scverse_doc.registry.intersphinx` mapping.
|
|
22
|
+
:mod:`scverse_doc.source`
|
|
23
|
+
The repository links: the header icon, “edit this page”, and ``[source]``.
|
|
24
|
+
:mod:`scverse_doc.theme`
|
|
25
|
+
The theme, its chrome, and the per-package accent.
|
|
26
|
+
|
|
27
|
+
Anything set in `conf.py` wins.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from typing import TYPE_CHECKING
|
|
33
|
+
|
|
34
|
+
from sphinx.util.typing import ExtensionMetadata
|
|
35
|
+
|
|
36
|
+
from . import autodoc, config, napoleon, registry, source, theme
|
|
37
|
+
from .config import _is_set_by_user
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from sphinx.application import Sphinx
|
|
41
|
+
from sphinx.config import Config
|
|
42
|
+
|
|
43
|
+
__all__ = ["autodoc", "config", "napoleon", "registry", "source", "theme", "setup"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _default_theme(app: Sphinx, config: Config) -> None:
|
|
47
|
+
"""Select the theme, since the umbrella extension means it too, unless `conf.py` picked another."""
|
|
48
|
+
if not _is_set_by_user(config, "html_theme"):
|
|
49
|
+
config.html_theme = "scverse"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def setup(app: Sphinx) -> ExtensionMetadata:
|
|
53
|
+
"""Set up the registry and the subextensions."""
|
|
54
|
+
for extension in set(__all__) - {"setup"}:
|
|
55
|
+
app.setup_extension(f"scverse_doc.{extension}")
|
|
56
|
+
app.connect("config-inited", _default_theme)
|
|
57
|
+
return ExtensionMetadata(parallel_read_safe=True)
|
scverse_doc/_color.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Colour maths for deriving accessible accent shades.
|
|
2
|
+
|
|
3
|
+
Package accents are chosen for brand recognition, not for contrast.
|
|
4
|
+
Several of the accents scverse already uses (``#6cf1a1`` for muon, ``#fbb822`` for scvi-tools)
|
|
5
|
+
fail WCAG AA as text colours on white, and others fail on a dark background.
|
|
6
|
+
So the raw accent is only ever used for decorative surfaces,
|
|
7
|
+
and text/link colours are *derived* from it here by moving lightness until the contrast target is met,
|
|
8
|
+
keeping hue and saturation intact so the result still reads as the package’s colour.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import colorsys
|
|
14
|
+
import re
|
|
15
|
+
from typing import NamedTuple
|
|
16
|
+
|
|
17
|
+
__all__ = ["contrast_ratio", "derive_readable", "parse_hex", "relative_luminance"]
|
|
18
|
+
|
|
19
|
+
#: Minimum contrast ratio for normal-size body text under WCAG 2.1 AA.
|
|
20
|
+
WCAG_AA_NORMAL = 4.5
|
|
21
|
+
|
|
22
|
+
_HEX_RE = re.compile(r"\A#?(?P<digits>[0-9a-fA-F]{3}|[0-9a-fA-F]{6})\Z")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RGB(NamedTuple):
|
|
26
|
+
"""An sRGB colour with channels in ``[0, 1]``."""
|
|
27
|
+
|
|
28
|
+
r: float
|
|
29
|
+
g: float
|
|
30
|
+
b: float
|
|
31
|
+
|
|
32
|
+
def to_hex(self) -> str:
|
|
33
|
+
"""Render as a lowercase ``#rrggbb`` string."""
|
|
34
|
+
return "#" + "".join(f"{round(c * 255):02x}" for c in self)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_hex(color: str) -> RGB:
|
|
38
|
+
"""Parse a CSS hex colour.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
color
|
|
43
|
+
A ``#rgb`` or ``#rrggbb`` string, with or without the leading ``#``.
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
The colour, with channels normalised to ``[0, 1]``.
|
|
48
|
+
|
|
49
|
+
Raises
|
|
50
|
+
------
|
|
51
|
+
ValueError
|
|
52
|
+
If `color` is not a three- or six-digit hex colour.
|
|
53
|
+
"""
|
|
54
|
+
if (m := _HEX_RE.match(color.strip())) is None:
|
|
55
|
+
msg = f"Not a hex colour: {color!r}"
|
|
56
|
+
raise ValueError(msg)
|
|
57
|
+
digits = m["digits"]
|
|
58
|
+
if len(digits) == 3:
|
|
59
|
+
digits = "".join(d * 2 for d in digits)
|
|
60
|
+
return RGB(*(c / 255 for c in bytes.fromhex(digits)))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _to_linear(channel: float) -> float:
|
|
64
|
+
"""Undo the sRGB transfer function for a single channel."""
|
|
65
|
+
return channel / 12.92 if channel <= 0.04045 else ((channel + 0.055) / 1.055) ** 2.4
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def relative_luminance(color: RGB | str) -> float:
|
|
69
|
+
"""Compute the WCAG relative luminance of a colour.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
color
|
|
74
|
+
An :class:`RGB` colour or a hex string.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
Relative luminance in ``[0, 1]``, where 0 is black and 1 is white.
|
|
79
|
+
"""
|
|
80
|
+
r, g, b = parse_hex(color) if isinstance(color, str) else color
|
|
81
|
+
return 0.2126 * _to_linear(r) + 0.7152 * _to_linear(g) + 0.0722 * _to_linear(b)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def contrast_ratio(fg: RGB | str, bg: RGB | str) -> float:
|
|
85
|
+
"""Compute the WCAG contrast ratio between two colours.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
fg
|
|
90
|
+
Foreground colour.
|
|
91
|
+
bg
|
|
92
|
+
Background colour.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
A ratio between 1 (identical) and 21 (black on white).
|
|
97
|
+
"""
|
|
98
|
+
lum_a, lum_b = relative_luminance(fg), relative_luminance(bg)
|
|
99
|
+
lighter, darker = max(lum_a, lum_b), min(lum_a, lum_b)
|
|
100
|
+
return (lighter + 0.05) / (darker + 0.05)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def derive_readable(accent: str, background: str, *, target: float = WCAG_AA_NORMAL) -> str:
|
|
104
|
+
"""Derive a text-safe variant of `accent` that meets `target` contrast against `background`.
|
|
105
|
+
|
|
106
|
+
Hue and saturation are preserved and only lightness is moved,
|
|
107
|
+
so the result still reads as the same colour.
|
|
108
|
+
Lightness moves away from the background:
|
|
109
|
+
darker on a light background, lighter on a dark one.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
accent
|
|
114
|
+
The brand accent to adjust, as a hex string.
|
|
115
|
+
background
|
|
116
|
+
The background the derived colour will sit on, as a hex string.
|
|
117
|
+
target
|
|
118
|
+
The contrast ratio to reach.
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
A hex colour meeting `target` where that is achievable by lightness alone,
|
|
123
|
+
otherwise the closest achievable colour, which is black or white.
|
|
124
|
+
|
|
125
|
+
Examples
|
|
126
|
+
--------
|
|
127
|
+
>>> derive_readable("#fbb822", "#ffffff") # scvi-tools yellow, unreadable as-is
|
|
128
|
+
'#9d6d03'
|
|
129
|
+
"""
|
|
130
|
+
if contrast_ratio(accent, background) >= target:
|
|
131
|
+
return parse_hex(accent).to_hex()
|
|
132
|
+
|
|
133
|
+
hue, lightness, saturation = colorsys.rgb_to_hls(*parse_hex(accent))
|
|
134
|
+
darken = relative_luminance(background) > 0.5
|
|
135
|
+
step = -1 / 512 if darken else 1 / 512
|
|
136
|
+
|
|
137
|
+
# Candidates are checked after rounding to 8-bit hex:
|
|
138
|
+
# a float lightness that clears the target can fall back under it once quantized,
|
|
139
|
+
# which is how #e5864b and #fbb822 ended up one hundredth short of AA.
|
|
140
|
+
while 0.0 <= (lightness := lightness + step) <= 1.0:
|
|
141
|
+
candidate = parse_hex(RGB(*colorsys.hls_to_rgb(hue, lightness, saturation)).to_hex())
|
|
142
|
+
if contrast_ratio(candidate, background) >= target:
|
|
143
|
+
return candidate.to_hex()
|
|
144
|
+
|
|
145
|
+
return "#000000" if darken else "#ffffff"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""The layout of the API pages.
|
|
2
|
+
|
|
3
|
+
Ships :mod:`~sphinx.ext.autosummary` templates for modules and classes.
|
|
4
|
+
A class page gets tables of its attributes and methods, then their documentation,
|
|
5
|
+
with each attribute documented as a property, method or plain attribute as appropriate.
|
|
6
|
+
Templates of the same name in your own `templates_path` still win.
|
|
7
|
+
|
|
8
|
+
Also hides ``object``, :class:`~typing.Generic` and private classes from the bases
|
|
9
|
+
that ``:show-inheritance:`` lists.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import traceback
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING, Generic, Literal, get_origin
|
|
17
|
+
|
|
18
|
+
from jinja2.defaults import DEFAULT_FILTERS # type: ignore[attr-defined]
|
|
19
|
+
from sphinx.ext.autodoc import import_object
|
|
20
|
+
from sphinx.util import logging
|
|
21
|
+
from sphinx.util.typing import ExtensionMetadata
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from sphinx.application import Sphinx
|
|
27
|
+
from sphinx.config import Config
|
|
28
|
+
|
|
29
|
+
__all__ = ["setup"]
|
|
30
|
+
|
|
31
|
+
_logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
TEMPLATES = Path(__file__).parent / "templates"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _member_type(obj_path: str) -> Literal["method", "property", "attribute"]:
|
|
37
|
+
"""Determine what kind of member `obj_path` is, for ``.. auto{{ fullname | member_type }}::``."""
|
|
38
|
+
cls_path, cls_name, member_name = obj_path.rsplit(".", 2)
|
|
39
|
+
try:
|
|
40
|
+
cls = import_object(cls_path, [cls_name], "class")[-1]
|
|
41
|
+
except ImportError:
|
|
42
|
+
_logger.error(f"Failed to import {cls_name} from {cls_path}: {traceback.format_exc()}")
|
|
43
|
+
return "attribute"
|
|
44
|
+
match getattr(cls, member_name, None):
|
|
45
|
+
case property():
|
|
46
|
+
return "property"
|
|
47
|
+
case member if callable(member):
|
|
48
|
+
return "method"
|
|
49
|
+
case _:
|
|
50
|
+
return "attribute"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _skip_private_bases(app: Sphinx, name: str, obj: type, options: Any, bases: list[type]) -> None:
|
|
54
|
+
bases[:] = [b for b in bases if b is not object and get_origin(b) is not Generic and not b.__name__.startswith("_")]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _add_templates(app: Sphinx, config: Config) -> None:
|
|
58
|
+
# Appended, so the package’s own templates win.
|
|
59
|
+
config.templates_path = [*config.templates_path, str(TEMPLATES)]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def setup(app: Sphinx) -> ExtensionMetadata:
|
|
63
|
+
"""Register the templates, their filter, and the bases hook."""
|
|
64
|
+
app.setup_extension("sphinx.ext.autodoc")
|
|
65
|
+
app.setup_extension("sphinx.ext.autosummary")
|
|
66
|
+
# TODO: autosummary builds its own jinja environment with no hook for filters, so this is global.
|
|
67
|
+
DEFAULT_FILTERS["member_type"] = _member_type
|
|
68
|
+
app.connect("config-inited", _add_templates)
|
|
69
|
+
app.connect("autodoc-process-bases", _skip_private_bases)
|
|
70
|
+
return ExtensionMetadata(parallel_read_safe=True)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{{ fullname | escape | underline }}
|
|
2
|
+
|
|
3
|
+
.. currentmodule:: {{ module }}
|
|
4
|
+
|
|
5
|
+
.. add toctree option to make autodoc generate the pages
|
|
6
|
+
|
|
7
|
+
.. autoclass:: {{ fullname }}
|
|
8
|
+
|
|
9
|
+
{% set methods = methods | select("ne", "__init__") | list %}
|
|
10
|
+
|
|
11
|
+
{% block attributes %}
|
|
12
|
+
{% for item in attributes %}
|
|
13
|
+
{% if loop.first %}
|
|
14
|
+
Attributes table
|
|
15
|
+
~~~~~~~~~~~~~~~~
|
|
16
|
+
|
|
17
|
+
.. autosummary::
|
|
18
|
+
{% endif %}
|
|
19
|
+
~{{ item }}
|
|
20
|
+
{%- endfor %}
|
|
21
|
+
{% endblock %}
|
|
22
|
+
|
|
23
|
+
{% block methods %}
|
|
24
|
+
{% for item in methods %}
|
|
25
|
+
{% if loop.first %}
|
|
26
|
+
Methods table
|
|
27
|
+
~~~~~~~~~~~~~
|
|
28
|
+
|
|
29
|
+
.. autosummary::
|
|
30
|
+
{% endif %}
|
|
31
|
+
~{{ item }}
|
|
32
|
+
{% endfor %}
|
|
33
|
+
{% endblock %}
|
|
34
|
+
|
|
35
|
+
{% block attributes_documentation %}
|
|
36
|
+
{% for item in attributes %}
|
|
37
|
+
{% if loop.first %}
|
|
38
|
+
Attributes
|
|
39
|
+
~~~~~~~~~~
|
|
40
|
+
|
|
41
|
+
{% endif %}
|
|
42
|
+
.. auto{{ [fullname, item] | join(".") | member_type }}:: {{ [fullname, item] | join(".") }}
|
|
43
|
+
{%- endfor %}
|
|
44
|
+
{% endblock %}
|
|
45
|
+
|
|
46
|
+
{% block methods_documentation %}
|
|
47
|
+
{% for item in methods %}
|
|
48
|
+
{% if loop.first %}
|
|
49
|
+
Methods
|
|
50
|
+
~~~~~~~
|
|
51
|
+
{% endif %}
|
|
52
|
+
.. automethod:: {{ [fullname, item] | join(".") }}
|
|
53
|
+
{%- endfor %}
|
|
54
|
+
{% endblock %}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{{ fullname | escape | underline}}
|
|
2
|
+
|
|
3
|
+
.. automodule:: {{ fullname }}
|
|
4
|
+
|
|
5
|
+
{% block attributes %}
|
|
6
|
+
{% if attributes %}
|
|
7
|
+
.. rubric:: {{ _('Module Attributes') }}
|
|
8
|
+
|
|
9
|
+
.. autosummary::
|
|
10
|
+
{% for item in attributes %}
|
|
11
|
+
{{ item }}
|
|
12
|
+
{%- endfor %}
|
|
13
|
+
{% endif %}
|
|
14
|
+
{% endblock %}
|
|
15
|
+
|
|
16
|
+
{% block functions %}
|
|
17
|
+
{% if functions %}
|
|
18
|
+
.. rubric:: {{ _('Functions') }}
|
|
19
|
+
|
|
20
|
+
.. autosummary::
|
|
21
|
+
:toctree:
|
|
22
|
+
{% for item in functions %}
|
|
23
|
+
{{ item }}
|
|
24
|
+
{%- endfor %}
|
|
25
|
+
{% endif %}
|
|
26
|
+
{% endblock %}
|
|
27
|
+
|
|
28
|
+
{% block classes %}
|
|
29
|
+
{% if classes %}
|
|
30
|
+
.. rubric:: {{ _('Classes') }}
|
|
31
|
+
|
|
32
|
+
.. autosummary::
|
|
33
|
+
:toctree:
|
|
34
|
+
{% for item in classes %}
|
|
35
|
+
{{ item }}
|
|
36
|
+
{%- endfor %}
|
|
37
|
+
{% endif %}
|
|
38
|
+
{% endblock %}
|
|
39
|
+
|
|
40
|
+
{% block exceptions %}
|
|
41
|
+
{% if exceptions %}
|
|
42
|
+
.. rubric:: {{ _('Exceptions') }}
|
|
43
|
+
|
|
44
|
+
.. autosummary::
|
|
45
|
+
:toctree:
|
|
46
|
+
{% for item in exceptions %}
|
|
47
|
+
{{ item }}
|
|
48
|
+
{%- endfor %}
|
|
49
|
+
{% endif %}
|
|
50
|
+
{% endblock %}
|
|
51
|
+
|
|
52
|
+
{% block modules %}
|
|
53
|
+
{% if modules %}
|
|
54
|
+
.. rubric:: Modules
|
|
55
|
+
|
|
56
|
+
.. autosummary::
|
|
57
|
+
:toctree:
|
|
58
|
+
{% for item in modules %}
|
|
59
|
+
{{ item }}
|
|
60
|
+
{%- endfor %}
|
|
61
|
+
{% endif %}
|
|
62
|
+
{% endblock %}
|
scverse_doc/config.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""The extension stack and the shared defaults.
|
|
2
|
+
|
|
3
|
+
Sets up the extensions in :data:`EXTENSIONS`, so `conf.py` lists only ``scverse_doc``,
|
|
4
|
+
and fills in :data:`DEFAULTS`. Anything set in `conf.py` always wins.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from sphinx.util.typing import ExtensionMetadata
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from sphinx.application import Sphinx
|
|
15
|
+
from sphinx.config import Config
|
|
16
|
+
|
|
17
|
+
__all__ = ["DEFAULTS", "EXTENSIONS", "MYST_ENABLE_EXTENSIONS", "setup"]
|
|
18
|
+
|
|
19
|
+
#: Extensions set up alongside this one,
|
|
20
|
+
#: so packages list only ``scverse_doc``.
|
|
21
|
+
EXTENSIONS = (
|
|
22
|
+
"myst_nb",
|
|
23
|
+
"sphinx.ext.autodoc",
|
|
24
|
+
"sphinx.ext.autosummary",
|
|
25
|
+
"sphinx.ext.intersphinx",
|
|
26
|
+
"sphinx.ext.napoleon",
|
|
27
|
+
"sphinx_copybutton",
|
|
28
|
+
"sphinx_design",
|
|
29
|
+
"sphinxext.opengraph",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
#: The MyST syntax extensions, i.e. the ``myst_enable_extensions`` entry of :data:`~scverse_doc.config.DEFAULTS`.
|
|
33
|
+
MYST_ENABLE_EXTENSIONS = (
|
|
34
|
+
"amsmath",
|
|
35
|
+
"colon_fence",
|
|
36
|
+
"deflist",
|
|
37
|
+
"dollarmath",
|
|
38
|
+
"html_image",
|
|
39
|
+
"html_admonition",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
#: Sphinx config values applied unless `conf.py` sets them.
|
|
43
|
+
DEFAULTS: dict[str, Any] = {
|
|
44
|
+
"autosummary_generate": True,
|
|
45
|
+
"autodoc_member_order": "groupwise",
|
|
46
|
+
"napoleon_google_docstring": False,
|
|
47
|
+
"napoleon_numpy_docstring": True,
|
|
48
|
+
"napoleon_include_init_with_doc": False,
|
|
49
|
+
"napoleon_use_param": True,
|
|
50
|
+
"napoleon_use_rtype": True,
|
|
51
|
+
"default_role": "literal",
|
|
52
|
+
"myst_enable_extensions": list(MYST_ENABLE_EXTENSIONS),
|
|
53
|
+
"myst_heading_anchors": 6,
|
|
54
|
+
"source_suffix": {".rst": "restructuredtext", ".md": "myst-nb", ".ipynb": "myst-nb"},
|
|
55
|
+
# Off: tutorials needing a GPU or a large download must not gate a docs build.
|
|
56
|
+
"nb_execution_mode": "off",
|
|
57
|
+
"nb_output_stderr": "remove",
|
|
58
|
+
"nb_merge_streams": True,
|
|
59
|
+
# One unreachable host must not stall the build.
|
|
60
|
+
"intersphinx_timeout": 10,
|
|
61
|
+
"exclude_patterns": ["_build", "Thumbs.db", ".DS_Store", "**.ipynb_checkpoints"],
|
|
62
|
+
"nitpicky": True,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _is_set_by_user(config: Config, name: str) -> bool:
|
|
67
|
+
"""Report whether `conf.py` or a ``-D`` override set `name`."""
|
|
68
|
+
return name in config._raw_config or name in config._overrides
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _apply_defaults(config: Config) -> None:
|
|
72
|
+
"""Fill in the values `conf.py` did not set."""
|
|
73
|
+
for name, value in DEFAULTS.items():
|
|
74
|
+
if _is_set_by_user(config, name):
|
|
75
|
+
continue
|
|
76
|
+
setattr(config, name, value)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _configure(app: Sphinx, config: Config) -> None:
|
|
80
|
+
_apply_defaults(config)
|
|
81
|
+
if not _is_set_by_user(config, "html_title"):
|
|
82
|
+
config.html_title = config.project
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def setup(app: Sphinx) -> ExtensionMetadata:
|
|
86
|
+
"""Set up the extension stack and apply the defaults."""
|
|
87
|
+
for extension in EXTENSIONS:
|
|
88
|
+
app.setup_extension(extension)
|
|
89
|
+
app.connect("config-inited", _configure)
|
|
90
|
+
return ExtensionMetadata(parallel_read_safe=True)
|
scverse_doc/napoleon.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Prose return sections in NumPy-style docstrings.
|
|
2
|
+
|
|
3
|
+
:mod:`sphinx.ext.napoleon` reads a NumPy-style ``Returns`` section as a list of ``name : type`` entries.
|
|
4
|
+
With this extension, it can be prose instead:
|
|
5
|
+
|
|
6
|
+
.. code:: rst
|
|
7
|
+
|
|
8
|
+
Returns
|
|
9
|
+
-------
|
|
10
|
+
The fitted model, or `None` if `copy` is false.
|
|
11
|
+
|
|
12
|
+
Lines that do look like ``name : type`` become an item linking to `type`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from sphinx.ext.napoleon import NumpyDocstring # type: ignore[attr-defined]
|
|
21
|
+
from sphinx.util.typing import ExtensionMetadata
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Iterable, Iterator
|
|
25
|
+
|
|
26
|
+
from sphinx.application import Sphinx
|
|
27
|
+
|
|
28
|
+
__all__ = ["setup"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _process_return(lines: Iterable[str]) -> Iterator[str]:
|
|
32
|
+
for line in lines:
|
|
33
|
+
if m := re.fullmatch(r"(?P<param>\w+)\s+:\s+(?P<type>[\w.]+)", line):
|
|
34
|
+
yield f"-{m['param']} (:class:`~{m['type']}`)"
|
|
35
|
+
else:
|
|
36
|
+
yield line
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _parse_returns_section(self: NumpyDocstring, section: str) -> list[str]:
|
|
40
|
+
lines_raw = self._dedent(self._consume_to_next_section())
|
|
41
|
+
if lines_raw and lines_raw[0] == ":":
|
|
42
|
+
del lines_raw[0]
|
|
43
|
+
lines = self._format_block(":returns: ", list(_process_return(lines_raw)))
|
|
44
|
+
if lines and lines[-1]:
|
|
45
|
+
lines.append("")
|
|
46
|
+
return lines
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def setup(app: Sphinx) -> ExtensionMetadata:
|
|
50
|
+
"""Set up napoleon and make it read ``Returns`` sections as prose."""
|
|
51
|
+
app.setup_extension("sphinx.ext.napoleon")
|
|
52
|
+
# TODO: patches private napoleon API; replace once napoleon has a hook for this.
|
|
53
|
+
NumpyDocstring._parse_returns_section = _parse_returns_section # type: ignore[method-assign,assignment]
|
|
54
|
+
return ExtensionMetadata(parallel_read_safe=True)
|
scverse_doc/py.typed
ADDED
|
File without changes
|