Coverage for src/ensembl/utils/docs/config.py: 100%
31 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-09 09:43 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-09 09:43 +0000
1# See the NOTICE file distributed with this work for additional information
2# regarding copyright ownership.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15"""Shared Sphinx configuration for Ensembl documentation builds.
17Downstream repositories drive their ``docs/conf.py`` from this module::
19 from ensembl.utils.docs import configure
21 configure(
22 globals(),
23 project="ensembl-utils",
24 repo_url="https://github.com/Ensembl/ensembl-utils",
25 docs_base_url="https://ensembl.github.io/ensembl-utils",
26 )
28Any standard Sphinx setting can be tweaked *after* the ``configure`` call by reassigning or mutating
29the matching module-level variable, e.g. ``extensions += ["sphinx_click"]`` or
30``html_theme_options["announcement"] = ...``.
32"""
34__all__ = ["build_config", "configure"]
36from datetime import datetime, timezone
37import os
38from pathlib import Path
39from typing import Any
40import warnings
42from ensembl.utils import StrPath
44_STATIC_DIR = Path(__file__).parent / "_static"
47def build_config(
48 *,
49 project: str,
50 repo_url: str,
51 docs_base_url: str,
52 release: str | None = None,
53 json_url: str | None = None,
54 coverage_root: StrPath | None = None,
55 include_entrypoints: bool = False,
56 add_pypi_icon: bool = False,
57 **overrides: Any,
58) -> dict[str, Any]:
59 """Return the Ensembl-standard Sphinx configuration as a mapping.
61 Args:
62 project: Human-readable project name, e.g. ``"ensembl-utils"``.
63 repo_url: GitHub or GitLab URL for this repository, used to build the source link.
64 docs_base_url: Public base URL where the docs are published; the version switcher JSON is
65 expected at ``{docs_base_url}/switcher.json``.
66 release: Release version the switcher highlights as current.
67 json_url: Location of the switcher JSON. If omitted, hosted/CI builds point at
68 ``{docs_base_url}/switcher.json`` and local builds use the relative ``_static/switcher.json``.
69 coverage_root: Absolute path to the directory where pytest's HTML coverage report **folder**
70 is generated to include it with the documentation.
71 include_entrypoints: When ``True``, adds :mod:`ensembl.utils.docs.entrypoints_table` to ``extensions``
72 so the CLI entry-points table is auto-injected into the target Markdown file at build time.
73 add_pypi_icon: When ``True``, adds the PyPI icon and link to the project at the top right of the page.
74 **overrides: Any extra ``conf.py`` values; these win over the defaults.
76 Returns:
77 A dictionary suitable for injecting into a ``conf.py`` namespace.
79 """
80 base_url = docs_base_url.rstrip("/")
81 # If release version is not provided, take it from the DOCS_VERSION environment variable, which
82 # should be set to the git tag name (e.g. "v1.2.0"). Falls back to "dev" for local builds so
83 # conf.py is always valid without any environment setup.
84 version_match = f"v{release}" if release is not None else os.environ.get("DOCS_VERSION", "dev")
85 # If run via GitHub Actions or GitLab CI/CD, point the switcher at the published JSON. Anything
86 # else is a local build and uses the copy generated into the build's own _static.
87 if json_url is None:
88 json_url = f"{base_url}/switcher.json" if os.environ.get("CI") else "_static/switcher.json"
89 # Set up Sphinx configuration
90 config: dict[str, Any] = {
91 # Project information
92 "project": project,
93 "author": "EMBL-European Bioinformatics Institute",
94 "copyright": f"2016-{datetime.now(tz=timezone.utc).year}, EMBL-European Bioinformatics Institute",
95 # General configuration
96 "extensions": [
97 "myst_parser",
98 "sphinx.ext.autodoc",
99 "sphinx.ext.coverage",
100 "sphinx.ext.extlinks",
101 "sphinx.ext.intersphinx",
102 "sphinx.ext.napoleon",
103 "sphinx.ext.viewcode",
104 "sphinx_autodoc_typehints",
105 "sphinx_copybutton",
106 # Registers this package's own setup(app) for deferred config (CSS, ...)
107 "ensembl.utils.docs",
108 ],
109 "language": "en",
110 # MyST settings
111 "myst_enable_extensions": ["colon_fence", "substitution"],
112 "myst_heading_anchors": 3,
113 # Autodoc settings
114 "autodoc_default_options": {
115 "members": True,
116 "show-inheritance": True,
117 "private-members": False,
118 "undoc-members": False,
119 "special-members": "__repr__",
120 },
121 "autodoc_typehints": "description",
122 "autodoc_typehints_description_target": "documented",
123 "suppress_warnings": ["autodoc.duplicate_object", "sphinx_autodoc_typehints.forward_reference"],
124 "typehints_defaults": "comma",
125 "typehints_document_rtype_none": False,
126 # Napolean settings
127 "napoleon_use_ivar": True,
128 # Coverage settings
129 "coverage_write_headline": False,
130 # HTML output settings
131 "html_theme": "pydata_sphinx_theme",
132 "html_sourcelink_suffix": "",
133 "html_last_updated_fmt": "",
134 "html_title": project,
135 "html_static_path": [],
136 "html_css_files": ["ensembl.css"],
137 "html_js_files": [
138 ("extra-icons.js", {"defer": "defer"}),
139 ],
140 # Additional HTML options
141 "html_theme_options": {
142 "footer_start": ["copyright"],
143 "footer_center": ["sphinx-version"],
144 "header_links_before_dropdown": 4,
145 "icon_links": [],
146 "logo": {
147 "text": project,
148 },
149 "navbar_align": "left",
150 "navbar_center": ["version-switcher", "navbar-nav"],
151 "navigation_with_keys": True,
152 "search_as_you_type": True,
153 "secondary_sidebar_items": {
154 "**/*": ["page-toc"],
155 "coverage_report": [],
156 },
157 "show_toc_level": 2,
158 "show_version_warning_banner": True,
159 "switcher": {
160 "json_url": json_url,
161 "version_match": version_match,
162 },
163 "use_edit_page_button": False,
164 },
165 "html_sidebars": {
166 "coverage_report": [],
167 },
168 }
169 if coverage_root:
170 if Path(coverage_root).exists():
171 config["html_extra_path"] = [str(coverage_root)]
172 else:
173 warnings.warn(
174 f"Coverage root folder '{coverage_root}' does not exist. Remember to run 'make coverage' "
175 "before 'make docs'.",
176 stacklevel=2,
177 )
178 if include_entrypoints:
179 config["extensions"].append("ensembl.utils.docs.entrypoints_ext")
180 if "github" in repo_url:
181 config["html_theme_options"]["icon_links"].append(
182 {"name": "GitHub", "url": repo_url, "icon": "fa-brands fa-github"}
183 )
184 elif "gitlab" in repo_url:
185 config["html_theme_options"]["icon_links"].append(
186 {"name": "GitLab", "url": repo_url, "icon": "fa-brands fa-square-gitlab"}
187 )
188 else:
189 warnings.warn(
190 f"Unrecognised repository platform in '{repo_url}'. No icon link will be added.",
191 stacklevel=2,
192 )
193 if add_pypi_icon:
194 config["html_theme_options"]["icon_links"].append(
195 {
196 "name": "PyPI",
197 "url": f"https://pypi.org/project/{project}/",
198 "icon": "fa-custom fa-pypi",
199 }
200 )
201 config.update(overrides)
202 return config
205def configure(namespace: dict[str, Any], **kwargs: Any) -> None:
206 """Populate a ``conf.py`` namespace in place with the Ensembl defaults.
208 Call at the top of ``docs/conf.py`` as ``configure(globals(), ...)``. See :func:`build_config`
209 for the accepted keyword arguments.
211 Args:
212 namespace: The ``conf.py`` module namespace to populate, normally passed as ``globals()``.
213 **kwargs: Forwarded verbatim to :func:`build_config`.
215 """
216 namespace.update(build_config(**kwargs)) # pylint: disable=missing-kwoa