doc-zero 0.1.1__tar.gz
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.
- doc_zero-0.1.1/PKG-INFO +102 -0
- doc_zero-0.1.1/README.md +73 -0
- doc_zero-0.1.1/doc0/__init__.py +9 -0
- doc_zero-0.1.1/doc0/__main__.py +4 -0
- doc_zero-0.1.1/doc0/base.py +445 -0
- doc_zero-0.1.1/doc0/cli.py +101 -0
- doc_zero-0.1.1/doc0/exports.py +243 -0
- doc_zero-0.1.1/doc0/module.py +181 -0
- doc_zero-0.1.1/doc0/py.typed +0 -0
- doc_zero-0.1.1/doc0/pyproject.py +167 -0
- doc_zero-0.1.1/doc0/util.py +61 -0
- doc_zero-0.1.1/pyproject.toml +92 -0
- doc_zero-0.1.1/pyproject.toml.orig +79 -0
doc_zero-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: doc-zero
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Zero-configuration documentation generator for Python.
|
|
5
|
+
Author: Fábio Macêdo Mendes
|
|
6
|
+
Author-email: Fábio Macêdo Mendes <fabiomacedomendes@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Topic :: Utilities
|
|
15
|
+
Requires-Dist: myst-parser>=5.1.0
|
|
16
|
+
Requires-Dist: rich>=15.0.0
|
|
17
|
+
Requires-Dist: sphinx>=9.1.0
|
|
18
|
+
Requires-Dist: sphinx-autobuild>=2025.8.25
|
|
19
|
+
Requires-Dist: sphinx-mdinclude>=0.6.2
|
|
20
|
+
Requires-Dist: sphinx-rtd-theme>=3.1.0
|
|
21
|
+
Requires-Dist: typer>=0.27.0
|
|
22
|
+
Maintainer: Fábio Macêdo Mendes
|
|
23
|
+
Maintainer-email: Fábio Macêdo Mendes <fabiomacedomendes@gmail.com>
|
|
24
|
+
Requires-Python: >=3.13
|
|
25
|
+
Project-URL: Homepage, http://github.com/fabiommendes/zero-doc
|
|
26
|
+
Project-URL: Repository, http://github.com/fabiommendes/zero-doc
|
|
27
|
+
Project-URL: Documentation, https://zero-doc.readthedocs.io/
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# doc-zero
|
|
31
|
+
|
|
32
|
+
`doc-zero` streamlines the process of writing documentation for your project. It is
|
|
33
|
+
an opinionated and explicitly non-configurable tool that extracts information
|
|
34
|
+
from your Python codebase and generates nice documentation with minimal effort.
|
|
35
|
+
|
|
36
|
+
The main influence is the `elm` language tooling: no config, nice defaults and
|
|
37
|
+
it creates a very decent documentation out of the box. Rust has a similar
|
|
38
|
+
experience with RustDoc. In comparison, both Sphinx and MkDocs are very powerful
|
|
39
|
+
but somewhat clunky to use and configure.
|
|
40
|
+
|
|
41
|
+
## How does it work?
|
|
42
|
+
|
|
43
|
+
`doc0` introspect your codebase and creates a Sphinx project under the hood. In
|
|
44
|
+
practice, if you have a relatively modern Python project (i.e., it assumes the
|
|
45
|
+
existence of pyproject.toml) just type
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
$ doc0 build
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
in the project root and it will create and build the documentation under
|
|
52
|
+
`<project-root>/docs`. In `doc0`, all your documentation resides either in the
|
|
53
|
+
README.md file in your repository or inside the source code.
|
|
54
|
+
|
|
55
|
+
You can also type
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
$ doc0 serve
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
and
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
doc0 test
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
to run either the live server or to test the doctests inside the documentation.
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
## Adding the documentation to your project
|
|
71
|
+
|
|
72
|
+
Doc0 assumes your project is already documented using docstrings and that
|
|
73
|
+
you have a README.md file in the project root. It will use those assets to
|
|
74
|
+
generate the documentation and the necessary configurations to make it buildable
|
|
75
|
+
with Sphinx and ready to be hosted to readthedocs.io.
|
|
76
|
+
|
|
77
|
+
The first step is to install `doc0` as a development dependency in your project.
|
|
78
|
+
You can do this by running
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
$ pip install doc0
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
or the equivalent command for the package manager of choice.
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
Then, the following command generates the documentation:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
$ doc0 build
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`doc0` always creates a module documentation for your toplevel module. It will
|
|
94
|
+
also scan all sub-modules and generate a documentation page if they satisfy the
|
|
95
|
+
following conditions:
|
|
96
|
+
|
|
97
|
+
* The module is not private (i.e., it does not start with an underscore).
|
|
98
|
+
* The module has a docstring.
|
|
99
|
+
* The module defines a `__all__` variable that lists its public API.
|
|
100
|
+
|
|
101
|
+
`doc0` only includes the public API in the generated documentation.
|
|
102
|
+
|
doc_zero-0.1.1/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# doc-zero
|
|
2
|
+
|
|
3
|
+
`doc-zero` streamlines the process of writing documentation for your project. It is
|
|
4
|
+
an opinionated and explicitly non-configurable tool that extracts information
|
|
5
|
+
from your Python codebase and generates nice documentation with minimal effort.
|
|
6
|
+
|
|
7
|
+
The main influence is the `elm` language tooling: no config, nice defaults and
|
|
8
|
+
it creates a very decent documentation out of the box. Rust has a similar
|
|
9
|
+
experience with RustDoc. In comparison, both Sphinx and MkDocs are very powerful
|
|
10
|
+
but somewhat clunky to use and configure.
|
|
11
|
+
|
|
12
|
+
## How does it work?
|
|
13
|
+
|
|
14
|
+
`doc0` introspect your codebase and creates a Sphinx project under the hood. In
|
|
15
|
+
practice, if you have a relatively modern Python project (i.e., it assumes the
|
|
16
|
+
existence of pyproject.toml) just type
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
$ doc0 build
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
in the project root and it will create and build the documentation under
|
|
23
|
+
`<project-root>/docs`. In `doc0`, all your documentation resides either in the
|
|
24
|
+
README.md file in your repository or inside the source code.
|
|
25
|
+
|
|
26
|
+
You can also type
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
$ doc0 serve
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
and
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
doc0 test
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
to run either the live server or to test the doctests inside the documentation.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## Adding the documentation to your project
|
|
42
|
+
|
|
43
|
+
Doc0 assumes your project is already documented using docstrings and that
|
|
44
|
+
you have a README.md file in the project root. It will use those assets to
|
|
45
|
+
generate the documentation and the necessary configurations to make it buildable
|
|
46
|
+
with Sphinx and ready to be hosted to readthedocs.io.
|
|
47
|
+
|
|
48
|
+
The first step is to install `doc0` as a development dependency in your project.
|
|
49
|
+
You can do this by running
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
$ pip install doc0
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
or the equivalent command for the package manager of choice.
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
Then, the following command generates the documentation:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
$ doc0 build
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`doc0` always creates a module documentation for your toplevel module. It will
|
|
65
|
+
also scan all sub-modules and generate a documentation page if they satisfy the
|
|
66
|
+
following conditions:
|
|
67
|
+
|
|
68
|
+
* The module is not private (i.e., it does not start with an underscore).
|
|
69
|
+
* The module has a docstring.
|
|
70
|
+
* The module defines a `__all__` variable that lists its public API.
|
|
71
|
+
|
|
72
|
+
`doc0` only includes the public API in the generated documentation.
|
|
73
|
+
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from importlib.metadata import version as module_version
|
|
7
|
+
from logging import getLogger
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Iterable, Iterator, TypedDict
|
|
10
|
+
|
|
11
|
+
from .module import Module
|
|
12
|
+
from .pyproject import PyProject
|
|
13
|
+
from .util import first_existing
|
|
14
|
+
|
|
15
|
+
type ModuleName = str
|
|
16
|
+
|
|
17
|
+
NOT_GIVEN: Any = object()
|
|
18
|
+
COPYRIGHT_RE = re.compile(
|
|
19
|
+
r"[cC]opyright\s+(?:\(c\)\s+)?(?P<year>\d+)\s*(:?,?\s+(?P<author>[^\n]+))?"
|
|
20
|
+
)
|
|
21
|
+
DEFAULT_EXTENSIONS = [
|
|
22
|
+
"sphinx.ext.autodoc",
|
|
23
|
+
"sphinx_mdinclude",
|
|
24
|
+
# "myst_parser",
|
|
25
|
+
]
|
|
26
|
+
SPHINX_THEME_ALIASES = {
|
|
27
|
+
"rtd": "sphinx_rtd_theme",
|
|
28
|
+
"readthedocs": "sphinx_rtd_theme",
|
|
29
|
+
"default": "alabaster",
|
|
30
|
+
}
|
|
31
|
+
READTHEDOCS_TEMPLATE = """
|
|
32
|
+
# Read the Docs configuration file
|
|
33
|
+
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
|
34
|
+
|
|
35
|
+
# Required
|
|
36
|
+
version: 2
|
|
37
|
+
|
|
38
|
+
# Set the OS, Python version, and other tools you might need
|
|
39
|
+
build:
|
|
40
|
+
os: ubuntu-24.04
|
|
41
|
+
tools:
|
|
42
|
+
python: "3.13"
|
|
43
|
+
|
|
44
|
+
# Build documentation in the "docs/" directory with Sphinx
|
|
45
|
+
sphinx:
|
|
46
|
+
configuration: docs/conf.py
|
|
47
|
+
|
|
48
|
+
# Optionally, but recommended,
|
|
49
|
+
# declare the Python requirements required to build your documentation
|
|
50
|
+
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
|
51
|
+
python:
|
|
52
|
+
install:
|
|
53
|
+
- requirements: docs/requirements.txt
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
log = getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class Doc0:
|
|
61
|
+
"""
|
|
62
|
+
The root type representing the documentation of your project.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
#: The pyproject.toml file for the project.
|
|
66
|
+
pyproject: PyProject
|
|
67
|
+
|
|
68
|
+
#: Base location for the documentation assets
|
|
69
|
+
doc_root: Path
|
|
70
|
+
|
|
71
|
+
#: The theme to use for the documentation.
|
|
72
|
+
theme: str
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def load(
|
|
76
|
+
cls,
|
|
77
|
+
root: Path | None = None,
|
|
78
|
+
/,
|
|
79
|
+
*,
|
|
80
|
+
theme: str | None = None,
|
|
81
|
+
docs: str = "docs",
|
|
82
|
+
) -> Doc0:
|
|
83
|
+
"""
|
|
84
|
+
Load project in the given path.
|
|
85
|
+
"""
|
|
86
|
+
root = root or Path.cwd()
|
|
87
|
+
pyproject = PyProject(root=root)
|
|
88
|
+
|
|
89
|
+
if theme is None:
|
|
90
|
+
theme = pyproject.get("tool.doc-zero.theme", default="default", type=str)
|
|
91
|
+
|
|
92
|
+
return Doc0(
|
|
93
|
+
doc_root=root / docs,
|
|
94
|
+
pyproject=pyproject,
|
|
95
|
+
theme=theme,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def root(self) -> Path:
|
|
100
|
+
"""
|
|
101
|
+
The root of the project.
|
|
102
|
+
"""
|
|
103
|
+
return self.pyproject.root
|
|
104
|
+
|
|
105
|
+
def init(self) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Assure that the documentation is initialized.
|
|
108
|
+
|
|
109
|
+
Call .generate() if the documentation is not initialized.
|
|
110
|
+
"""
|
|
111
|
+
self.doc_root.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
(self.doc_root / "_static").mkdir(exist_ok=True)
|
|
113
|
+
|
|
114
|
+
# Write/overwrite docs/conf.py.
|
|
115
|
+
conf_path = self.doc_root / "conf.py"
|
|
116
|
+
conf = Conf.from_pyproject(self.pyproject, theme=self.theme)
|
|
117
|
+
conf_path.write_text(conf.render())
|
|
118
|
+
|
|
119
|
+
# Write docs/index.rst and docs/api/*
|
|
120
|
+
self.write_rst_files()
|
|
121
|
+
self.write_readme_md()
|
|
122
|
+
|
|
123
|
+
# Write the Read the Docs configuration file, if it doesn't exist.
|
|
124
|
+
rtd_path = self.root / ".readthedocs.yml"
|
|
125
|
+
if not rtd_path.exists():
|
|
126
|
+
rtd_path.write_text(READTHEDOCS_TEMPLATE)
|
|
127
|
+
|
|
128
|
+
# Write the requirements.txt file for Read the Docs, if it doesn't exist.
|
|
129
|
+
req_path = self.root / "docs" / "requirements.txt"
|
|
130
|
+
if not req_path.exists():
|
|
131
|
+
req_path.write_text(f"doc0>={module_version('doc0')}")
|
|
132
|
+
|
|
133
|
+
def build(self) -> None:
|
|
134
|
+
"""
|
|
135
|
+
Build the documentation using sphinx.
|
|
136
|
+
"""
|
|
137
|
+
from sphinx.cmd.build import main
|
|
138
|
+
|
|
139
|
+
self.init()
|
|
140
|
+
main([str(self.doc_root), str(self.root / "dist" / "docs")])
|
|
141
|
+
|
|
142
|
+
def serve(self) -> None:
|
|
143
|
+
"""
|
|
144
|
+
Start the live server.
|
|
145
|
+
"""
|
|
146
|
+
from sphinx_autobuild.__main__ import main
|
|
147
|
+
|
|
148
|
+
self.init()
|
|
149
|
+
main([str(self.doc_root), str(self.root / "dist" / "docs")])
|
|
150
|
+
|
|
151
|
+
def test(self) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Execute all doctests.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
#
|
|
157
|
+
# Write parts of the documentation
|
|
158
|
+
#
|
|
159
|
+
def write_rst_files(self) -> None:
|
|
160
|
+
"""
|
|
161
|
+
Write the index, API docs and process the User guide.
|
|
162
|
+
"""
|
|
163
|
+
self.doc_root.mkdir(parents=True, exist_ok=True)
|
|
164
|
+
roots = list(self.pyproject.find_root_modules())
|
|
165
|
+
public_modules = [root.load_module() for root in roots]
|
|
166
|
+
|
|
167
|
+
for root in roots:
|
|
168
|
+
for sub_module in root.iter_submodules(skip_private=True):
|
|
169
|
+
mod = sub_module.load_module()
|
|
170
|
+
docstring = mod.docstring
|
|
171
|
+
if docstring is None:
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
if mod.exports is None:
|
|
175
|
+
msg = "Module %s has no __all__ attribute" % sub_module.name
|
|
176
|
+
log.warning(msg)
|
|
177
|
+
elif not mod.exports:
|
|
178
|
+
msg = "Module %s do not export any symbols" % sub_module.name
|
|
179
|
+
log.warning(msg)
|
|
180
|
+
|
|
181
|
+
public_modules.append(mod)
|
|
182
|
+
|
|
183
|
+
index = Index.load(self.pyproject.name, self.doc_root, public_modules)
|
|
184
|
+
index_path = self.doc_root / "index.rst"
|
|
185
|
+
index_path.write_text(index.render())
|
|
186
|
+
|
|
187
|
+
# Clean the docs/api directory
|
|
188
|
+
api_dir = self.doc_root / "api"
|
|
189
|
+
if api_dir.exists():
|
|
190
|
+
shutil.rmtree(api_dir)
|
|
191
|
+
api_dir.mkdir(parents=True, exist_ok=True)
|
|
192
|
+
|
|
193
|
+
# Create the API documentation for each public module and the index.rst file.
|
|
194
|
+
for module in public_modules:
|
|
195
|
+
module_path = self.doc_root / "api" / f"{module.name}.rst"
|
|
196
|
+
module_path.write_text(module.render())
|
|
197
|
+
(self.doc_root / "api" / "_index.rst").write_text(
|
|
198
|
+
render_modules_index(public_modules)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def write_readme_md(self) -> None:
|
|
202
|
+
"""
|
|
203
|
+
Write the README.md file for the documentation.
|
|
204
|
+
"""
|
|
205
|
+
readme_path = self.root / "README.md"
|
|
206
|
+
if not readme_path.exists():
|
|
207
|
+
src = f"This is the documentation for {self.pyproject.name}. Please include a README.md file in the documentation root directory."
|
|
208
|
+
readme_path.write_text(src)
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
src = readme_path.read_text()
|
|
212
|
+
parts = re.split(r"<!--\s*doc0-start\s*-->", src, maxsplit=1)
|
|
213
|
+
if len(parts) == 1:
|
|
214
|
+
src = remove_md_title(src)
|
|
215
|
+
else:
|
|
216
|
+
src = parts[1]
|
|
217
|
+
|
|
218
|
+
(self.doc_root / "_readme.md").write_text(src)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@dataclass
|
|
222
|
+
class Index:
|
|
223
|
+
"""
|
|
224
|
+
Content of the index.rst file.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
name: str
|
|
228
|
+
|
|
229
|
+
# It uses the framework described at https://diataxis.fr
|
|
230
|
+
tutorials: Path | None = None
|
|
231
|
+
how_to_guides: Path | None = None
|
|
232
|
+
explanations: Path | None = None
|
|
233
|
+
|
|
234
|
+
# Reference is concepts + api documentation
|
|
235
|
+
concepts: Path | None = None
|
|
236
|
+
api_modules: list[str] = field(default_factory=list)
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def load(name: str, root: Path, modules: Iterable[Module]):
|
|
240
|
+
"""
|
|
241
|
+
Load the index.rst configuration from the given root path and modules.
|
|
242
|
+
|
|
243
|
+
It will search the root path for the tutorials, how-to guides and
|
|
244
|
+
explanations directories in order to fill-in the appropriate fields.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
module_names = [mod.name for mod in modules]
|
|
248
|
+
|
|
249
|
+
def select(name: str, plural: str | None = None) -> Path | None:
|
|
250
|
+
"""
|
|
251
|
+
Select the first existing path for the given name.
|
|
252
|
+
"""
|
|
253
|
+
plural = plural or name + "s"
|
|
254
|
+
return first_existing(
|
|
255
|
+
[
|
|
256
|
+
root / plural,
|
|
257
|
+
root / f"{name}.rst",
|
|
258
|
+
root / f"{name}.md",
|
|
259
|
+
]
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
tutorials = select("tutorial")
|
|
263
|
+
how_to_guides = select("how-to-guide")
|
|
264
|
+
explanations = select("explanation")
|
|
265
|
+
concepts = select("concept")
|
|
266
|
+
|
|
267
|
+
return Index(
|
|
268
|
+
name=name,
|
|
269
|
+
tutorials=tutorials,
|
|
270
|
+
how_to_guides=how_to_guides,
|
|
271
|
+
explanations=explanations,
|
|
272
|
+
concepts=concepts,
|
|
273
|
+
api_modules=module_names,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def render(self) -> str:
|
|
277
|
+
"""
|
|
278
|
+
Render the index.rst file.
|
|
279
|
+
"""
|
|
280
|
+
return "\n".join(self._iter_lines())
|
|
281
|
+
|
|
282
|
+
def _iter_lines(self) -> Iterator[str]:
|
|
283
|
+
yield f"Welcome to the {self.name} documentation!"
|
|
284
|
+
yield "=" * (len(self.name) + 30)
|
|
285
|
+
yield from [
|
|
286
|
+
".. mdinclude:: _readme.md",
|
|
287
|
+
"",
|
|
288
|
+
"",
|
|
289
|
+
"Table of contents",
|
|
290
|
+
"-----------------",
|
|
291
|
+
"",
|
|
292
|
+
".. toctree::",
|
|
293
|
+
" :maxdepth: 3",
|
|
294
|
+
"",
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
if self.tutorials:
|
|
298
|
+
yield f" {self.tutorials.stem}"
|
|
299
|
+
if self.how_to_guides:
|
|
300
|
+
yield f" {self.how_to_guides.stem}"
|
|
301
|
+
if self.explanations:
|
|
302
|
+
yield f" {self.explanations.stem}"
|
|
303
|
+
if self.concepts:
|
|
304
|
+
yield f" {self.concepts.stem}"
|
|
305
|
+
if self.api_modules:
|
|
306
|
+
yield " api/_index"
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@dataclass
|
|
310
|
+
class Conf:
|
|
311
|
+
"""
|
|
312
|
+
Information to build the conf.py file.
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
project: str | None = None
|
|
316
|
+
author: str | None = None
|
|
317
|
+
email: str | None = None
|
|
318
|
+
year: int | None = None
|
|
319
|
+
extensions: list[str] = field(default_factory=DEFAULT_EXTENSIONS.copy)
|
|
320
|
+
theme: str = "default"
|
|
321
|
+
extra_options: dict[str, Any] = field(default_factory=dict)
|
|
322
|
+
|
|
323
|
+
@staticmethod
|
|
324
|
+
def from_pyproject(
|
|
325
|
+
pyproject: PyProject,
|
|
326
|
+
/,
|
|
327
|
+
*,
|
|
328
|
+
theme: str,
|
|
329
|
+
author: str | None = None,
|
|
330
|
+
email: str | None = None,
|
|
331
|
+
year: int | None = None,
|
|
332
|
+
extensions: Iterable[str] = DEFAULT_EXTENSIONS,
|
|
333
|
+
root: Path | None = None,
|
|
334
|
+
) -> Conf:
|
|
335
|
+
"""
|
|
336
|
+
Create Conf object from a PyProject object.
|
|
337
|
+
"""
|
|
338
|
+
project = pyproject.name
|
|
339
|
+
extensions = list(extensions or [])
|
|
340
|
+
root = root or pyproject.root
|
|
341
|
+
|
|
342
|
+
# Extract author information from the pyproject.toml file
|
|
343
|
+
try:
|
|
344
|
+
author_data = pyproject.authors[0]
|
|
345
|
+
author = author or author_data["name"]
|
|
346
|
+
if not email:
|
|
347
|
+
email = author_data.get("email")
|
|
348
|
+
except (TypeError, IndexError): # empty authors list or invalid data
|
|
349
|
+
pass
|
|
350
|
+
|
|
351
|
+
# Read the year from the Copyright notice in the LICENSE file.
|
|
352
|
+
if (licence_file := Path(root / "LICENSE")).exists():
|
|
353
|
+
copyright = find_copyright(licence_file.read_text())
|
|
354
|
+
if year is None:
|
|
355
|
+
try:
|
|
356
|
+
year = int(copyright["year"])
|
|
357
|
+
except ValueError:
|
|
358
|
+
pass
|
|
359
|
+
if author is None:
|
|
360
|
+
author = copyright["author"]
|
|
361
|
+
|
|
362
|
+
return Conf(
|
|
363
|
+
project=project,
|
|
364
|
+
author=author,
|
|
365
|
+
email=email,
|
|
366
|
+
year=year,
|
|
367
|
+
extensions=extensions,
|
|
368
|
+
theme=theme,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def render(self) -> str:
|
|
372
|
+
return "\n".join(self._iter_lines())
|
|
373
|
+
|
|
374
|
+
def _iter_lines(self) -> Iterator[str]:
|
|
375
|
+
theme = SPHINX_THEME_ALIASES.get(self.theme, self.theme)
|
|
376
|
+
copyright = f"{self.year}, " if self.year else ""
|
|
377
|
+
copyright += self.author or "unknown author"
|
|
378
|
+
author = self.author or "unknown author"
|
|
379
|
+
if self.email:
|
|
380
|
+
author += f" <{self.email}>"
|
|
381
|
+
|
|
382
|
+
yield f"project = {self.project or 'unnamed project'!r}"
|
|
383
|
+
yield f"copyright = {copyright!r}"
|
|
384
|
+
yield f"author = {author!r}"
|
|
385
|
+
yield f"extensions = {self.extensions!r}"
|
|
386
|
+
yield "templates_path = ['_templates']"
|
|
387
|
+
yield f"html_theme = {theme!r}"
|
|
388
|
+
yield "html_static_path = ['_static']"
|
|
389
|
+
yield "exclude_patterns = ['_readme.md', 'requirements.txt']"
|
|
390
|
+
for key, value in sorted(self.extra_options.items()):
|
|
391
|
+
yield f"{key} = {value!r}"
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class Copyright(TypedDict):
|
|
395
|
+
year: int
|
|
396
|
+
author: str | None
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def find_copyright(src: str) -> Copyright:
|
|
400
|
+
"""
|
|
401
|
+
Find the copyright notice in the given source code.
|
|
402
|
+
"""
|
|
403
|
+
match = COPYRIGHT_RE.search(src)
|
|
404
|
+
if not match:
|
|
405
|
+
raise ValueError("Copyright notice not found")
|
|
406
|
+
return {
|
|
407
|
+
"year": int(match.group("year")),
|
|
408
|
+
"author": match.group("author"),
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def render_modules_index(modules: Iterable[Module]) -> str:
|
|
413
|
+
"""
|
|
414
|
+
Render the index.rst file for the API documentation.
|
|
415
|
+
"""
|
|
416
|
+
lines = [
|
|
417
|
+
"Modules",
|
|
418
|
+
"=======",
|
|
419
|
+
"",
|
|
420
|
+
".. toctree::",
|
|
421
|
+
" :maxdepth: 2",
|
|
422
|
+
" :caption: Contents:",
|
|
423
|
+
"",
|
|
424
|
+
]
|
|
425
|
+
for module in modules:
|
|
426
|
+
lines.append(f" {module.name}")
|
|
427
|
+
return "\n".join(lines)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def remove_md_title(src: str) -> str:
|
|
431
|
+
"""
|
|
432
|
+
Remove the title from the given markdown source code.
|
|
433
|
+
"""
|
|
434
|
+
lines = src.splitlines()
|
|
435
|
+
if not lines:
|
|
436
|
+
return src
|
|
437
|
+
|
|
438
|
+
# Remove the first line if it is a title
|
|
439
|
+
if lines[0].startswith("#"):
|
|
440
|
+
lines.pop(0)
|
|
441
|
+
# Remove the second line if it is a title underline
|
|
442
|
+
if lines and re.match(r"^=+$", lines[0]):
|
|
443
|
+
lines.pop(0)
|
|
444
|
+
|
|
445
|
+
return "\n".join(lines).lstrip("\n")
|