Coverage for pycommons / dev / doc / setup_doc.py: 100%

77 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-11 03:04 +0000

1"""Set up the documentation builder in a unified way.""" 

2 

3import sys 

4from datetime import UTC, datetime 

5from inspect import stack 

6from typing import Any, Final, Iterable, Mapping 

7 

8from pycommons.dev.doc.doc_info import DocInfo, load_doc_info_from_setup_cfg 

9from pycommons.dev.doc.index_rst import make_index_rst 

10from pycommons.dev.doc.process_md import ( 

11 process_markdown_for_sphinx, 

12) 

13from pycommons.io.console import logger 

14from pycommons.io.path import Path, directory_path, line_writer 

15from pycommons.types import check_int_range, type_error 

16 

17#: the default intersphinx mappings 

18__DEFAULT_INTERSPHINX: Final[dict[str, tuple[str, None]]] = { 

19 "latexgit": ("https://thomasweise.github.io/latexgit_py/", None), 

20 "matplotlib": ("https://matplotlib.org/stable/", None), 

21 "moptipy": ("https://thomasweise.github.io/moptipy/", None), 

22 "moptipyapps": ("https://thomasweise.github.io/moptipyapps/", None), 

23 "numpy": ("https://numpy.org/doc/stable/", None), 

24 "psutil": ("https://psutil.readthedocs.io/en/stable/", None), 

25 "pycommons": ("https://thomasweise.github.io/pycommons/", None), 

26 "python": ("https://docs.python.org/3/", None), 

27 "scipy": ("https://docs.scipy.org/doc/scipy/", None), 

28 "sklearn": ("https://scikit-learn.org/stable/", None), 

29 "urllib3": ("https://urllib3.readthedocs.io/en/stable/", None), 

30} 

31 

32 

33def setup_doc(doc_dir: str, root_dir: str, 

34 copyright_start_year: int | None = None, 

35 dependencies: Iterable[str | tuple[str, str]] | None = None, 

36 base_urls: Mapping[str, str] | None = None, 

37 full_urls: Mapping[str, str] | None = None, 

38 static_paths: Iterable[str] | None = None) -> None: 

39 """ 

40 Set up the documentation building process in a unified way. 

41 

42 This function must be called directly from the `conf.py` script. It will 

43 configure the sphinx documentation generation engine using my default 

44 settings. It can automatically link to several standard dependencies, 

45 render the `README.md` file of the project to a format that the myst 

46 parser used by sphinx can understand, fix absolute URLs in the 

47 `README.md` file that point to the documentation URL to relative links, 

48 get the documentation URL, the project title, author, and version from the 

49 root `setup.cfg` file from which it traces to the `README.md` and the 

50 `version.py` file of the project, and also construct an `index.rst` file. 

51 All in all, you will end up with a very unified setup for the 

52 documentation generation. Nothing fancy, but it will work and work the 

53 same in all of my projects without the need to copy and maintain 

54 boilerplate code. 

55 

56 :param doc_dir: the folder where the documentation is to be built. 

57 :param root_dir: the root path of the project. 

58 :param copyright_start_year: the copyright start year 

59 :param dependencies: the external libraries to use 

60 :param base_urls: a mapping of basic urls to shortcuts 

61 :param full_urls: a mapping of full urls to abbreviations 

62 :param static_paths: a list of static paths, if there are any 

63 """ 

64 doc_path: Final[Path] = directory_path(doc_dir) 

65 root_path: Final[Path] = directory_path(root_dir) 

66 logger(f"Beginning to set up sphinx for document folder {doc_path!r} " 

67 f"and root folder {root_path!r}.") 

68 

69 doc_info: Final[DocInfo] = load_doc_info_from_setup_cfg( 

70 root_path.resolve_inside("setup.cfg")) 

71 

72 # the global variables 

73 global_vars: Final[dict[str, Any]] = stack()[1].frame.f_globals 

74 

75 # create the copyright information 

76 current_year: Final[int] = datetime.now(UTC).year 

77 thecopyright: str = str(current_year) \ 

78 if (copyright_start_year is None) or (check_int_range( 

79 copyright_start_year, "copyright_start_year", 1980, 

80 current_year) == current_year) \ 

81 else f"{copyright_start_year}-{current_year}" 

82 thecopyright = f"{thecopyright}, {doc_info.author}" 

83 logger(f"Printing copyright information {thecopyright!r}.") 

84 global_vars["copyright"] = thecopyright 

85 

86 use_deps: dict[str, tuple[str, None]] = { 

87 "python": __DEFAULT_INTERSPHINX["python"]} 

88 if dependencies is not None: 

89 if not isinstance(dependencies, Iterable): 

90 raise type_error(dependencies, "dependencies", Iterable) 

91 

92 # Location of dependency documentation for cross-referencing. 

93 for x in dependencies: 

94 if isinstance(x, tuple): 

