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

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. 

16 

17Downstream repositories drive their ``docs/conf.py`` from this module:: 

18 

19 from ensembl.utils.docs import configure 

20 

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 ) 

27 

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"] = ...``. 

31 

32""" 

33 

34__all__ = ["build_config", "configure"] 

35 

36from datetime import datetime, timezone 

37import os 

38from pathlib import Path 

39from typing import Any 

40import warnings 

41 

42from ensembl.utils import StrPath 

43 

44_STATIC_DIR = Path(__file__).parent / "_static" 

45 

46 

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. 

60 

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. 

75 

76 Returns: 

77 A dictionary suitable for injecting into a ``conf.py`` namespace. 

78 

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 

203 

204 

205def configure(namespace: dict[str, Any], **kwargs: Any) -> None: 

206 """Populate a ``conf.py`` namespace in place with the Ensembl defaults. 

207 

208 Call at the top of ``docs/conf.py`` as ``configure(globals(), ...)``. See :func:`build_config` 

209 for the accepted keyword arguments. 

210 

211 Args: 

212 namespace: The ``conf.py`` module namespace to populate, normally passed as ``globals()``. 

213 **kwargs: Forwarded verbatim to :func:`build_config`. 

214 

215 """ 

216 namespace.update(build_config(**kwargs)) # pylint: disable=missing-kwoa