doc-zero 0.2.0__tar.gz → 0.2.2__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.2.0 → doc_zero-0.2.2}/PKG-INFO +8 -1
- {doc_zero-0.2.0 → doc_zero-0.2.2}/README.md +7 -0
- doc_zero-0.2.2/doc0/base.py +392 -0
- {doc_zero-0.2.0 → doc_zero-0.2.2}/doc0/cli.py +30 -26
- doc_zero-0.2.2/doc0/module.py +249 -0
- doc_zero-0.2.2/doc0/pyproject.py +274 -0
- doc_zero-0.2.2/doc0/readme.py +206 -0
- doc_zero-0.2.2/doc0/theme.py +64 -0
- doc_zero-0.2.2/doc0/tree.py +75 -0
- {doc_zero-0.2.0 → doc_zero-0.2.2}/doc0/util.py +1 -24
- {doc_zero-0.2.0 → doc_zero-0.2.2}/pyproject.toml +7 -2
- {doc_zero-0.2.0 → doc_zero-0.2.2}/pyproject.toml.orig +6 -2
- doc_zero-0.2.0/doc0/base.py +0 -451
- doc_zero-0.2.0/doc0/module.py +0 -182
- doc_zero-0.2.0/doc0/pyproject.py +0 -180
- {doc_zero-0.2.0 → doc_zero-0.2.2}/doc0/__init__.py +0 -0
- {doc_zero-0.2.0 → doc_zero-0.2.2}/doc0/__main__.py +0 -0
- {doc_zero-0.2.0 → doc_zero-0.2.2}/doc0/exports.py +0 -0
- {doc_zero-0.2.0 → doc_zero-0.2.2}/doc0/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: doc-zero
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Zero-configuration documentation generator for Python.
|
|
5
5
|
Author: Fábio Macêdo Mendes
|
|
6
6
|
Author-email: Fábio Macêdo Mendes <fabiomacedomendes@gmail.com>
|
|
@@ -29,6 +29,11 @@ Description-Content-Type: text/markdown
|
|
|
29
29
|
|
|
30
30
|
# doc-zero
|
|
31
31
|
|
|
32
|
+
[](https://pypi.org/project/doc-zero/)
|
|
33
|
+
[](https://github.com/fabiommendes/doc0/actions/workflows/ci.yml)
|
|
34
|
+
[](https://doc-zero.readthedocs.io/en/latest/)
|
|
35
|
+
[](https://coveralls.io/github/fabiommendes/doc0?branch=main)
|
|
36
|
+
|
|
32
37
|
**Doc-zero** streamlines the process of writing documentation for your project. It is
|
|
33
38
|
an opinionated and explicitly non-configurable tool that extracts information
|
|
34
39
|
from your Python codebase and generates nice documentation with minimal effort.
|
|
@@ -100,3 +105,5 @@ following conditions:
|
|
|
100
105
|
|
|
101
106
|
`doc-zero` only includes the public API in the generated documentation.
|
|
102
107
|
|
|
108
|
+
|
|
109
|
+
## Adding extra documentation
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# doc-zero
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/doc-zero/)
|
|
4
|
+
[](https://github.com/fabiommendes/doc0/actions/workflows/ci.yml)
|
|
5
|
+
[](https://doc-zero.readthedocs.io/en/latest/)
|
|
6
|
+
[](https://coveralls.io/github/fabiommendes/doc0?branch=main)
|
|
7
|
+
|
|
3
8
|
**Doc-zero** streamlines the process of writing documentation for your project. It is
|
|
4
9
|
an opinionated and explicitly non-configurable tool that extracts information
|
|
5
10
|
from your Python codebase and generates nice documentation with minimal effort.
|
|
@@ -71,3 +76,5 @@ following conditions:
|
|
|
71
76
|
|
|
72
77
|
`doc-zero` only includes the public API in the generated documentation.
|
|
73
78
|
|
|
79
|
+
|
|
80
|
+
## Adding extra documentation
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from importlib.metadata import version as module_version
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Iterable, Iterator, TypedDict
|
|
8
|
+
|
|
9
|
+
from .module import Module, find_public_modules
|
|
10
|
+
from .pyproject import PyProject
|
|
11
|
+
from .readme import readme_body
|
|
12
|
+
from .theme import resolve_theme
|
|
13
|
+
from .tree import DocFile, DocTree, WritePolicy
|
|
14
|
+
from .util import first_existing
|
|
15
|
+
|
|
16
|
+
type ModuleName = str
|
|
17
|
+
|
|
18
|
+
NOT_GIVEN: Any = object()
|
|
19
|
+
COPYRIGHT_RE = re.compile(
|
|
20
|
+
r"[cC]opyright\s+(?:\(c\)\s+)?(?P<year>\d+)\s*(:?,?\s+(?P<author>[^\n]+))?"
|
|
21
|
+
)
|
|
22
|
+
DEFAULT_EXTENSIONS = [
|
|
23
|
+
"sphinx.ext.autodoc",
|
|
24
|
+
"sphinx_mdinclude",
|
|
25
|
+
# "myst_parser",
|
|
26
|
+
]
|
|
27
|
+
README_PLACEHOLDER = (
|
|
28
|
+
"This is the documentation for {name}. "
|
|
29
|
+
"Please include a README.md file in the documentation root directory."
|
|
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
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class Doc0:
|
|
59
|
+
"""
|
|
60
|
+
The root type representing the documentation of your project.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
#: The pyproject.toml file for the project.
|
|
64
|
+
pyproject: PyProject
|
|
65
|
+
|
|
66
|
+
#: Base location for the documentation assets
|
|
67
|
+
doc_root: Path
|
|
68
|
+
|
|
69
|
+
#: The resolved Sphinx theme name to use for the documentation.
|
|
70
|
+
theme: str
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def load(
|
|
74
|
+
cls,
|
|
75
|
+
root: Path | None = None,
|
|
76
|
+
/,
|
|
77
|
+
*,
|
|
78
|
+
theme: str | None = None,
|
|
79
|
+
docs: str = "docs",
|
|
80
|
+
) -> Doc0:
|
|
81
|
+
"""
|
|
82
|
+
Load project in the given path.
|
|
83
|
+
|
|
84
|
+
``theme`` is the CLI-provided value, if any; the final theme is
|
|
85
|
+
resolved via ``doc0.theme.resolve_theme`` (CLI value, else
|
|
86
|
+
``[tool.doc-zero] theme`` in pyproject.toml, else the default).
|
|
87
|
+
"""
|
|
88
|
+
root = root or Path.cwd()
|
|
89
|
+
pyproject = PyProject(root=root)
|
|
90
|
+
resolved_theme = resolve_theme(theme, pyproject)
|
|
91
|
+
|
|
92
|
+
return Doc0(
|
|
93
|
+
doc_root=root / docs,
|
|
94
|
+
pyproject=pyproject,
|
|
95
|
+
theme=resolved_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
|
+
Generate the documentation content and write it to disk.
|
|
108
|
+
"""
|
|
109
|
+
self.generate().write()
|
|
110
|
+
|
|
111
|
+
def generate(self) -> DocTree:
|
|
112
|
+
"""
|
|
113
|
+
Compute the documentation tree as data, without writing anything to
|
|
114
|
+
disk (no directory is created, no file is read except the project's
|
|
115
|
+
inputs: pyproject.toml, README.md, LICENSE, and the source modules).
|
|
116
|
+
"""
|
|
117
|
+
pyproject = self.pyproject
|
|
118
|
+
root = self.root
|
|
119
|
+
doc_root = self.doc_root
|
|
120
|
+
|
|
121
|
+
public_modules = find_public_modules(pyproject.find_root_modules())
|
|
122
|
+
|
|
123
|
+
conf = Conf.from_pyproject(pyproject, theme=self.theme, license_text=_read_license(root))
|
|
124
|
+
index = Index(
|
|
125
|
+
name=pyproject.name,
|
|
126
|
+
tutorials=_select_diataxis_entry(doc_root, "tutorial"),
|
|
127
|
+
how_to_guides=_select_diataxis_entry(doc_root, "how-to-guide"),
|
|
128
|
+
user_guides=_select_diataxis_entry(doc_root, "user-guide"),
|
|
129
|
+
explanations=_select_diataxis_entry(doc_root, "explanation"),
|
|
130
|
+
concepts=_select_diataxis_entry(doc_root, "concept"),
|
|
131
|
+
api_modules=[module.name for module in public_modules],
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
files: dict[Path, DocFile] = {
|
|
135
|
+
doc_root / "conf.py": DocFile(conf.render()),
|
|
136
|
+
doc_root / "index.rst": DocFile(index.render()),
|
|
137
|
+
doc_root / "_readme.md": DocFile(_readme_content(root, pyproject.name)),
|
|
138
|
+
doc_root / "api" / "_index.rst": DocFile(render_modules_index(public_modules)),
|
|
139
|
+
}
|
|
140
|
+
for module in public_modules:
|
|
141
|
+
files[doc_root / "api" / f"{module.name}.rst"] = DocFile(module.render())
|
|
142
|
+
|
|
143
|
+
docs_rel = doc_root.relative_to(root).as_posix()
|
|
144
|
+
files[doc_root / "requirements.txt"] = DocFile(
|
|
145
|
+
f"doc-zero>={module_version('doc-zero')}", policy=WritePolicy.IF_MISSING
|
|
146
|
+
)
|
|
147
|
+
files[root / ".readthedocs.yml"] = DocFile(
|
|
148
|
+
READTHEDOCS_TEMPLATE.format(docs=docs_rel), policy=WritePolicy.IF_MISSING
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return DocTree(
|
|
152
|
+
files=files,
|
|
153
|
+
dirs=[doc_root / "_static"],
|
|
154
|
+
owned_dirs=[doc_root / "api"],
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def build(self) -> int:
|
|
158
|
+
"""
|
|
159
|
+
Build the documentation using sphinx.
|
|
160
|
+
|
|
161
|
+
Returns Sphinx's exit status: 0 on success.
|
|
162
|
+
"""
|
|
163
|
+
from sphinx.cmd.build import main
|
|
164
|
+
|
|
165
|
+
self.init()
|
|
166
|
+
return main([str(self.doc_root), str(self.root / "dist" / "docs")])
|
|
167
|
+
|
|
168
|
+
def serve(self) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Start the live server.
|
|
171
|
+
"""
|
|
172
|
+
from sphinx_autobuild.__main__ import main
|
|
173
|
+
|
|
174
|
+
self.init()
|
|
175
|
+
main([str(self.doc_root), str(self.root / "dist" / "docs")])
|
|
176
|
+
|
|
177
|
+
def test(self) -> None:
|
|
178
|
+
"""
|
|
179
|
+
Execute all doctests.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass
|
|
184
|
+
class Index:
|
|
185
|
+
"""
|
|
186
|
+
Content of the index.rst file.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
name: str
|
|
190
|
+
|
|
191
|
+
# It uses the framework described at https://diataxis.fr
|
|
192
|
+
tutorials: Path | None = None
|
|
193
|
+
how_to_guides: Path | None = None
|
|
194
|
+
explanations: Path | None = None
|
|
195
|
+
user_guides: Path | None = None
|
|
196
|
+
|
|
197
|
+
# Reference is concepts + api documentation
|
|
198
|
+
concepts: Path | None = None
|
|
199
|
+
api_modules: list[str] = field(default_factory=list)
|
|
200
|
+
|
|
201
|
+
def render(self) -> str:
|
|
202
|
+
"""
|
|
203
|
+
Render the index.rst file.
|
|
204
|
+
"""
|
|
205
|
+
return "\n".join(self._iter_lines())
|
|
206
|
+
|
|
207
|
+
def _iter_lines(self) -> Iterator[str]:
|
|
208
|
+
yield f"Welcome to the {self.name} documentation!"
|
|
209
|
+
yield "=" * (len(self.name) + 30)
|
|
210
|
+
yield from [
|
|
211
|
+
".. mdinclude:: _readme.md",
|
|
212
|
+
"",
|
|
213
|
+
"",
|
|
214
|
+
"Table of contents",
|
|
215
|
+
"-----------------",
|
|
216
|
+
"",
|
|
217
|
+
".. toctree::",
|
|
218
|
+
" :maxdepth: 3",
|
|
219
|
+
"",
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
if self.tutorials:
|
|
223
|
+
yield f" {self.tutorials.stem}"
|
|
224
|
+
if self.how_to_guides:
|
|
225
|
+
yield f" {self.how_to_guides.stem}"
|
|
226
|
+
if self.user_guides:
|
|
227
|
+
yield f" {self.user_guides.stem}"
|
|
228
|
+
if self.explanations:
|
|
229
|
+
yield f" {self.explanations.stem}"
|
|
230
|
+
if self.concepts:
|
|
231
|
+
yield f" {self.concepts.stem}"
|
|
232
|
+
if self.api_modules:
|
|
233
|
+
yield " api/_index"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass
|
|
237
|
+
class Conf:
|
|
238
|
+
"""
|
|
239
|
+
Information to build the conf.py file.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
project: str | None = None
|
|
243
|
+
author: str | None = None
|
|
244
|
+
email: str | None = None
|
|
245
|
+
year: int | None = None
|
|
246
|
+
extensions: list[str] = field(default_factory=DEFAULT_EXTENSIONS.copy)
|
|
247
|
+
theme: str = "default"
|
|
248
|
+
|
|
249
|
+
@staticmethod
|
|
250
|
+
def from_pyproject(
|
|
251
|
+
pyproject: PyProject,
|
|
252
|
+
/,
|
|
253
|
+
*,
|
|
254
|
+
theme: str,
|
|
255
|
+
license_text: str | None = None,
|
|
256
|
+
) -> Conf:
|
|
257
|
+
"""
|
|
258
|
+
Create a Conf object from a PyProject object and (optionally) the
|
|
259
|
+
text of the project's LICENSE file.
|
|
260
|
+
|
|
261
|
+
The first pyproject author's name/email are used, if any. The year
|
|
262
|
+
and, absent a pyproject author, the author name are taken from the
|
|
263
|
+
LICENSE's copyright notice, if one can be found. A LICENSE without a
|
|
264
|
+
recognizable copyright notice is not an error: the year is simply
|
|
265
|
+
omitted and the author falls back to "unknown author" at render
|
|
266
|
+
time.
|
|
267
|
+
"""
|
|
268
|
+
project = pyproject.name
|
|
269
|
+
author: str | None = None
|
|
270
|
+
email: str | None = None
|
|
271
|
+
year: int | None = None
|
|
272
|
+
|
|
273
|
+
# Extract author information from the pyproject.toml file
|
|
274
|
+
try:
|
|
275
|
+
author_data = pyproject.authors[0]
|
|
276
|
+
author = author_data["name"]
|
|
277
|
+
email = author_data.get("email")
|
|
278
|
+
except (TypeError, IndexError): # empty authors list or invalid data
|
|
279
|
+
pass
|
|
280
|
+
|
|
281
|
+
# Read the year (and, absent a pyproject author, the author) from
|
|
282
|
+
# the Copyright notice in the LICENSE file.
|
|
283
|
+
if license_text is not None:
|
|
284
|
+
try:
|
|
285
|
+
copyright = find_copyright(license_text)
|
|
286
|
+
year = int(copyright["year"])
|
|
287
|
+
if author is None:
|
|
288
|
+
author = copyright["author"]
|
|
289
|
+
except ValueError:
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
return Conf(
|
|
293
|
+
project=project,
|
|
294
|
+
author=author,
|
|
295
|
+
email=email,
|
|
296
|
+
year=year,
|
|
297
|
+
theme=theme,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def render(self) -> str:
|
|
301
|
+
return "\n".join(self._iter_lines())
|
|
302
|
+
|
|
303
|
+
def _iter_lines(self) -> Iterator[str]:
|
|
304
|
+
copyright = f"{self.year}, " if self.year else ""
|
|
305
|
+
copyright += self.author or "unknown author"
|
|
306
|
+
author = self.author or "unknown author"
|
|
307
|
+
if self.email:
|
|
308
|
+
author += f" <{self.email}>"
|
|
309
|
+
|
|
310
|
+
yield f"project = {self.project or 'unnamed project'!r}"
|
|
311
|
+
yield f"copyright = {copyright!r}"
|
|
312
|
+
yield f"author = {author!r}"
|
|
313
|
+
yield f"extensions = {self.extensions!r}"
|
|
314
|
+
yield "templates_path = ['_templates']"
|
|
315
|
+
yield f"html_theme = {self.theme!r}"
|
|
316
|
+
yield "html_static_path = ['_static']"
|
|
317
|
+
yield "exclude_patterns = ['_readme.md', 'requirements.txt']"
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class Copyright(TypedDict):
|
|
321
|
+
year: int
|
|
322
|
+
author: str | None
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def find_copyright(src: str) -> Copyright:
|
|
326
|
+
"""
|
|
327
|
+
Find the copyright notice in the given source code.
|
|
328
|
+
"""
|
|
329
|
+
match = COPYRIGHT_RE.search(src)
|
|
330
|
+
if not match:
|
|
331
|
+
raise ValueError("Copyright notice not found")
|
|
332
|
+
return {
|
|
333
|
+
"year": int(match.group("year")),
|
|
334
|
+
"author": match.group("author"),
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def render_modules_index(modules: Iterable[Module]) -> str:
|
|
339
|
+
"""
|
|
340
|
+
Render the index.rst file for the API documentation.
|
|
341
|
+
"""
|
|
342
|
+
lines = [
|
|
343
|
+
"Modules",
|
|
344
|
+
"=======",
|
|
345
|
+
"",
|
|
346
|
+
".. toctree::",
|
|
347
|
+
" :maxdepth: 2",
|
|
348
|
+
" :caption: Contents:",
|
|
349
|
+
"",
|
|
350
|
+
]
|
|
351
|
+
for module in modules:
|
|
352
|
+
lines.append(f" {module.name}")
|
|
353
|
+
return "\n".join(lines)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _select_diataxis_entry(doc_root: Path, name: str, plural: str | None = None) -> Path | None:
|
|
357
|
+
"""
|
|
358
|
+
Select the first existing diataxis entry for ``name`` under
|
|
359
|
+
``doc_root``: the plural directory, then ``<name>.rst``, then
|
|
360
|
+
``<name>.md``.
|
|
361
|
+
"""
|
|
362
|
+
plural = plural or f"{name}s"
|
|
363
|
+
return first_existing(
|
|
364
|
+
[
|
|
365
|
+
doc_root / plural,
|
|
366
|
+
doc_root / f"{name}.rst",
|
|
367
|
+
doc_root / f"{name}.md",
|
|
368
|
+
]
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _read_license(root: Path) -> str | None:
|
|
373
|
+
"""
|
|
374
|
+
Return the text of ``<root>/LICENSE``, or None if it doesn't exist.
|
|
375
|
+
"""
|
|
376
|
+
license_path = root / "LICENSE"
|
|
377
|
+
if license_path.exists():
|
|
378
|
+
return license_path.read_text()
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _readme_content(root: Path, project_name: str) -> str:
|
|
383
|
+
"""
|
|
384
|
+
Compute the content of ``_readme.md`` from ``<root>/README.md``.
|
|
385
|
+
|
|
386
|
+
If the README is missing, a placeholder mentioning the project name is
|
|
387
|
+
used. Otherwise, see ``readme_body`` for what is kept.
|
|
388
|
+
"""
|
|
389
|
+
readme_path = root / "README.md"
|
|
390
|
+
if not readme_path.exists():
|
|
391
|
+
return README_PLACEHOLDER.format(name=project_name)
|
|
392
|
+
return readme_body(readme_path.read_text())
|
|
@@ -10,7 +10,7 @@ from typing import Annotated, Any
|
|
|
10
10
|
import typer
|
|
11
11
|
|
|
12
12
|
from .base import Doc0
|
|
13
|
-
from .
|
|
13
|
+
from .theme import ThemeError
|
|
14
14
|
|
|
15
15
|
__all__ = [
|
|
16
16
|
"main",
|
|
@@ -26,6 +26,29 @@ app = typer.Typer(
|
|
|
26
26
|
no_args_is_help=True,
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
+
#: The `--theme` option shared by `build` and `serve`. Validation happens in
|
|
30
|
+
#: `Doc0.load` (via `doc0.theme.resolve_theme`), not here, so an invalid
|
|
31
|
+
#: value -- from the flag or from pyproject.toml -- is reported the same way.
|
|
32
|
+
ThemeOption = Annotated[str | None, typer.Option("--theme", help="Select the Sphinx theme")]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _load(theme: str | None) -> Doc0:
|
|
36
|
+
"""
|
|
37
|
+
Load the current project, converting an invalid theme into a clean
|
|
38
|
+
CLI usage error (exit code 2, no traceback) instead of a bare
|
|
39
|
+
ValueError.
|
|
40
|
+
|
|
41
|
+
``typer.BadParameter`` is used rather than ``click.UsageError``: this
|
|
42
|
+
typer version vendors its own click fork internally (``typer._click``)
|
|
43
|
+
and only recognizes exceptions from that fork, not from the standalone
|
|
44
|
+
``click`` package. ``typer.BadParameter`` is a public re-export of that
|
|
45
|
+
fork's ``UsageError`` subclass, so it's caught the same way.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
return Doc0.load(Path.cwd(), theme=theme)
|
|
49
|
+
except ThemeError as exc:
|
|
50
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
51
|
+
|
|
29
52
|
|
|
30
53
|
@app.command()
|
|
31
54
|
def test() -> None:
|
|
@@ -37,40 +60,21 @@ def test() -> None:
|
|
|
37
60
|
|
|
38
61
|
|
|
39
62
|
@app.command()
|
|
40
|
-
def build(
|
|
41
|
-
theme: Annotated[
|
|
42
|
-
str | None,
|
|
43
|
-
typer.Option(
|
|
44
|
-
...,
|
|
45
|
-
"--theme",
|
|
46
|
-
help="Select the Sphinx theme",
|
|
47
|
-
callback=maybe_map(validate_theme),
|
|
48
|
-
),
|
|
49
|
-
] = None,
|
|
50
|
-
) -> None:
|
|
63
|
+
def build(theme: ThemeOption = None) -> None:
|
|
51
64
|
"""
|
|
52
65
|
Build the documentation for the current project.
|
|
53
66
|
"""
|
|
54
|
-
doc =
|
|
55
|
-
doc.build()
|
|
67
|
+
doc = _load(theme)
|
|
68
|
+
if status := doc.build():
|
|
69
|
+
raise typer.Exit(status)
|
|
56
70
|
|
|
57
71
|
|
|
58
72
|
@app.command()
|
|
59
|
-
def serve(
|
|
60
|
-
theme: Annotated[
|
|
61
|
-
str | None,
|
|
62
|
-
typer.Option(
|
|
63
|
-
...,
|
|
64
|
-
"--theme",
|
|
65
|
-
help="Select the Sphinx theme",
|
|
66
|
-
callback=maybe_map(validate_theme),
|
|
67
|
-
),
|
|
68
|
-
] = None,
|
|
69
|
-
) -> None:
|
|
73
|
+
def serve(theme: ThemeOption = None) -> None:
|
|
70
74
|
"""
|
|
71
75
|
Serve the documentation in the live server.
|
|
72
76
|
"""
|
|
73
|
-
doc =
|
|
77
|
+
doc = _load(theme)
|
|
74
78
|
doc.serve()
|
|
75
79
|
|
|
76
80
|
|