95 use_deps[str.strip(x[0])] = (str.strip(x[1]), None) 

96 else: 

97 xx = str.strip(x) 

98 if xx in __DEFAULT_INTERSPHINX: 

99 use_deps[xx] = __DEFAULT_INTERSPHINX[xx] 

100 else: 

101 raise ValueError( 

102 f"{x!r} is not among the known dependencies" 

103 f" {sorted(__DEFAULT_INTERSPHINX.keys())}.") 

104 

105 global_vars["intersphinx_mapping"] = use_deps 

106 logger(f"Setting dependency mappings to {use_deps}.") 

107 

108 # set the base url 

109 use_url: Final[str] = f"{doc_info.doc_url}" 

110 logger(f"Using base url {use_url!r}.") 

111 global_vars["html_baseurl"] = use_url 

112 

113 # set up the default urls 

114 if base_urls is None: 

115 base_urls = {doc_info.doc_url: "./"} 

116 elif doc_info.doc_url not in base_urls: 

117 base_urls = dict(base_urls) 

118 base_urls[doc_info.doc_url] = "./" 

119 

120 readme_out: Final[Path] = doc_path.resolve_inside("README.md") 

121 logger(f"Now processing {doc_info.readme_md_file!r} to {readme_out!r} " 

122 f"with replacers {base_urls} and {full_urls}.") 

123 with (doc_info.readme_md_file.open_for_read() as rd, 

124 readme_out.open_for_write() as wd): 

125 process_markdown_for_sphinx(rd, line_writer(wd), base_urls, full_urls) 

126 

127 index_rst_file: Final[Path] = doc_path.resolve_inside("index.rst") 

128 logger(f"Now writing index.rst file {index_rst_file!r}.") 

129 with index_rst_file.open_for_write() as wd: 

130 make_index_rst(doc_info, line_writer(wd)) 

131 

132 # enable myst header anchors 

133 global_vars["myst_heading_anchors"] = 6 

134 

135 # project information 

136 global_vars["project"] = doc_info.project 

137 global_vars["author"] = doc_info.author 

138 

139 # tell sphinx to go kaboom on errors 

140 global_vars["nitpicky"] = True 

141 global_vars["myst_all_links_external"] = True 

142 

143 # The full version, including alpha/beta/rc tags. 

144 global_vars["release"] = doc_info.version 

145 global_vars["version"] = doc_info.version 

146 

147 # The Sphinx extension modules that we use. 

148 extensions: Final[list[str]] = [ 

149 "myst_parser", # for processing README.md 

150 "sphinx.ext.autodoc", # to convert docstrings to documentation 

151 "sphinx.ext.doctest", # do the doc tests again 

152 "sphinx.ext.intersphinx", # to link to numpy et al. 

153 "sphinx_autodoc_typehints", # to infer types from hints 

154 "sphinx.ext.viewcode", # add rendered source code 

155 ] 

156 logger(f"Using extensions {extensions}.") 

157 global_vars["extensions"] = extensions 

158 

159 if "sphinx.ext.autodoc" in extensions: # Only relevant in this case. 

160 # Sometimes, we get a strange error that "typing.Union" is not found. 

161 # ------------------------------------------------ 

162 # ---- <unknown>:1: WARNING: 

163 # ---- py:data reference target not found: typing.Union [ref.data] 

164 # ------------------------------------------------ 

165 # This fixes it. We mark typing.Union as "known missing" targets. 

166 # see https://www.sphinx-doc.org/en/master/usage/configuration.html 

167 global_vars["nitpick_ignore"] = { 

168 ("py:data", "typing.Union"), 

169 ("ref.data", "typing.Union"), 

170 } 

171 

172 # inherit docstrings in autodoc 

173 global_vars["autodoc_inherit_docstrings"] = True 

174 

175 # add default values after comma 

176 global_vars["typehints_defaults"] = "comma" 

177 

178 # the sources to be processed 

179 global_vars["source_suffix"] = [".rst", ".md"] 

180 

181 # Code syntax highlighting style: 

182 global_vars["pygments_style"] = "default" 

183 

184 # The language is English. 

185 global_vars["language"] = "en" 

186 

187 # The theme to use for HTML and HTML Help pages. 

188 global_vars["html_theme"] = "bizstyle" 

189 # The potential static paths 

190 if static_paths is not None: 

191 global_vars["html_static_path"] = sorted(map(str.strip, static_paths)) 

192 global_vars["html_show_sphinx"] = False 

193 

194 # Some python options 

195 global_vars["python_display_short_literal_types"] = True 

196 global_vars["python_use_unqualified_type_names"] = True 

197 

198 # get the path to the root directory of this project 

199 if (list.__len__(sys.path) <= 0) or (sys.path[0] != root_path): 

200 sys.path.insert(0, root_path) 

201 

202 logger(f"Finished setting up sphinx for document folder {doc_path!r} " 

203 f"and root folder {root_path!r}.")