Sphinx 8.2.0rc1__py3-none-any.whl → 8.2.1__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 (164) hide show
  1. sphinx/__init__.py +2 -2
  2. sphinx/application.py +3 -3
  3. sphinx/domains/math.py +2 -0
  4. sphinx/domains/python/_annotations.py +26 -10
  5. sphinx/domains/python/_object.py +4 -1
  6. sphinx/ext/apidoc/__init__.py +46 -1
  7. sphinx/ext/apidoc/_cli.py +25 -25
  8. sphinx/ext/apidoc/_extension.py +262 -0
  9. sphinx/ext/apidoc/_generate.py +14 -14
  10. sphinx/ext/apidoc/_shared.py +51 -18
  11. sphinx/ext/autodoc/__init__.py +3 -3
  12. sphinx/ext/autodoc/directive.py +1 -1
  13. sphinx/ext/autodoc/mock.py +1 -1
  14. sphinx/ext/autosummary/__init__.py +11 -0
  15. sphinx/ext/napoleon/__init__.py +23 -24
  16. sphinx/ext/viewcode.py +12 -8
  17. sphinx/highlighting.py +1 -1
  18. sphinx/locale/ar/LC_MESSAGES/sphinx.mo +0 -0
  19. sphinx/locale/ar/LC_MESSAGES/sphinx.po +2155 -2050
  20. sphinx/locale/bg/LC_MESSAGES/sphinx.mo +0 -0
  21. sphinx/locale/bg/LC_MESSAGES/sphinx.po +2045 -1940
  22. sphinx/locale/bn/LC_MESSAGES/sphinx.mo +0 -0
  23. sphinx/locale/bn/LC_MESSAGES/sphinx.po +2175 -2070
  24. sphinx/locale/ca/LC_MESSAGES/sphinx.js +3 -3
  25. sphinx/locale/ca/LC_MESSAGES/sphinx.mo +0 -0
  26. sphinx/locale/ca/LC_MESSAGES/sphinx.po +2690 -2585
  27. sphinx/locale/ca@valencia/LC_MESSAGES/sphinx.js +63 -0
  28. sphinx/locale/ca@valencia/LC_MESSAGES/sphinx.mo +0 -0
  29. sphinx/locale/ca@valencia/LC_MESSAGES/sphinx.po +4216 -0
  30. sphinx/locale/cak/LC_MESSAGES/sphinx.mo +0 -0
  31. sphinx/locale/cak/LC_MESSAGES/sphinx.po +2096 -1991
  32. sphinx/locale/cs/LC_MESSAGES/sphinx.mo +0 -0
  33. sphinx/locale/cs/LC_MESSAGES/sphinx.po +2248 -2143
  34. sphinx/locale/cy/LC_MESSAGES/sphinx.mo +0 -0
  35. sphinx/locale/cy/LC_MESSAGES/sphinx.po +2201 -2096
  36. sphinx/locale/da/LC_MESSAGES/sphinx.mo +0 -0
  37. sphinx/locale/da/LC_MESSAGES/sphinx.po +2282 -2177
  38. sphinx/locale/de/LC_MESSAGES/sphinx.mo +0 -0
  39. sphinx/locale/de/LC_MESSAGES/sphinx.po +2261 -2156
  40. sphinx/locale/de_DE/LC_MESSAGES/sphinx.mo +0 -0
  41. sphinx/locale/de_DE/LC_MESSAGES/sphinx.po +2045 -1940
  42. sphinx/locale/el/LC_MESSAGES/sphinx.mo +0 -0
  43. sphinx/locale/el/LC_MESSAGES/sphinx.po +2604 -2499
  44. sphinx/locale/en_DE/LC_MESSAGES/sphinx.mo +0 -0
  45. sphinx/locale/en_DE/LC_MESSAGES/sphinx.po +2045 -1940
  46. sphinx/locale/en_FR/LC_MESSAGES/sphinx.mo +0 -0
  47. sphinx/locale/en_FR/LC_MESSAGES/sphinx.po +2045 -1940
  48. sphinx/locale/en_GB/LC_MESSAGES/sphinx.mo +0 -0
  49. sphinx/locale/en_GB/LC_MESSAGES/sphinx.po +2631 -2526
  50. sphinx/locale/en_HK/LC_MESSAGES/sphinx.mo +0 -0
  51. sphinx/locale/en_HK/LC_MESSAGES/sphinx.po +2045 -1940
  52. sphinx/locale/eo/LC_MESSAGES/sphinx.mo +0 -0
  53. sphinx/locale/eo/LC_MESSAGES/sphinx.po +2078 -1973
  54. sphinx/locale/es/LC_MESSAGES/sphinx.mo +0 -0
  55. sphinx/locale/es/LC_MESSAGES/sphinx.po +2633 -2528
  56. sphinx/locale/es_CO/LC_MESSAGES/sphinx.mo +0 -0
  57. sphinx/locale/es_CO/LC_MESSAGES/sphinx.po +2045 -1940
  58. sphinx/locale/et/LC_MESSAGES/sphinx.mo +0 -0
  59. sphinx/locale/et/LC_MESSAGES/sphinx.po +2449 -2344
  60. sphinx/locale/eu/LC_MESSAGES/sphinx.mo +0 -0
  61. sphinx/locale/eu/LC_MESSAGES/sphinx.po +2241 -2136
  62. sphinx/locale/fa/LC_MESSAGES/sphinx.mo +0 -0
  63. sphinx/locale/fa/LC_MESSAGES/sphinx.po +504 -500
  64. sphinx/locale/fi/LC_MESSAGES/sphinx.mo +0 -0
  65. sphinx/locale/fi/LC_MESSAGES/sphinx.po +499 -495
  66. sphinx/locale/fr/LC_MESSAGES/sphinx.mo +0 -0
  67. sphinx/locale/fr/LC_MESSAGES/sphinx.po +513 -509
  68. sphinx/locale/fr_FR/LC_MESSAGES/sphinx.mo +0 -0
  69. sphinx/locale/fr_FR/LC_MESSAGES/sphinx.po +499 -495
  70. sphinx/locale/gl/LC_MESSAGES/sphinx.mo +0 -0
  71. sphinx/locale/gl/LC_MESSAGES/sphinx.po +2644 -2539
  72. sphinx/locale/he/LC_MESSAGES/sphinx.mo +0 -0
  73. sphinx/locale/he/LC_MESSAGES/sphinx.po +499 -495
  74. sphinx/locale/hi/LC_MESSAGES/sphinx.mo +0 -0
  75. sphinx/locale/hi/LC_MESSAGES/sphinx.po +504 -500
  76. sphinx/locale/hi_IN/LC_MESSAGES/sphinx.mo +0 -0
  77. sphinx/locale/hi_IN/LC_MESSAGES/sphinx.po +499 -495
  78. sphinx/locale/hr/LC_MESSAGES/sphinx.mo +0 -0
  79. sphinx/locale/hr/LC_MESSAGES/sphinx.po +501 -497
  80. sphinx/locale/hu/LC_MESSAGES/sphinx.mo +0 -0
  81. sphinx/locale/hu/LC_MESSAGES/sphinx.po +499 -495
  82. sphinx/locale/id/LC_MESSAGES/sphinx.mo +0 -0
  83. sphinx/locale/id/LC_MESSAGES/sphinx.po +2609 -2504
  84. sphinx/locale/is/LC_MESSAGES/sphinx.mo +0 -0
  85. sphinx/locale/is/LC_MESSAGES/sphinx.po +499 -495
  86. sphinx/locale/it/LC_MESSAGES/sphinx.mo +0 -0
  87. sphinx/locale/it/LC_MESSAGES/sphinx.po +2265 -2160
  88. sphinx/locale/ja/LC_MESSAGES/sphinx.mo +0 -0
  89. sphinx/locale/ja/LC_MESSAGES/sphinx.po +2621 -2516
  90. sphinx/locale/ka/LC_MESSAGES/sphinx.mo +0 -0
  91. sphinx/locale/ka/LC_MESSAGES/sphinx.po +2567 -2462
  92. sphinx/locale/ko/LC_MESSAGES/sphinx.mo +0 -0
  93. sphinx/locale/ko/LC_MESSAGES/sphinx.po +2631 -2526
  94. sphinx/locale/lt/LC_MESSAGES/sphinx.mo +0 -0
  95. sphinx/locale/lt/LC_MESSAGES/sphinx.po +2214 -2109
  96. sphinx/locale/lv/LC_MESSAGES/sphinx.mo +0 -0
  97. sphinx/locale/lv/LC_MESSAGES/sphinx.po +2218 -2113
  98. sphinx/locale/mk/LC_MESSAGES/sphinx.mo +0 -0
  99. sphinx/locale/mk/LC_MESSAGES/sphinx.po +2088 -1983
  100. sphinx/locale/nb_NO/LC_MESSAGES/sphinx.mo +0 -0
  101. sphinx/locale/nb_NO/LC_MESSAGES/sphinx.po +2247 -2142
  102. sphinx/locale/ne/LC_MESSAGES/sphinx.mo +0 -0
  103. sphinx/locale/ne/LC_MESSAGES/sphinx.po +2227 -2122
  104. sphinx/locale/nl/LC_MESSAGES/sphinx.mo +0 -0
  105. sphinx/locale/nl/LC_MESSAGES/sphinx.po +2316 -2211
  106. sphinx/locale/pl/LC_MESSAGES/sphinx.js +2 -2
  107. sphinx/locale/pl/LC_MESSAGES/sphinx.mo +0 -0
  108. sphinx/locale/pl/LC_MESSAGES/sphinx.po +2442 -2336
  109. sphinx/locale/pt/LC_MESSAGES/sphinx.mo +0 -0
  110. sphinx/locale/pt/LC_MESSAGES/sphinx.po +2045 -1940
  111. sphinx/locale/pt_BR/LC_MESSAGES/sphinx.mo +0 -0
  112. sphinx/locale/pt_BR/LC_MESSAGES/sphinx.po +2657 -2552
  113. sphinx/locale/pt_PT/LC_MESSAGES/sphinx.mo +0 -0
  114. sphinx/locale/pt_PT/LC_MESSAGES/sphinx.po +2243 -2138
  115. sphinx/locale/ro/LC_MESSAGES/sphinx.mo +0 -0
  116. sphinx/locale/ro/LC_MESSAGES/sphinx.po +2244 -2139
  117. sphinx/locale/ru/LC_MESSAGES/sphinx.js +1 -1
  118. sphinx/locale/ru/LC_MESSAGES/sphinx.mo +0 -0
  119. sphinx/locale/ru/LC_MESSAGES/sphinx.po +2660 -2555
  120. sphinx/locale/si/LC_MESSAGES/sphinx.mo +0 -0
  121. sphinx/locale/si/LC_MESSAGES/sphinx.po +2134 -2029
  122. sphinx/locale/sk/LC_MESSAGES/sphinx.mo +0 -0
  123. sphinx/locale/sk/LC_MESSAGES/sphinx.po +2614 -2509
  124. sphinx/locale/sl/LC_MESSAGES/sphinx.mo +0 -0
  125. sphinx/locale/sl/LC_MESSAGES/sphinx.po +2167 -2062
  126. sphinx/locale/sphinx.pot +2069 -1964
  127. sphinx/locale/sq/LC_MESSAGES/sphinx.mo +0 -0
  128. sphinx/locale/sq/LC_MESSAGES/sphinx.po +2661 -2556
  129. sphinx/locale/sr/LC_MESSAGES/sphinx.mo +0 -0
  130. sphinx/locale/sr/LC_MESSAGES/sphinx.po +2213 -2108
  131. sphinx/locale/sv/LC_MESSAGES/sphinx.mo +0 -0
  132. sphinx/locale/sv/LC_MESSAGES/sphinx.po +2229 -2124
  133. sphinx/locale/te/LC_MESSAGES/sphinx.mo +0 -0
  134. sphinx/locale/te/LC_MESSAGES/sphinx.po +2045 -1940
  135. sphinx/locale/tr/LC_MESSAGES/sphinx.mo +0 -0
  136. sphinx/locale/tr/LC_MESSAGES/sphinx.po +2608 -2503
  137. sphinx/locale/uk_UA/LC_MESSAGES/sphinx.mo +0 -0
  138. sphinx/locale/uk_UA/LC_MESSAGES/sphinx.po +2167 -2062
  139. sphinx/locale/ur/LC_MESSAGES/sphinx.mo +0 -0
  140. sphinx/locale/ur/LC_MESSAGES/sphinx.po +2045 -1940
  141. sphinx/locale/vi/LC_MESSAGES/sphinx.mo +0 -0
  142. sphinx/locale/vi/LC_MESSAGES/sphinx.po +2204 -2099
  143. sphinx/locale/yue/LC_MESSAGES/sphinx.mo +0 -0
  144. sphinx/locale/yue/LC_MESSAGES/sphinx.po +2045 -1940
  145. sphinx/locale/zh_HK/LC_MESSAGES/sphinx.mo +0 -0
  146. sphinx/locale/zh_HK/LC_MESSAGES/sphinx.po +2045 -1940
  147. sphinx/locale/zh_TW/LC_MESSAGES/sphinx.mo +0 -0
  148. sphinx/locale/zh_TW/LC_MESSAGES/sphinx.po +2659 -2554
  149. sphinx/locale/zh_TW.Big5/LC_MESSAGES/sphinx.mo +0 -0
  150. sphinx/locale/zh_TW.Big5/LC_MESSAGES/sphinx.po +2045 -1940
  151. sphinx/testing/path.py +8 -0
  152. sphinx/themes/basic/static/basic.css.jinja +0 -8
  153. sphinx/util/_files.py +2 -2
  154. sphinx/util/fileutil.py +2 -1
  155. sphinx/util/osutil.py +6 -1
  156. sphinx/writers/html5.py +2 -2
  157. sphinx/writers/latex.py +3 -2
  158. sphinx/writers/texinfo.py +3 -2
  159. sphinx/writers/text.py +2 -2
  160. {sphinx-8.2.0rc1.dist-info → sphinx-8.2.1.dist-info}/METADATA +8 -5
  161. {sphinx-8.2.0rc1.dist-info → sphinx-8.2.1.dist-info}/RECORD +164 -160
  162. {sphinx-8.2.0rc1.dist-info → sphinx-8.2.1.dist-info}/WHEEL +1 -1
  163. {sphinx-8.2.0rc1.dist-info → sphinx-8.2.1.dist-info}/entry_points.txt +0 -0
  164. {sphinx-8.2.0rc1.dist-info → sphinx-8.2.1.dist-info/licenses}/LICENSE.rst +0 -0
