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
« 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."""
3import sys
4from datetime import UTC, datetime
5from inspect import stack
6from typing import Any, Final, Iterable, Mapping
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
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}
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.
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.
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}.")
69 doc_info: Final[DocInfo] = load_doc_info_from_setup_cfg(
70 root_path.resolve_inside("setup.cfg"))
72 # the global variables
73 global_vars: Final[dict[str, Any]] = stack()[1].frame.f_globals
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
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)
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())}.")
105 global_vars["intersphinx_mapping"] = use_deps
106 logger(f"Setting dependency mappings to {use_deps}.")
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
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] = "./"
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)
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))
132 # enable myst header anchors
133 global_vars["myst_heading_anchors"] = 6
135 # project information
136 global_vars["project"] = doc_info.project
137 global_vars["author"] = doc_info.author
139 # tell sphinx to go kaboom on errors
140 global_vars["nitpicky"] = True
141 global_vars["myst_all_links_external"] = True
143 # The full version, including alpha/beta/rc tags.
144 global_vars["release"] = doc_info.version
145 global_vars["version"] = doc_info.version
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
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 }
172 # inherit docstrings in autodoc
173 global_vars["autodoc_inherit_docstrings"] = True
175 # add default values after comma
176 global_vars["typehints_defaults"] = "comma"
178 # the sources to be processed
179 global_vars["source_suffix"] = [".rst", ".md"]
181 # Code syntax highlighting style:
182 global_vars["pygments_style"] = "default"
184 # The language is English.
185 global_vars["language"] = "en"
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
194 # Some python options
195 global_vars["python_display_short_literal_types"] = True
196 global_vars["python_use_unqualified_type_names"] = True
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)
202 logger(f"Finished setting up sphinx for document folder {doc_path!r} "
203 f"and root folder {root_path!r}.")