Sphinx 8.1.2__py3-none-any.whl → 8.2.0rc1__py3-none-any.whl

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.

Potentially problematic release.


This version of Sphinx might be problematic. Click here for more details.

Files changed (193) hide show
  1. sphinx/__init__.py +8 -4
  2. sphinx/__main__.py +2 -0
  3. sphinx/_cli/__init__.py +2 -5
  4. sphinx/_cli/util/colour.py +34 -11
  5. sphinx/_cli/util/errors.py +128 -61
  6. sphinx/addnodes.py +51 -35
  7. sphinx/application.py +362 -230
  8. sphinx/builders/__init__.py +87 -64
  9. sphinx/builders/_epub_base.py +65 -56
  10. sphinx/builders/changes.py +17 -23
  11. sphinx/builders/dirhtml.py +8 -13
  12. sphinx/builders/epub3.py +70 -38
  13. sphinx/builders/gettext.py +93 -73
  14. sphinx/builders/html/__init__.py +240 -186
  15. sphinx/builders/html/_assets.py +9 -2
  16. sphinx/builders/html/_build_info.py +3 -0
  17. sphinx/builders/latex/__init__.py +64 -54
  18. sphinx/builders/latex/constants.py +14 -11
  19. sphinx/builders/latex/nodes.py +2 -0
  20. sphinx/builders/latex/theming.py +8 -9
  21. sphinx/builders/latex/transforms.py +7 -5
  22. sphinx/builders/linkcheck.py +193 -149
  23. sphinx/builders/manpage.py +17 -17
  24. sphinx/builders/singlehtml.py +28 -16
  25. sphinx/builders/texinfo.py +28 -21
  26. sphinx/builders/text.py +10 -15
  27. sphinx/builders/xml.py +10 -19
  28. sphinx/cmd/build.py +49 -119
  29. sphinx/cmd/make_mode.py +35 -31
  30. sphinx/cmd/quickstart.py +78 -62
  31. sphinx/config.py +265 -163
  32. sphinx/directives/__init__.py +51 -54
  33. sphinx/directives/admonitions.py +107 -0
  34. sphinx/directives/code.py +24 -19
  35. sphinx/directives/other.py +21 -42
  36. sphinx/directives/patches.py +28 -16
  37. sphinx/domains/__init__.py +54 -31
  38. sphinx/domains/_domains_container.py +22 -17
  39. sphinx/domains/_index.py +5 -8
  40. sphinx/domains/c/__init__.py +366 -245
  41. sphinx/domains/c/_ast.py +378 -256
  42. sphinx/domains/c/_ids.py +89 -31
  43. sphinx/domains/c/_parser.py +283 -214
  44. sphinx/domains/c/_symbol.py +269 -198
  45. sphinx/domains/changeset.py +39 -24
  46. sphinx/domains/citation.py +54 -24
  47. sphinx/domains/cpp/__init__.py +517 -362
  48. sphinx/domains/cpp/_ast.py +999 -682
  49. sphinx/domains/cpp/_ids.py +133 -65
  50. sphinx/domains/cpp/_parser.py +746 -588
  51. sphinx/domains/cpp/_symbol.py +692 -489
  52. sphinx/domains/index.py +10 -8
  53. sphinx/domains/javascript.py +152 -74
  54. sphinx/domains/math.py +48 -40
  55. sphinx/domains/python/__init__.py +402 -211
  56. sphinx/domains/python/_annotations.py +114 -57
  57. sphinx/domains/python/_object.py +151 -67
  58. sphinx/domains/rst.py +94 -49
  59. sphinx/domains/std/__init__.py +510 -249
  60. sphinx/environment/__init__.py +345 -61
  61. sphinx/environment/adapters/asset.py +7 -1
  62. sphinx/environment/adapters/indexentries.py +15 -20
  63. sphinx/environment/adapters/toctree.py +19 -9
  64. sphinx/environment/collectors/__init__.py +3 -1
  65. sphinx/environment/collectors/asset.py +18 -15
  66. sphinx/environment/collectors/dependencies.py +8 -10
  67. sphinx/environment/collectors/metadata.py +6 -4
  68. sphinx/environment/collectors/title.py +3 -1
  69. sphinx/environment/collectors/toctree.py +4 -4
  70. sphinx/errors.py +1 -3
  71. sphinx/events.py +4 -4
  72. sphinx/ext/apidoc/__init__.py +21 -0
  73. sphinx/ext/apidoc/__main__.py +9 -0
  74. sphinx/ext/apidoc/_cli.py +356 -0
  75. sphinx/ext/apidoc/_generate.py +356 -0
  76. sphinx/ext/apidoc/_shared.py +66 -0
  77. sphinx/ext/autodoc/__init__.py +837 -483
  78. sphinx/ext/autodoc/directive.py +57 -21
  79. sphinx/ext/autodoc/importer.py +184 -67
  80. sphinx/ext/autodoc/mock.py +25 -10
  81. sphinx/ext/autodoc/preserve_defaults.py +17 -9
  82. sphinx/ext/autodoc/type_comment.py +56 -29
  83. sphinx/ext/autodoc/typehints.py +49 -26
  84. sphinx/ext/autosectionlabel.py +28 -11
  85. sphinx/ext/autosummary/__init__.py +271 -143
  86. sphinx/ext/autosummary/generate.py +121 -51
  87. sphinx/ext/coverage.py +152 -91
  88. sphinx/ext/doctest.py +169 -101
  89. sphinx/ext/duration.py +12 -6
  90. sphinx/ext/extlinks.py +33 -21
  91. sphinx/ext/githubpages.py +8 -8
  92. sphinx/ext/graphviz.py +175 -109
  93. sphinx/ext/ifconfig.py +11 -6
  94. sphinx/ext/imgconverter.py +48 -25
  95. sphinx/ext/imgmath.py +127 -97
  96. sphinx/ext/inheritance_diagram.py +177 -103
  97. sphinx/ext/intersphinx/__init__.py +22 -13
  98. sphinx/ext/intersphinx/__main__.py +3 -1
  99. sphinx/ext/intersphinx/_cli.py +18 -14
  100. sphinx/ext/intersphinx/_load.py +91 -82
  101. sphinx/ext/intersphinx/_resolve.py +108 -74
  102. sphinx/ext/intersphinx/_shared.py +2 -2
  103. sphinx/ext/linkcode.py +28 -12
  104. sphinx/ext/mathjax.py +60 -29
  105. sphinx/ext/napoleon/__init__.py +19 -7
  106. sphinx/ext/napoleon/docstring.py +229 -231
  107. sphinx/ext/todo.py +44 -49
  108. sphinx/ext/viewcode.py +105 -57
  109. sphinx/extension.py +3 -1
  110. sphinx/highlighting.py +13 -7
  111. sphinx/io.py +9 -13
  112. sphinx/jinja2glue.py +29 -26
  113. sphinx/locale/__init__.py +8 -9
  114. sphinx/parsers.py +8 -7
  115. sphinx/project.py +2 -2
  116. sphinx/pycode/__init__.py +31 -21
  117. sphinx/pycode/ast.py +6 -3
  118. sphinx/pycode/parser.py +14 -8
  119. sphinx/pygments_styles.py +4 -5
  120. sphinx/registry.py +192 -92
  121. sphinx/roles.py +58 -7
  122. sphinx/search/__init__.py +75 -54
  123. sphinx/search/en.py +11 -13
  124. sphinx/search/fi.py +1 -1
  125. sphinx/search/ja.py +8 -6
  126. sphinx/search/nl.py +1 -1
  127. sphinx/search/zh.py +19 -21
  128. sphinx/testing/fixtures.py +26 -29
  129. sphinx/testing/path.py +26 -62
  130. sphinx/testing/restructuredtext.py +14 -8
  131. sphinx/testing/util.py +21 -19
  132. sphinx/texinputs/make.bat.jinja +50 -50
  133. sphinx/texinputs/sphinx.sty +4 -3
  134. sphinx/texinputs/sphinxlatexadmonitions.sty +1 -1
  135. sphinx/texinputs/sphinxlatexobjects.sty +29 -10
  136. sphinx/themes/basic/static/searchtools.js +8 -5
  137. sphinx/theming.py +49 -61
  138. sphinx/transforms/__init__.py +17 -38
  139. sphinx/transforms/compact_bullet_list.py +5 -3
  140. sphinx/transforms/i18n.py +8 -21
  141. sphinx/transforms/post_transforms/__init__.py +142 -93
  142. sphinx/transforms/post_transforms/code.py +5 -5
  143. sphinx/transforms/post_transforms/images.py +28 -24
  144. sphinx/transforms/references.py +3 -1
  145. sphinx/util/__init__.py +109 -60
  146. sphinx/util/_files.py +39 -23
  147. sphinx/util/_importer.py +4 -1
  148. sphinx/util/_inventory_file_reader.py +76 -0
  149. sphinx/util/_io.py +2 -2
  150. sphinx/util/_lines.py +6 -3
  151. sphinx/util/_pathlib.py +40 -2
  152. sphinx/util/build_phase.py +2 -0
  153. sphinx/util/cfamily.py +19 -14
  154. sphinx/util/console.py +44 -179
  155. sphinx/util/display.py +9 -10
  156. sphinx/util/docfields.py +140 -122
  157. sphinx/util/docstrings.py +1 -1
  158. sphinx/util/docutils.py +118 -77
  159. sphinx/util/fileutil.py +25 -26
  160. sphinx/util/http_date.py +2 -0
  161. sphinx/util/i18n.py +77 -64
  162. sphinx/util/images.py +8 -6
  163. sphinx/util/inspect.py +147 -38
  164. sphinx/util/inventory.py +215 -116
  165. sphinx/util/logging.py +33 -33
  166. sphinx/util/matching.py +12 -4
  167. sphinx/util/nodes.py +18 -13
  168. sphinx/util/osutil.py +38 -39
  169. sphinx/util/parallel.py +22 -13
  170. sphinx/util/parsing.py +2 -1
  171. sphinx/util/png.py +6 -2
  172. sphinx/util/requests.py +33 -2
  173. sphinx/util/rst.py +3 -2
  174. sphinx/util/tags.py +1 -1
  175. sphinx/util/template.py +18 -10
  176. sphinx/util/texescape.py +8 -6
  177. sphinx/util/typing.py +148 -122
  178. sphinx/versioning.py +3 -3
  179. sphinx/writers/html.py +3 -1
  180. sphinx/writers/html5.py +61 -50
  181. sphinx/writers/latex.py +80 -65
  182. sphinx/writers/manpage.py +19 -38
  183. sphinx/writers/texinfo.py +44 -45
  184. sphinx/writers/text.py +48 -30
  185. sphinx/writers/xml.py +11 -8
  186. {sphinx-8.1.2.dist-info → sphinx-8.2.0rc1.dist-info}/LICENSE.rst +1 -1
  187. {sphinx-8.1.2.dist-info → sphinx-8.2.0rc1.dist-info}/METADATA +23 -15
  188. {sphinx-8.1.2.dist-info → sphinx-8.2.0rc1.dist-info}/RECORD +190 -186
  189. {sphinx-8.1.2.dist-info → sphinx-8.2.0rc1.dist-info}/WHEEL +1 -1
  190. sphinx/builders/html/transforms.py +0 -90
  191. sphinx/ext/apidoc.py +0 -721
  192. sphinx/util/exceptions.py +0 -74
  193. {sphinx-8.1.2.dist-info → sphinx-8.2.0rc1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,356 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import fnmatch
5
+ import locale
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING
10
+
11
+ import sphinx.locale
12
+ from sphinx import __display_version__
13
+ from sphinx.cmd.quickstart import EXTENSIONS
14
+ from sphinx.ext.apidoc._generate import create_modules_toc_file, recurse_tree
15
+ from sphinx.ext.apidoc._shared import LOGGER, ApidocOptions, _remove_old_files
16
+ from sphinx.locale import __
17
+ from sphinx.util.osutil import ensuredir
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Sequence
21
+ from typing import Any
22
+
23
+
24
+ def get_parser() -> argparse.ArgumentParser:
25
+ parser = argparse.ArgumentParser(
26
+ usage='%(prog)s [OPTIONS] -o <OUTPUT_PATH> <MODULE_PATH> [EXCLUDE_PATTERN, ...]',
27
+ epilog=__('For more information, visit <https://www.sphinx-doc.org/>.'),
28
+ description=__("""
29
+ Look recursively in <MODULE_PATH> for Python modules and packages and create
30
+ one reST file with automodule directives per package in the <OUTPUT_PATH>.
31
+
32
+ The <EXCLUDE_PATTERN>s can be file and/or directory patterns that will be
33
+ excluded from generation.
34
+
35
+ Note: By default this script will not overwrite already created files."""),
36
+ )
37
+
38
+ parser.add_argument(
39
+ '--version',
40
+ action='version',
41
+ dest='show_version',
42
+ version=f'%(prog)s {__display_version__}',
43
+ )
44
+
45
+ parser.add_argument('module_path', help=__('path to module to document'))
46
+ parser.add_argument(
47
+ 'exclude_pattern',
48
+ nargs='*',
49
+ help=__(
50
+ 'fnmatch-style file and/or directory patterns to exclude from generation'
51
+ ),
52
+ )
53
+
54
+ parser.add_argument(
55
+ '-o',
56
+ '--output-dir',
57
+ action='store',
58
+ dest='destdir',
59
+ required=True,
60
+ help=__('directory to place all output'),
61
+ )
62
+ parser.add_argument(
63
+ '-q',
64
+ action='store_true',
65
+ dest='quiet',
66
+ help=__('no output on stdout, just warnings on stderr'),
67
+ )
68
+ parser.add_argument(
69
+ '-d',
70
+ '--maxdepth',
71
+ action='store',
72
+ dest='maxdepth',
73
+ type=int,
74
+ default=4,
75
+ help=__('maximum depth of submodules to show in the TOC (default: 4)'),
76
+ )
77
+ parser.add_argument(
78
+ '-f',
79
+ '--force',
80
+ action='store_true',
81
+ dest='force',
82
+ help=__('overwrite existing files'),
83
+ )
84
+ parser.add_argument(
85
+ '-l',
86
+ '--follow-links',
87
+ action='store_true',
88
+ dest='followlinks',
89
+ default=False,
90
+ help=__(
91
+ 'follow symbolic links. Powerful when combined with collective.recipe.omelette.'
92
+ ),
93
+ )
94
+ parser.add_argument(
95
+ '-n',
96
+ '--dry-run',
97
+ action='store_true',
98
+ dest='dryrun',
99
+ help=__('run the script without creating files'),
100
+ )
101
+ parser.add_argument(
102
+ '-e',
103
+ '--separate',
104
+ action='store_true',
105
+ dest='separatemodules',
106
+ help=__('put documentation for each module on its own page'),
107
+ )
108
+ parser.add_argument(
109
+ '-P',
110
+ '--private',
111
+ action='store_true',
112
+ dest='includeprivate',
113
+ help=__('include "_private" modules'),
114
+ )
115
+ parser.add_argument(
116
+ '--tocfile',
117
+ action='store',
118
+ dest='tocfile',
119
+ default='modules',
120
+ help=__('filename of table of contents (default: modules)'),
121
+ )
122
+ parser.add_argument(
123
+ '-T',
124
+ '--no-toc',
125
+ action='store_false',
126
+ dest='tocfile',
127
+ help=__("don't create a table of contents file"),
128
+ )
129
+ parser.add_argument(
130
+ '-E',
131
+ '--no-headings',
132
+ action='store_true',
133
+ dest='noheadings',
134
+ help=__(
135
+ "don't create headings for the module/package "
136
+ 'packages (e.g. when the docstrings already '
137
+ 'contain them)'
138
+ ),
139
+ )
140
+ parser.add_argument(
141
+ '-M',
142
+ '--module-first',
143
+ action='store_true',
144
+ dest='modulefirst',
145
+ help=__('put module documentation before submodule documentation'),
146
+ )
147
+ parser.add_argument(
148
+ '--implicit-namespaces',
149
+ action='store_true',
150
+ dest='implicit_namespaces',
151
+ help=__(
152
+ 'interpret module paths according to PEP-0420 implicit namespaces specification'
153
+ ),
154
+ )
155
+ parser.add_argument(
156
+ '--automodule-options',
157
+ dest='automodule_options',
158
+ default='',
159
+ help=__(
160
+ 'Comma-separated list of options to pass to automodule directive '
161
+ '(or use SPHINX_APIDOC_OPTIONS).'
162
+ ),
163
+ )
164
+ parser.add_argument(
165
+ '-s',
166
+ '--suffix',
167
+ action='store',
168
+ dest='suffix',
169
+ default='rst',
170
+ help=__('file suffix (default: rst)'),
171
+ )
172
+ exclusive_group = parser.add_mutually_exclusive_group()
173
+ exclusive_group.add_argument(
174
+ '--remove-old',
175
+ action='store_true',
176
+ dest='remove_old',
177
+ help=__(
178
+ 'Remove existing files in the output directory that were not generated'
179
+ ),
180
+ )
181
+ exclusive_group.add_argument(
182
+ '-F',
183
+ '--full',
184
+ action='store_true',
185
+ dest='full',
186
+ help=__('generate a full project with sphinx-quickstart'),
187
+ )
188
+ parser.add_argument(
189
+ '-a',
190
+ '--append-syspath',
191
+ action='store_true',
192
+ dest='append_syspath',
193
+ help=__('append module_path to sys.path, used when --full is given'),
194
+ )
195
+ parser.add_argument(
196
+ '-H',
197
+ '--doc-project',
198
+ action='store',
199
+ dest='header',
200
+ help=__('project name (default: root module name)'),
201
+ )
202
+ parser.add_argument(
203
+ '-A',
204
+ '--doc-author',
205
+ action='store',
206
+ dest='author',
207
+ help=__('project author(s), used when --full is given'),
208
+ )
209
+ parser.add_argument(
210
+ '-V',
211
+ '--doc-version',
212
+ action='store',
213
+ dest='version',
214
+ help=__('project version, used when --full is given'),
215
+ )
216
+ parser.add_argument(
217
+ '-R',
218
+ '--doc-release',
219
+ action='store',
220
+ dest='release',
221
+ help=__(
222
+ 'project release, used when --full is given, defaults to --doc-version'
223
+ ),
224
+ )
225
+
226
+ group = parser.add_argument_group(__('extension options'))
227
+ group.add_argument(
228
+ '--extensions',
229
+ metavar='EXTENSIONS',
230
+ dest='extensions',
231
+ action='append',
232
+ help=__('enable arbitrary extensions, used when --full is given'),
233
+ )
234
+ for ext in EXTENSIONS:
235
+ group.add_argument(
236
+ f'--ext-{ext}',
237
+ action='append_const',
238
+ const=f'sphinx.ext.{ext}',
239
+ dest='extensions',
240
+ help=__('enable %s extension, used when --full is given') % ext,
241
+ )
242
+
243
+ group = parser.add_argument_group(__('Project templating'))
244
+ group.add_argument(
245
+ '-t',
246
+ '--templatedir',
247
+ metavar='TEMPLATEDIR',
248
+ dest='templatedir',
249
+ help=__('template directory for template files'),
250
+ )
251
+
252
+ return parser
253
+
254
+
255
+ def main(argv: Sequence[str] = (), /) -> int:
256
+ """Run the apidoc CLI."""
257
+ locale.setlocale(locale.LC_ALL, '')
258
+ sphinx.locale.init_console()
259
+
260
+ opts = _parse_args(argv)
261
+ rootpath = opts.module_path
262
+ excludes = tuple(
263
+ re.compile(fnmatch.translate(str(Path(exclude).resolve())))
264
+ for exclude in dict.fromkeys(opts.exclude_pattern)
265
+ )
266
+
267
+ written_files, modules = recurse_tree(rootpath, excludes, opts, opts.templatedir)
268
+
269
+ if opts.full:
270
+ _full_quickstart(opts, modules=modules)
271
+ elif opts.tocfile:
272
+ written_files.append(
273
+ create_modules_toc_file(modules, opts, opts.tocfile, opts.templatedir)
274
+ )
275
+
276
+ if opts.remove_old and not opts.dryrun:
277
+ _remove_old_files(written_files, opts.destdir, opts.suffix)
278
+
279
+ return 0
280
+
281
+
282
+ def _parse_args(argv: Sequence[str], /) -> ApidocOptions:
283
+ parser = get_parser()
284
+ args = parser.parse_args(argv or sys.argv[1:])
285
+
286
+ # normalise options
287
+
288
+ args.module_path = root_path = Path(args.module_path).resolve()
289
+ args.destdir = Path(args.destdir)
290
+ if not root_path.is_dir():
291
+ LOGGER.error(__('%s is not a directory.'), root_path)
292
+ raise SystemExit(1)
293
+
294
+ if args.header is None:
295
+ args.header = root_path.name
296
+ args.suffix = args.suffix.removeprefix('.')
297
+
298
+ if not args.dryrun:
299
+ ensuredir(args.destdir)
300
+
301
+ if not args.automodule_options:
302
+ args.automodule_options = set()
303
+ elif isinstance(args.automodule_options, str):
304
+ args.automodule_options = set(args.automodule_options.split(','))
305
+
306
+ return ApidocOptions(**args.__dict__)
307
+
308
+
309
+ def _full_quickstart(opts: ApidocOptions, /, *, modules: list[str]) -> None:
310
+ from sphinx.cmd import quickstart as qs
311
+
312
+ modules.sort()
313
+ prev_module = ''
314
+ text = ''
315
+ for module in modules:
316
+ if module.startswith(prev_module + '.'):
317
+ continue
318
+ prev_module = module
319
+ text += f' {module}\n'
320
+ d: dict[str, Any] = {
321
+ 'path': str(opts.destdir),
322
+ 'sep': False,
323
+ 'dot': '_',
324
+ 'project': opts.header,
325
+ 'author': opts.author or 'Author',
326
+ 'version': opts.version or '',
327
+ 'release': opts.release or opts.version or '',
328
+ 'suffix': '.' + opts.suffix,
329
+ 'master': 'index',
330
+ 'epub': True,
331
+ 'extensions': [
332
+ 'sphinx.ext.autodoc',
333
+ 'sphinx.ext.viewcode',
334
+ 'sphinx.ext.todo',
335
+ ],
336
+ 'makefile': True,
337
+ 'batchfile': True,
338
+ 'make_mode': True,
339
+ 'mastertocmaxdepth': opts.maxdepth,
340
+ 'mastertoctree': text,
341
+ 'language': 'en',
342
+ 'module_path': str(opts.module_path),
343
+ 'append_syspath': opts.append_syspath,
344
+ }
345
+ if opts.extensions:
346
+ d['extensions'].extend(opts.extensions)
347
+ if opts.quiet:
348
+ d['quiet'] = True
349
+
350
+ for ext in d['extensions'][:]:
351
+ if ',' in ext:
352
+ d['extensions'].remove(ext)
353
+ d['extensions'].extend(ext.split(','))
354
+
355
+ if not opts.dryrun:
356
+ qs.generate(d, silent=True, overwrite=opts.force, templatedir=opts.templatedir)
@@ -0,0 +1,356 @@
1
+ from __future__ import annotations
2
+
3
+ import glob
4
+ import os
5
+ import os.path
6
+ from importlib.machinery import EXTENSION_SUFFIXES
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ from sphinx import package_dir
11
+ from sphinx.ext.apidoc._shared import LOGGER
12
+ from sphinx.locale import __
13
+ from sphinx.util.osutil import FileAvoidWrite
14
+ from sphinx.util.template import ReSTRenderer
15
+
16
+ if TYPE_CHECKING:
17
+ import re
18
+ from collections.abc import Iterable, Iterator, Sequence
19
+
20
+ from sphinx.ext.apidoc._shared import ApidocOptions
21
+
22
+
23
+ # automodule options
24
+ if 'SPHINX_APIDOC_OPTIONS' in os.environ:
25
+ OPTIONS = set(os.environ['SPHINX_APIDOC_OPTIONS'].split(','))
26
+ else:
27
+ OPTIONS = {
28
+ 'members',
29
+ 'undoc-members',
30
+ # 'inherited-members', # disabled because there's a bug in sphinx
31
+ 'show-inheritance',
32
+ }
33
+
34
+ PY_SUFFIXES = ('.py', '.pyx', *EXTENSION_SUFFIXES)
35
+
36
+ template_dir = package_dir.joinpath('templates', 'apidoc')
37
+
38
+
39
+ def is_initpy(filename: str | Path) -> bool:
40
+ """Check *filename* is __init__ file or not."""
41
+ basename = Path(filename).name
42
+ return any(
43
+ basename == '__init__' + suffix
44
+ for suffix in sorted(PY_SUFFIXES, key=len, reverse=True)
45
+ )
46
+
47
+
48
+ def module_join(*modnames: str | None) -> str:
49
+ """Join module names with dots."""
50
+ return '.'.join(filter(None, modnames))
51
+
52
+
53
+ def is_package_dir(
54
+ files: Iterable[Path | str] = (), *, dir_path: Path | None = None
55
+ ) -> bool:
56
+ """Check given *files* contains __init__ file."""
57
+ if files != ():
58
+ return any(map(is_initpy, files))
59
+ if dir_path is not None:
60
+ return any(map(is_initpy, dir_path.iterdir()))
61
+ return False
62
+
63
+
64
+ def write_file(name: str, text: str, opts: ApidocOptions) -> Path:
65
+ """Write the output file for module/package <name>."""
66
+ fname = Path(opts.destdir, f'{name}.{opts.suffix}')
67
+ if opts.dryrun:
68
+ if not opts.quiet:
69
+ LOGGER.info(__('Would create file %s.'), fname)
70
+ return fname
71
+ if not opts.force and fname.is_file():
72
+ if not opts.quiet:
73
+ LOGGER.info(__('File %s already exists, skipping.'), fname)
74
+ else:
75
+ if not opts.quiet:
76
+ LOGGER.info(__('Creating file %s.'), fname)
77
+ with FileAvoidWrite(fname) as f:
78
+ f.write(text)
79
+ return fname
80
+
81
+
82
+ def create_module_file(
83
+ package: str | None,
84
+ basename: str,
85
+ opts: ApidocOptions,
86
+ user_template_dir: str | os.PathLike[str] | None = None,
87
+ ) -> Path:
88
+ """Build the text of the file and write the file."""
89
+ options = set(OPTIONS if not opts.automodule_options else opts.automodule_options)
90
+ if opts.includeprivate:
91
+ options.add('private-members')
92
+
93
+ qualname = module_join(package, basename)
94
+ context = {
95
+ 'show_headings': not opts.noheadings,
96
+ 'basename': basename,
97
+ 'qualname': qualname,
98
+ 'automodule_options': sorted(options),
99
+ }
100
+ template_path: Sequence[str | os.PathLike[str]]
101
+ if user_template_dir is not None:
102
+ template_path = [user_template_dir, template_dir]
103
+ else:
104
+ template_path = [template_dir]
105
+ text = ReSTRenderer(template_path).render('module.rst.jinja', context)
106
+ return write_file(qualname, text, opts)
107
+
108
+
109
+ def create_package_file(
110
+ root: str,
111
+ master_package: str | None,
112
+ subroot: str,
113
+ py_files: list[str],
114
+ opts: ApidocOptions,
115
+ subs: list[str],
116
+ is_namespace: bool,
117
+ excludes: Sequence[re.Pattern[str]] = (),
118
+ user_template_dir: str | os.PathLike[str] | None = None,
119
+ ) -> list[Path]:
120
+ """Build the text of the file and write the file.
121
+
122
+ Also create submodules if necessary.
123
+
124
+ :returns: list of written files
125
+ """
126
+ # build a list of sub packages (directories containing an __init__ file)
127
+ subpackages = [
128
+ module_join(master_package, subroot, pkgname)
129
+ for pkgname in subs
130
+ if not is_skipped_package(Path(root, pkgname), opts, excludes)
131
+ ]
132
+ # build a list of sub modules
133
+ submodules = [
134
+ sub.split('.')[0]
135
+ for sub in py_files
136
+ if not is_skipped_module(Path(root, sub), opts, excludes) and not is_initpy(sub)
137
+ ]
138
+ submodules = sorted(set(submodules))
139
+ submodules = [
140
+ module_join(master_package, subroot, modname) for modname in submodules
141
+ ]
142
+ options = OPTIONS.copy()
143
+ if opts.includeprivate:
144
+ options.add('private-members')
145
+
146
+ pkgname = module_join(master_package, subroot)
147
+ context = {
148
+ 'pkgname': pkgname,
149
+ 'subpackages': subpackages,
150
+ 'submodules': submodules,
151
+ 'is_namespace': is_namespace,
152
+ 'modulefirst': opts.modulefirst,
153
+ 'separatemodules': opts.separatemodules,
154
+ 'automodule_options': sorted(options),
155
+ 'show_headings': not opts.noheadings,
156
+ 'maxdepth': opts.maxdepth,
157
+ }
158
+ if user_template_dir is not None:
159
+ template_path = [user_template_dir, template_dir]
160
+ else:
161
+ template_path = [template_dir]
162
+
163
+ written: list[Path] = []
164
+
165
+ text = ReSTRenderer(template_path).render('package.rst.jinja', context)
166
+ written.append(write_file(pkgname, text, opts))
167
+
168
+ if submodules and opts.separatemodules:
169
+ written.extend([
170
+ create_module_file(None, submodule, opts, user_template_dir)
171
+ for submodule in submodules
172
+ ])
173
+
174
+ return written
175
+
176
+
177
+ def create_modules_toc_file(
178
+ modules: list[str],
179
+ opts: ApidocOptions,
180
+ name: str = 'modules',
181
+ user_template_dir: str | os.PathLike[str] | None = None,
182
+ ) -> Path:
183
+ """Create the module's index."""
184
+ modules.sort()
185
+ prev_module = ''
186
+ for module in modules.copy():
187
+ # look if the module is a subpackage and, if yes, ignore it
188
+ if module.startswith(prev_module + '.'):
189
+ modules.remove(module)
190
+ else:
191
+ prev_module = module
192
+
193
+ context = {
194
+ 'header': opts.header,
195
+ 'maxdepth': opts.maxdepth,
196
+ 'docnames': modules,
197
+ }
198
+ template_path: Sequence[str | os.PathLike[str]]
199
+ if user_template_dir is not None:
200
+ template_path = [user_template_dir, template_dir]
201
+ else:
202
+ template_path = [template_dir]
203
+ text = ReSTRenderer(template_path).render('toc.rst.jinja', context)
204
+ return write_file(name, text, opts)
205
+
206
+
207
+ def is_skipped_package(
208
+ dirname: str | Path, opts: ApidocOptions, excludes: Sequence[re.Pattern[str]] = ()
209
+ ) -> bool:
210
+ """Check if we want to skip this module."""
211
+ if not Path(dirname).is_dir():
212
+ return False
213
+
214
+ files = glob.glob(str(Path(dirname, '*.py'))) # NoQA: PTH207
215
+ regular_package = any(f for f in files if is_initpy(f))
216
+ if not regular_package and not opts.implicit_namespaces:
217
+ # *dirname* is not both a regular package and an implicit namespace package
218
+ return True
219
+
220
+ # Check there is some showable module inside package
221
+ return all(is_excluded(Path(dirname, f), excludes) for f in files)
222
+
223
+
224
+ def is_skipped_module(
225
+ filename: str | Path, opts: ApidocOptions, _excludes: Sequence[re.Pattern[str]]
226
+ ) -> bool:
227
+ """Check if we want to skip this module."""
228
+ filename = Path(filename)
229
+ if not filename.exists():
230
+ # skip if the file doesn't exist
231
+ return True
232
+ # skip if the module has a "private" name
233
+ return filename.name.startswith('_') and not opts.includeprivate
234
+
235
+
236
+ def walk(
237
+ root_path: str | Path,
238
+ excludes: Sequence[re.Pattern[str]],
239
+ opts: ApidocOptions,
240
+ ) -> Iterator[tuple[str, list[str], list[str]]]:
241
+ """Walk through the directory and list files and subdirectories up."""
242
+ for root, subs, files in os.walk(root_path, followlinks=opts.followlinks):
243
+ # document only Python module files (that aren't excluded)
244
+ files = sorted(
245
+ f
246
+ for f in files
247
+ if f.endswith(PY_SUFFIXES) and not is_excluded(Path(root, f), excludes)
248
+ )
249
+
250
+ # remove hidden ('.') and private ('_') directories, as well as
251
+ # excluded dirs
252
+ if opts.includeprivate:
253
+ exclude_prefixes: tuple[str, ...] = ('.',)
254
+ else:
255
+ exclude_prefixes = ('.', '_')
256
+
257
+ subs[:] = sorted(
258
+ sub
259
+ for sub in subs
260
+ if not sub.startswith(exclude_prefixes)
261
+ and not is_excluded(Path(root, sub), excludes)
262
+ )
263
+
264
+ yield root, subs, files
265
+
266
+
267
+ def has_child_module(
268
+ root_path: str | Path, excludes: Sequence[re.Pattern[str]], opts: ApidocOptions
269
+ ) -> bool:
270
+ """Check the given directory contains child module/s (at least one)."""
271
+ return any(files for _root, _subs, files in walk(root_path, excludes, opts))
272
+
273
+
274
+ def recurse_tree(
275
+ root_path: str | os.PathLike[str],
276
+ excludes: Sequence[re.Pattern[str]],
277
+ opts: ApidocOptions,
278
+ user_template_dir: str | os.PathLike[str] | None = None,
279
+ ) -> tuple[list[Path], list[str]]:
280
+ """Look for every file in the directory tree and create the corresponding
281
+ ReST files.
282
+ """
283
+ # check if the base directory is a package and get its name
284
+ root_path = Path(root_path)
285
+ if is_package_dir(dir_path=root_path) or opts.implicit_namespaces:
286
+ root_package = root_path.name
287
+ else:
288
+ # otherwise, the base is a directory with packages
289
+ root_package = None
290
+
291
+ toplevels = []
292
+ written_files = []
293
+ for root, subs, files in walk(root_path, excludes, opts):
294
+ is_pkg = is_package_dir(files)
295
+ is_namespace = not is_pkg and opts.implicit_namespaces
296
+ if is_pkg:
297
+ for f in files.copy():
298
+ if is_initpy(f):
299
+ files.remove(f)
300
+ files.insert(0, f)
301
+ elif root != str(root_path):
302
+ # only accept non-package at toplevel unless using implicit namespaces
303
+ if not opts.implicit_namespaces:
304
+ subs.clear()
305
+ continue
306
+
307
+ if is_pkg or is_namespace:
308
+ # we are in a package with something to document
309
+ if subs or len(files) > 1 or not is_skipped_package(root, opts):
310
+ subpackage = (
311
+ root.removeprefix(str(root_path))
312
+ .lstrip(os.path.sep)
313
+ .replace(os.path.sep, '.')
314
+ )
315
+ # if this is not a namespace or
316
+ # a namespace and there is something there to document
317
+ if not is_namespace or has_child_module(root, excludes, opts):
318
+ written_files.extend(
319
+ create_package_file(
320
+ root,
321
+ root_package,
322
+ subpackage,
323
+ files,
324
+ opts,
325
+ subs,
326
+ is_namespace,
327
+ excludes,
328
+ user_template_dir,
329
+ )
330
+ )
331
+ toplevels.append(module_join(root_package, subpackage))
332
+ else:
333
+ # if we are at the root level, we don't require it to be a package
334
+ assert root == str(root_path)
335
+ assert root_package is None
336
+ for py_file in files:
337
+ if not is_skipped_module(Path(root_path, py_file), opts, excludes):
338
+ module = py_file.split('.')[0]
339
+ written_files.append(
340
+ create_module_file(
341
+ root_package, module, opts, user_template_dir
342
+ )
343
+ )
344
+ toplevels.append(module)
345
+
346
+ return written_files, toplevels
347
+
348
+
349
+ def is_excluded(root: str | Path, excludes: Sequence[re.Pattern[str]]) -> bool:
350
+ """Check if the directory is in the exclude list.
351
+
352
+ Note: by having trailing slashes, we avoid common prefix issues, like
353
+ e.g. an exclude "foo" also accidentally excluding "foobar".
354
+ """
355
+ root_str = str(root)
356
+ return any(exclude.match(root_str) for exclude in excludes)