sphinx/__init__.py CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = '8.2.0rc1'
5
+ __version__ = '8.2.1'
6
6
  __display_version__ = __version__ # used for command line version
7
7
 
8
8
  # Keep this file executable as-is in Python 3!
@@ -34,7 +34,7 @@ warnings.filterwarnings(
34
34
  #:
35
35
  #: .. versionadded:: 1.2
36
36
  #: Before version 1.2, check the string ``sphinx.__version__``.
37
- version_info = (8, 2, 0, 'candidate', 1)
37
+ version_info = (8, 2, 1, 'final', 0)
38
38
 
39
39
  package_dir = _StrPath(__file__).resolve().parent
40
40
 
sphinx/application.py CHANGED
@@ -1116,7 +1116,7 @@ class Sphinx:
1116
1116
  logger.debug('[app] adding directive: %r', (name, cls))
1117
1117
  if not override and docutils.is_directive_registered(name):
1118
1118
  logger.warning(
1119
- __('directive %r is already registered, it will be overridden'),
1119
+ __('directive %r is already registered and will not be overridden'),
1120
1120
  name,
1121
1121
  type='app',
1122
1122
  subtype='add_directive',
@@ -1142,7 +1142,7 @@ class Sphinx:
1142
1142
  logger.debug('[app] adding role: %r', (name, role))
1143
1143
  if not override and docutils.is_role_registered(name):
1144
1144
  logger.warning(
1145
- __('role %r is already registered, it will be overridden'),
1145
+ __('role %r is already registered and will not be overridden'),
1146
1146
  name,
1147
1147
  type='app',
1148
1148
  subtype='add_role',
@@ -1170,7 +1170,7 @@ class Sphinx:
1170
1170
  logger.debug('[app] adding generic role: %r', (name, nodeclass))
1171
1171
  if not override and docutils.is_role_registered(name):
1172
1172
  logger.warning(
1173
- __('role %r is already registered, it will be overridden'),
1173
+ __('role %r is already registered and will not be overridden'),
1174
1174
  name,
1175
1175
  type='app',
1176
1176
  subtype='add_generic_role',
sphinx/domains/math.py CHANGED
@@ -49,6 +49,8 @@ class MathDomain(Domain):
49
49
 
50
50
  initial_data: dict[str, Any] = {
51
51
  'objects': {}, # labelid -> (docname, eqno)
52
+ # backwards compatibility
53
+ 'has_equations': {}, # https://github.com/sphinx-doc/sphinx/issues/13346
52
54
  }
53
55
  dangling_warnings = {
54
56
  'eq': 'equation not found: %(target)s',
@@ -12,6 +12,7 @@ from docutils import nodes
12
12
 
13
13
  from sphinx import addnodes
14
14
  from sphinx.addnodes import pending_xref, pending_xref_condition
15
+ from sphinx.locale import _
15
16
  from sphinx.pycode.parser import Token, TokenProcessor
16
17
  from sphinx.util.inspect import signature_from_str
17
18
 
@@ -479,19 +480,13 @@ def _parse_arglist(
479
480
  last_kind = None
480
481
  for param in sig.parameters.values():
481
482
  if param.kind != param.POSITIONAL_ONLY and last_kind == param.POSITIONAL_ONLY:
482
- # PEP-570: Separator for Positional Only Parameter: /
483
- params += addnodes.desc_parameter(
484
- '', '', addnodes.desc_sig_operator('', '/')
485
- )
483
+ params += _positional_only_separator()
486
484
  if param.kind == param.KEYWORD_ONLY and last_kind in {
487
485
  param.POSITIONAL_OR_KEYWORD,
488
486
  param.POSITIONAL_ONLY,
489
487
  None,
490
488
  }:
491
- # PEP-3102: Separator for Keyword Only Parameter: *
492
- params += addnodes.desc_parameter(
493
- '', '', addnodes.desc_sig_operator('', '*')
494
- )
489
+ params += _keyword_only_separator()
495
490
 
496
491
  node = addnodes.desc_parameter()
497
492
  if param.kind == param.VAR_POSITIONAL:
@@ -523,12 +518,33 @@ def _parse_arglist(
523
518
  last_kind = param.kind
524
519
 
525
520
  if last_kind == Parameter.POSITIONAL_ONLY:
526
- # PEP-570: Separator for Positional Only Parameter: /
527
- params += addnodes.desc_parameter('', '', addnodes.desc_sig_operator('', '/'))
521
+ params += _positional_only_separator()
528
522
 
529
523
  return params
530
524
 
531
525
 
526
+ def _positional_only_separator() -> addnodes.desc_parameter:
527
+ # PEP 570: Separator for positional only parameters: /
528
+ positional_only_abbr = nodes.abbreviation(
529
+ '/', '/', explanation=_('Positional-only parameter separator (PEP 570)')
530
+ )
531
+ positional_only_op = addnodes.desc_sig_operator(
532
+ '/', '', positional_only_abbr, classes=['positional-only-separator']
533
+ )
534
+ return addnodes.desc_parameter('/', '', positional_only_op)
535
+
536
+
537
+ def _keyword_only_separator() -> addnodes.desc_parameter:
538
+ # PEP 3102: Separator for keyword only parameters: *
539
+ keyword_only_abbr = nodes.abbreviation(
540
+ '*', '*', explanation=_('Keyword-only parameters separator (PEP 3102)')
541
+ )
542
+ keyword_only_op = addnodes.desc_sig_operator(
543
+ '*', '', keyword_only_abbr, classes=['keyword-only-separator']
544
+ )
545
+ return addnodes.desc_parameter('*', '', keyword_only_op)
546
+
547
+
532
548
  def _pseudo_parse_arglist(
533
549
  signode: desc_signature,
534
550
  arglist: str,
@@ -352,11 +352,14 @@ class PyObject(ObjectDescription[tuple[str, str]]):
352
352
  multi_line_parameter_list,
353
353
  trailing_comma,
354
354
  )
355
- except SyntaxError:
355
+ except SyntaxError as exc:
356
356
  # fallback to parse arglist original parser
357
357
  # (this may happen if the argument list is incorrectly used
358
358
  # as a list of bases when documenting a class)
359
359
  # it supports to represent optional arguments (ex. "func(foo [, bar])")
360
+ logger.debug(
361
+ 'syntax error in arglist (%r): %s', arglist, exc, location=signode
362
+ )
360
363
  _pseudo_parse_arglist(
361
364
  signode,
362
365
  arglist,
@@ -13,9 +13,54 @@ from __future__ import annotations
13
13
 
14
14
  from typing import TYPE_CHECKING
15
15
 
16
+ import sphinx
16
17
  from sphinx.ext.apidoc._cli import main
17
18
 
18
19
  if TYPE_CHECKING:
19
20
  from collections.abc import Sequence
20
21
 
21
- __all__: Sequence[str] = ('main',)
22
+ from sphinx.application import Sphinx
23
+ from sphinx.util.typing import ExtensionMetadata
24
+
25
+ __all__: Sequence[str] = 'main', 'setup'
26
+
27
+
28
+ def setup(app: Sphinx) -> ExtensionMetadata:
29
+ from sphinx.ext.apidoc._extension import run_apidoc
30
+
31
+ # Require autodoc
32
+ app.setup_extension('sphinx.ext.autodoc')
33
+
34
+ # Configuration values
35
+ app.add_config_value(
36
+ 'apidoc_exclude_patterns', (), 'env', types=frozenset({list, tuple})
37
+ )
38
+ app.add_config_value('apidoc_max_depth', 4, 'env', types=frozenset({int}))
39
+ app.add_config_value('apidoc_follow_links', False, 'env', types=frozenset({bool}))
40
+ app.add_config_value(
41
+ 'apidoc_separate_modules', False, 'env', types=frozenset({bool})
42
+ )
43
+ app.add_config_value(
44
+ 'apidoc_include_private', False, 'env', types=frozenset({bool})
45
+ )
46
+ app.add_config_value('apidoc_no_headings', False, 'env', types=frozenset({bool}))
47
+ app.add_config_value('apidoc_module_first', False, 'env', types=frozenset({bool}))
48
+ app.add_config_value(
49
+ 'apidoc_implicit_namespaces', False, 'env', types=frozenset({bool})
50
+ )
51
+ app.add_config_value(
52
+ 'apidoc_automodule_options',
53
+ frozenset(('members', 'undoc-members', 'show-inheritance')),
54
+ 'env',
55
+ types=frozenset({frozenset, list, set, tuple}),
56
+ )
57
+ app.add_config_value('apidoc_modules', (), 'env', types=frozenset({list, tuple}))
58
+
59
+ # Entry point to run apidoc
60
+ app.connect('builder-inited', run_apidoc)
61
+
62
+ return {
63
+ 'version': sphinx.__display_version__,
64
+ 'parallel_read_safe': True,
65
+ 'parallel_write_safe': True,
66
+ }
sphinx/ext/apidoc/_cli.py CHANGED
@@ -55,7 +55,7 @@ Note: By default this script will not overwrite already created files."""),
55
55
  '-o',
56
56
  '--output-dir',
57
57
  action='store',
58
- dest='destdir',
58
+ dest='dest_dir',
59
59
  required=True,
60
60
  help=__('directory to place all output'),
61
61
  )
@@ -69,7 +69,7 @@ Note: By default this script will not overwrite already created files."""),
69
69
  '-d',
70
70
  '--maxdepth',
71
71
  action='store',
72
- dest='maxdepth',
72
+ dest='max_depth',
73
73
  type=int,
74
74
  default=4,
75
75
  help=__('maximum depth of submodules to show in the TOC (default: 4)'),
@@ -85,7 +85,7 @@ Note: By default this script will not overwrite already created files."""),
85
85
  '-l',
86
86
  '--follow-links',
87
87
  action='store_true',
88
- dest='followlinks',
88
+ dest='follow_links',
89
89
  default=False,
90
90
  help=__(
91
91
  'follow symbolic links. Powerful when combined with collective.recipe.omelette.'
@@ -95,27 +95,27 @@ Note: By default this script will not overwrite already created files."""),
95
95
  '-n',
96
96
  '--dry-run',
97
97
  action='store_true',
98
- dest='dryrun',
98
+ dest='dry_run',
99
99
  help=__('run the script without creating files'),
100
100
  )
101
101
  parser.add_argument(
102
102
  '-e',
103
103
  '--separate',
104
104
  action='store_true',
105
- dest='separatemodules',
105
+ dest='separate_modules',
106
106
  help=__('put documentation for each module on its own page'),
107
107
  )
108
108
  parser.add_argument(
109
109
  '-P',
110
110
  '--private',
111
111
  action='store_true',
112
- dest='includeprivate',
112
+ dest='include_private',
113
113
  help=__('include "_private" modules'),
114
114
  )
115
115
  parser.add_argument(
116
116
  '--tocfile',
117
117
  action='store',
118
- dest='tocfile',
118
+ dest='toc_file',
119
119
  default='modules',
120
120
  help=__('filename of table of contents (default: modules)'),
121
121
  )
@@ -123,14 +123,14 @@ Note: By default this script will not overwrite already created files."""),
123
123
  '-T',
124
124
  '--no-toc',
125
125
  action='store_false',
126
- dest='tocfile',
126
+ dest='toc_file',
127
127
  help=__("don't create a table of contents file"),
128
128
  )
129
129
  parser.add_argument(
130
130
  '-E',
131
131
  '--no-headings',
132
132
  action='store_true',
133
- dest='noheadings',
133
+ dest='no_headings',
134
134
  help=__(
135
135
  "don't create headings for the module/package "
136
136
  'packages (e.g. when the docstrings already '
@@ -141,7 +141,7 @@ Note: By default this script will not overwrite already created files."""),
141
141
  '-M',
142
142
  '--module-first',
143
143
  action='store_true',
144
- dest='modulefirst',
144
+ dest='module_first',
145
145
  help=__('put module documentation before submodule documentation'),
146
146
  )
147
147
  parser.add_argument(
@@ -245,7 +245,7 @@ Note: By default this script will not overwrite already created files."""),
245
245
  '-t',
246
246
  '--templatedir',
247
247
  metavar='TEMPLATEDIR',
248
- dest='templatedir',
248
+ dest='template_dir',
249
249
  help=__('template directory for template files'),
250
250
  )
251
251
 
@@ -264,17 +264,17 @@ def main(argv: Sequence[str] = (), /) -> int:
264
264
  for exclude in dict.fromkeys(opts.exclude_pattern)
265
265
  )
266
266
 
267
- written_files, modules = recurse_tree(rootpath, excludes, opts, opts.templatedir)
267
+ written_files, modules = recurse_tree(rootpath, excludes, opts, opts.template_dir)
268
268
 
269
269
  if opts.full:
270
270
  _full_quickstart(opts, modules=modules)
271
- elif opts.tocfile:
271
+ elif opts.toc_file:
272
272
  written_files.append(
273
- create_modules_toc_file(modules, opts, opts.tocfile, opts.templatedir)
273
+ create_modules_toc_file(modules, opts, opts.toc_file, opts.template_dir)
274
274
  )
275
275
 
276
- if opts.remove_old and not opts.dryrun:
277
- _remove_old_files(written_files, opts.destdir, opts.suffix)
276
+ if opts.remove_old and not opts.dry_run:
277
+ _remove_old_files(written_files, opts.dest_dir, opts.suffix)
278
278
 
279
279
  return 0
280
280
 
@@ -286,7 +286,7 @@ def _parse_args(argv: Sequence[str], /) -> ApidocOptions:
286
286
  # normalise options
287
287
 
288
288
  args.module_path = root_path = Path(args.module_path).resolve()
289
- args.destdir = Path(args.destdir)
289
+ args.dest_dir = Path(args.dest_dir)
290
290
  if not root_path.is_dir():
291
291
  LOGGER.error(__('%s is not a directory.'), root_path)
292
292
  raise SystemExit(1)
@@ -295,13 +295,13 @@ def _parse_args(argv: Sequence[str], /) -> ApidocOptions:
295
295
  args.header = root_path.name
296
296
  args.suffix = args.suffix.removeprefix('.')
297
297
 
298
- if not args.dryrun:
299
- ensuredir(args.destdir)
298
+ if not args.dry_run:
299
+ ensuredir(args.dest_dir)
300
300
 
301
301
  if not args.automodule_options:
302
- args.automodule_options = set()
302
+ args.automodule_options = frozenset()
303
303
  elif isinstance(args.automodule_options, str):
304
- args.automodule_options = set(args.automodule_options.split(','))
304
+ args.automodule_options = frozenset(args.automodule_options.split(','))
305
305
 
306
306
  return ApidocOptions(**args.__dict__)
307
307
 
@@ -318,7 +318,7 @@ def _full_quickstart(opts: ApidocOptions, /, *, modules: list[str]) -> None:
318
318
  prev_module = module
319
319
  text += f' {module}\n'
320
320
  d: dict[str, Any] = {
321
- 'path': str(opts.destdir),
321
+ 'path': str(opts.dest_dir),
322
322
  'sep': False,
323
323
  'dot': '_',
324
324
  'project': opts.header,
@@ -336,7 +336,7 @@ def _full_quickstart(opts: ApidocOptions, /, *, modules: list[str]) -> None:
336
336
  'makefile': True,
337
337
  'batchfile': True,
338
338
  'make_mode': True,
339
- 'mastertocmaxdepth': opts.maxdepth,
339
+ 'mastertocmaxdepth': opts.max_depth,
340
340
  'mastertoctree': text,
341
341
  'language': 'en',
342
342
  'module_path': str(opts.module_path),
@@ -352,5 +352,5 @@ def _full_quickstart(opts: ApidocOptions, /, *, modules: list[str]) -> None:
352
352
  d['extensions'].remove(ext)
353
353
  d['extensions'].extend(ext.split(','))
354
354
 
355
- if not opts.dryrun:
356
- qs.generate(d, silent=True, overwrite=opts.force, templatedir=opts.templatedir)
355
+ if not opts.dry_run:
356
+ qs.generate(d, silent=True, overwrite=opts.force, templatedir=opts.template_dir)
@@ -0,0 +1,262 @@
1
+ """Sphinx extension for auto-generating API documentation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import fnmatch
6
+ import re
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ from sphinx._cli.util.colour import bold
11
+ from sphinx.ext.apidoc._generate import create_modules_toc_file, recurse_tree
12
+ from sphinx.ext.apidoc._shared import (
13
+ LOGGER,
14
+ ApidocDefaults,
15
+ ApidocOptions,
16
+ _remove_old_files,
17
+ )
18
+ from sphinx.locale import __
19
+
20
+ if TYPE_CHECKING:
21
+ from collections.abc import Collection, Sequence
22
+ from typing import Any
23
+
24
+ from sphinx.application import Sphinx
25
+
26
+ _BOOL_KEYS = frozenset({
27
+ 'follow_links',
28
+ 'separate_modules',
29
+ 'include_private',
30
+ 'no_headings',
31
+ 'module_first',
32
+ 'implicit_namespaces',
33
+ })
34
+ _ALLOWED_KEYS = _BOOL_KEYS | frozenset({
35
+ 'path',
36
+ 'destination',
37
+ 'exclude_patterns',
38
+ 'automodule_options',
39
+ 'max_depth',
40
+ })
41
+
42
+
43
+ def run_apidoc(app: Sphinx) -> None:
44
+ """Run the apidoc extension."""
45
+ defaults = ApidocDefaults.from_config(app.config)
46
+ apidoc_modules: Sequence[dict[str, Any]] = app.config.apidoc_modules
47
+ srcdir: Path = app.srcdir
48
+ confdir: Path = app.confdir
49
+
50
+ LOGGER.info(bold(__('Running apidoc')))
51
+
52
+ module_options: dict[str, Any]
53
+ for i, module_options in enumerate(apidoc_modules):
54
+ _run_apidoc_module(
55
+ i,
56
+ options=module_options,
57
+ defaults=defaults,
58
+ srcdir=srcdir,
59
+ confdir=confdir,
60
+ )
61
+
62
+
63
+ def _run_apidoc_module(
64
+ i: int,
65
+ *,
66
+ options: dict[str, Any],
67
+ defaults: ApidocDefaults,
68
+ srcdir: Path,
69
+ confdir: Path,
70
+ ) -> None:
71
+ """Run apidoc for a single module."""
72
+ args = _parse_module_options(
73
+ i, options=options, defaults=defaults, srcdir=srcdir, confdir=confdir
74
+ )
75
+ if args is None:
76
+ return
77
+
78
+ exclude_patterns_compiled: list[re.Pattern[str]] = [
79
+ re.compile(fnmatch.translate(exclude)) for exclude in args.exclude_pattern
80
+ ]
81
+
82
+ written_files, modules = recurse_tree(
83
+ args.module_path, exclude_patterns_compiled, args, args.template_dir
84
+ )
85
+ if args.toc_file:
86
+ written_files.append(
87
+ create_modules_toc_file(modules, args, args.toc_file, args.template_dir)
88
+ )
89
+ if args.remove_old:
90
+ _remove_old_files(written_files, args.dest_dir, args.suffix)
91
+
92
+
93
+ def _parse_module_options(
94
+ i: int,
95
+ *,
96
+ options: dict[str, Any],
97
+ defaults: ApidocDefaults,
98
+ srcdir: Path,
99
+ confdir: Path,
100
+ ) -> ApidocOptions | None:
101
+ if not isinstance(options, dict):
102
+ LOGGER.warning(__('apidoc_modules item %i must be a dict'), i, type='apidoc')
103
+ return None
104
+
105
+ # module path should be absolute or relative to the conf directory
106
+ try:
107
+ path = Path(options['path'])
108
+ except KeyError:
109
+ LOGGER.warning(
110
+ __("apidoc_modules item %i must have a 'path' key"), i, type='apidoc'
111
+ )
112
+ return None
113
+ except TypeError:
114
+ LOGGER.warning(
115
+ __("apidoc_modules item %i 'path' must be a string"), i, type='apidoc'
116
+ )
117
+ return None
118
+ module_path = confdir / path
119
+ if not module_path.is_dir():
120
+ LOGGER.warning(
121
+ __("apidoc_modules item %i 'path' is not an existing folder: %s"),
122
+ i,
123
+ module_path,
124
+ type='apidoc',
125
+ )
126
+ return None
127
+
128
+ # destination path should be relative to the source directory
129
+ try:
130
+ destination = Path(options['destination'])
131
+ except KeyError:
132
+ LOGGER.warning(
133
+ __("apidoc_modules item %i must have a 'destination' key"),
134
+ i,
135
+ type='apidoc',
136
+ )
137
+ return None
138
+ except TypeError:
139
+ LOGGER.warning(
140
+ __("apidoc_modules item %i 'destination' must be a string"),
141
+ i,
142
+ type='apidoc',
143
+ )
144
+ return None
145
+ if destination.is_absolute():
146
+ LOGGER.warning(
147
+ __("apidoc_modules item %i 'destination' should be a relative path"),
148
+ i,
149
+ type='apidoc',
150
+ )
151
+ return None
152
+ dest_path = srcdir / destination
153
+ try:
154
+ dest_path.mkdir(parents=True, exist_ok=True)
155
+ except OSError as exc:
156
+ LOGGER.warning(
157
+ __('apidoc_modules item %i cannot create destination directory: %s'),
158
+ i,
159
+ exc.strerror,
160
+ type='apidoc',
161
+ )
162
+ return None
163
+
164
+ # exclude patterns should be absolute or relative to the conf directory
165
+ exclude_patterns: list[str] = [
166
+ str(confdir / pattern)
167
+ for pattern in _check_collection_of_strings(
168
+ i, options, key='exclude_patterns', default=defaults.exclude_patterns
169
+ )
170
+ ]
171
+
172
+ # TODO template_dir
173
+
174
+ max_depth = defaults.max_depth
175
+ if 'max_depth' in options:
176
+ if not isinstance(options['max_depth'], int):
177
+ LOGGER.warning(
178
+ __("apidoc_modules item %i '%s' must be an int"),
179
+ i,
180
+ 'max_depth',
181
+ type='apidoc',
182
+ )
183
+ else:
184
+ max_depth = options['max_depth']
185
+
186
+ bool_options: dict[str, bool] = {}
187
+ for key in sorted(_BOOL_KEYS):
188
+ if key not in options:
189
+ bool_options[key] = getattr(defaults, key)
190
+ elif not isinstance(options[key], bool):
191
+ LOGGER.warning(
192
+ __("apidoc_modules item %i '%s' must be a boolean"),
193
+ i,
194
+ key,
195
+ type='apidoc',
196
+ )
197
+ bool_options[key] = getattr(defaults, key)
198
+ else:
199
+ bool_options[key] = options[key]
200
+
201
+ # TODO per-module automodule_options
202
+ automodule_options = frozenset(
203
+ _check_collection_of_strings(
204
+ i, options, key='automodule_options', default=defaults.automodule_options
205
+ )
206
+ )
207
+
208
+ if diff := options.keys() - _ALLOWED_KEYS:
209
+ LOGGER.warning(
210
+ __('apidoc_modules item %i has unexpected keys: %s'),
211
+ i,
212
+ ', '.join(sorted(diff)),
213
+ type='apidoc',
214
+ )
215
+
216
+ return ApidocOptions(
217
+ dest_dir=dest_path,
218
+ module_path=module_path,
219
+ exclude_pattern=exclude_patterns,
220
+ automodule_options=automodule_options,
221
+ max_depth=max_depth,
222
+ quiet=True,
223
+ follow_links=bool_options['follow_links'],
224
+ separate_modules=bool_options['separate_modules'],
225
+ include_private=bool_options['include_private'],
226
+ no_headings=bool_options['no_headings'],
227
+ module_first=bool_options['module_first'],
228
+ implicit_namespaces=bool_options['implicit_namespaces'],
229
+ )
230
+
231
+
232
+ def _check_collection_of_strings(
233
+ index: int,
234
+ options: dict[str, Any],
235
+ *,
236
+ key: str,
237
+ default: Collection[str],
238
+ ) -> Collection[str]:
239
+ """Check that a key's value is a collection of strings in the options.
240
+
241
+ :returns: The value of the key, or None if invalid.
242
+ """
243
+ if key not in options:
244
+ return default
245
+ if not isinstance(options[key], list | tuple | set | frozenset):
246
+ LOGGER.warning(
247
+ __("apidoc_modules item %i '%s' must be a sequence"),
248
+ index,
249
+ key,
250
+ type='apidoc',
251
+ )
252
+ return default
253
+ for item in options[key]:
254
+ if not isinstance(item, str):
255
+ LOGGER.warning(
256
+ __("apidoc_modules item %i '%s' must contain strings"),
257
+ index,
258
+ key,
259
+ type='apidoc',
260
+ )
261
+ return default
262
+ return options[key]