Sphinx 8.1.3__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 +829 -480
  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.3.dist-info → sphinx-8.2.0rc1.dist-info}/LICENSE.rst +1 -1
  187. {sphinx-8.1.3.dist-info → sphinx-8.2.0rc1.dist-info}/METADATA +23 -15
  188. {sphinx-8.1.3.dist-info → sphinx-8.2.0rc1.dist-info}/RECORD +190 -186
  189. {sphinx-8.1.3.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.3.dist-info → sphinx-8.2.0rc1.dist-info}/entry_points.txt +0 -0
sphinx/config.py CHANGED
@@ -2,24 +2,19 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import sys
6
5
  import time
7
6
  import traceback
8
7
  import types
9
8
  import warnings
10
- from os import getenv, path
9
+ from contextlib import chdir
10
+ from os import getenv
11
+ from pathlib import Path
11
12
  from typing import TYPE_CHECKING, Any, Literal, NamedTuple
12
13
 
13
14
  from sphinx.deprecation import RemovedInSphinx90Warning
14
15
  from sphinx.errors import ConfigError, ExtensionError
15
16
  from sphinx.locale import _, __
16
17
  from sphinx.util import logging
17
- from sphinx.util.osutil import fs_encoding
18
-
19
- if sys.version_info >= (3, 11):
20
- from contextlib import chdir
21
- else:
22
- from sphinx.util.osutil import _chdir as chdir
23
18
 
24
19
  if TYPE_CHECKING:
25
20
  import os
@@ -34,7 +29,11 @@ if TYPE_CHECKING:
34
29
  logger = logging.getLogger(__name__)
35
30
 
36
31
  _ConfigRebuild: TypeAlias = Literal[
37
- '', 'env', 'epub', 'gettext', 'html',
32
+ '',
33
+ 'env',
34
+ 'epub',
35
+ 'gettext',
36
+ 'html',
38
37
  # sphinxcontrib-applehelp
39
38
  'applehelp',
40
39
  # sphinxcontrib-devhelp
@@ -84,16 +83,18 @@ class ENUM:
84
83
  """
85
84
 
86
85
  def __init__(self, *candidates: str | bool | None) -> None:
87
- self.candidates = candidates
86
+ self._candidates = frozenset(candidates)
88
87
 
89
- def match(self, value: str | list | tuple) -> bool:
90
- if isinstance(value, list | tuple):
91
- return all(item in self.candidates for item in value)
92
- else:
93
- return value in self.candidates
88
+ def __repr__(self) -> str:
89
+ return f'ENUM({", ".join(sorted(map(repr, self._candidates)))})'
90
+
91
+ def match(self, value: str | bool | None | Sequence[str | bool | None]) -> bool: # NoQA: RUF036
92
+ if isinstance(value, str | bool | None):
93
+ return value in self._candidates
94
+ return all(item in self._candidates for item in value)
94
95
 
95
96
 
96
- _OptValidTypes: TypeAlias = tuple[()] | tuple[type, ...] | frozenset[type] | ENUM
97
+ _OptValidTypes: TypeAlias = frozenset[type] | ENUM
97
98
 
98
99
 
99
100
  class _Opt:
@@ -135,15 +136,35 @@ class _Opt:
135
136
 
136
137
  def __eq__(self, other: object) -> bool:
137
138
  if isinstance(other, _Opt):
138
- self_tpl = (self.default, self.rebuild, self.valid_types, self.description)
139
- other_tpl = (other.default, other.rebuild, other.valid_types, self.description)
139
+ self_tpl = (
140
+ self.default,
141
+ self.rebuild,
142
+ self.valid_types,
143
+ self.description,
144
+ )
145
+ other_tpl = (
146
+ other.default,
147
+ other.rebuild,
148
+ other.valid_types,
149
+ other.description,
150
+ )
140
151
  return self_tpl == other_tpl
141
152
  return NotImplemented
142
153
 
143
154
  def __lt__(self, other: _Opt) -> bool:
144
155
  if self.__class__ is other.__class__:
145
- self_tpl = (self.default, self.rebuild, self.valid_types, self.description)
146
- other_tpl = (other.default, other.rebuild, other.valid_types, self.description)
156
+ self_tpl = (
157
+ self.default,
158
+ self.rebuild,
159
+ self.valid_types,
160
+ self.description,
161
+ )
162
+ other_tpl = (
163
+ other.default,
164
+ other.rebuild,
165
+ other.valid_types,
166
+ other.description,
167
+ )
147
168
  return self_tpl > other_tpl
148
169
  return NotImplemented
149
170
 
@@ -166,7 +187,8 @@ class _Opt:
166
187
  return self.default, self.rebuild, self.valid_types, self.description
167
188
 
168
189
  def __setstate__(
169
- self, state: tuple[Any, _ConfigRebuild, _OptValidTypes, str]) -> None:
190
+ self, state: tuple[Any, _ConfigRebuild, _OptValidTypes, str]
191
+ ) -> None:
170
192
  default, rebuild, valid_types, description = state
171
193
  super().__setattr__('default', default)
172
194
  super().__setattr__('rebuild', rebuild)
@@ -177,7 +199,9 @@ class _Opt:
177
199
  warnings.warn(
178
200
  f'The {self.__class__.__name__!r} object tuple interface is deprecated, '
179
201
  "use attribute access instead for 'default', 'rebuild', and 'valid_types'.",
180
- RemovedInSphinx90Warning, stacklevel=2)
202
+ RemovedInSphinx90Warning,
203
+ stacklevel=2,
204
+ )
181
205
  return (self.default, self.rebuild, self.valid_types)[item]
182
206
 
183
207
 
@@ -202,81 +226,93 @@ class Config:
202
226
 
203
227
  config_values: dict[str, _Opt] = {
204
228
  # general options
205
- 'project': _Opt('Project name not set', 'env', ()),
206
- 'author': _Opt('Author name not set', 'env', ()),
229
+ 'project': _Opt('Project name not set', 'env', frozenset((str,))),
230
+ 'author': _Opt('Author name not set', 'env', frozenset((str,))),
207
231
  'project_copyright': _Opt('', 'html', frozenset((str, tuple, list))),
208
232
  'copyright': _Opt(
209
- lambda config: config.project_copyright, 'html', frozenset((str, tuple, list))),
210
- 'version': _Opt('', 'env', ()),
211
- 'release': _Opt('', 'env', ()),
212
- 'today': _Opt('', 'env', ()),
233
+ lambda config: config.project_copyright,
234
+ 'html',
235
+ frozenset((str, tuple, list)),
236
+ ),
237
+ 'version': _Opt('', 'env', frozenset((str,))),
238
+ 'release': _Opt('', 'env', frozenset((str,))),
239
+ 'today': _Opt('', 'env', frozenset((str,))),
213
240
  # the real default is locale-dependent
214
241
  'today_fmt': _Opt(None, 'env', frozenset((str,))),
215
-
216
242
  'language': _Opt('en', 'env', frozenset((str,))),
217
- 'locale_dirs': _Opt(['locales'], 'env', ()),
218
- 'figure_language_filename': _Opt('{root}.{language}{ext}', 'env', frozenset((str,))),
219
- 'gettext_allow_fuzzy_translations': _Opt(False, 'gettext', ()),
243
+ 'locale_dirs': _Opt(['locales'], 'env', frozenset((list, tuple))),
244
+ 'figure_language_filename': _Opt(
245
+ '{root}.{language}{ext}', 'env', frozenset((str,))
246
+ ),
247
+ 'gettext_allow_fuzzy_translations': _Opt(False, 'gettext', frozenset((bool,))),
220
248
  'translation_progress_classes': _Opt(
221
- False, 'env', ENUM(True, False, 'translated', 'untranslated')),
222
-
223
- 'master_doc': _Opt('index', 'env', ()),
224
- 'root_doc': _Opt(lambda config: config.master_doc, 'env', ()),
249
+ False, 'env', ENUM(True, False, 'translated', 'untranslated')
250
+ ),
251
+ 'master_doc': _Opt('index', 'env', frozenset((str,))),
252
+ 'root_doc': _Opt(lambda config: config.master_doc, 'env', frozenset((str,))),
225
253
  # ``source_suffix`` type is actually ``dict[str, str | None]``:
226
254
  # see ``convert_source_suffix()`` below.
227
- 'source_suffix': _Opt(
228
- {'.rst': 'restructuredtext'}, 'env', Any), # type: ignore[arg-type]
229
- 'source_encoding': _Opt('utf-8-sig', 'env', ()),
255
+ 'source_suffix': _Opt({'.rst': 'restructuredtext'}, 'env', Any), # type: ignore[arg-type]
256
+ 'source_encoding': _Opt('utf-8-sig', 'env', frozenset((str,))),
230
257
  'exclude_patterns': _Opt([], 'env', frozenset((str,))),
231
- 'include_patterns': _Opt(["**"], 'env', frozenset((str,))),
258
+ 'include_patterns': _Opt(['**'], 'env', frozenset((str,))),
232
259
  'default_role': _Opt(None, 'env', frozenset((str,))),
233
- 'add_function_parentheses': _Opt(True, 'env', ()),
234
- 'add_module_names': _Opt(True, 'env', ()),
260
+ 'add_function_parentheses': _Opt(True, 'env', frozenset((bool,))),
261
+ 'add_module_names': _Opt(True, 'env', frozenset((bool,))),
235
262
  'toc_object_entries': _Opt(True, 'env', frozenset((bool,))),
236
263
  'toc_object_entries_show_parents': _Opt(
237
- 'domain', 'env', ENUM('domain', 'all', 'hide')),
238
- 'trim_footnote_reference_space': _Opt(False, 'env', ()),
239
- 'show_authors': _Opt(False, 'env', ()),
264
+ 'domain', 'env', ENUM('domain', 'all', 'hide')
265
+ ),
266
+ 'trim_footnote_reference_space': _Opt(False, 'env', frozenset((bool,))),
267
+ 'show_authors': _Opt(False, 'env', frozenset((bool,))),
240
268
  'pygments_style': _Opt(None, 'html', frozenset((str,))),
241
- 'highlight_language': _Opt('default', 'env', ()),
242
- 'highlight_options': _Opt({}, 'env', ()),
243
- 'templates_path': _Opt([], 'html', ()),
269
+ 'highlight_language': _Opt('default', 'env', frozenset((str,))),
270
+ 'highlight_options': _Opt({}, 'env', frozenset((dict,))),
271
+ 'templates_path': _Opt([], 'html', frozenset((list,))),
244
272
  'template_bridge': _Opt(None, 'html', frozenset((str,))),
245
- 'keep_warnings': _Opt(False, 'env', ()),
246
- 'suppress_warnings': _Opt([], 'env', ()),
273
+ 'keep_warnings': _Opt(False, 'env', frozenset((bool,))),
274
+ 'suppress_warnings': _Opt([], 'env', frozenset((list, tuple))),
247
275
  'show_warning_types': _Opt(True, 'env', frozenset((bool,))),
248
- 'modindex_common_prefix': _Opt([], 'html', ()),
276
+ 'modindex_common_prefix': _Opt([], 'html', frozenset((list, tuple))),
249
277
  'rst_epilog': _Opt(None, 'env', frozenset((str,))),
250
278
  'rst_prolog': _Opt(None, 'env', frozenset((str,))),
251
- 'trim_doctest_flags': _Opt(True, 'env', ()),
279
+ 'trim_doctest_flags': _Opt(True, 'env', frozenset((bool,))),
252
280
  'primary_domain': _Opt('py', 'env', frozenset((types.NoneType,))),
253
281
  'needs_sphinx': _Opt(None, '', frozenset((str,))),
254
- 'needs_extensions': _Opt({}, '', ()),
255
- 'manpages_url': _Opt(None, 'env', ()),
256
- 'nitpicky': _Opt(False, '', ()),
282
+ 'needs_extensions': _Opt({}, '', frozenset((dict,))),
283
+ 'manpages_url': _Opt(None, 'env', frozenset((str, types.NoneType))),
284
+ 'nitpicky': _Opt(False, '', frozenset((bool,))),
257
285
  'nitpick_ignore': _Opt([], '', frozenset((set, list, tuple))),
258
286
  'nitpick_ignore_regex': _Opt([], '', frozenset((set, list, tuple))),
259
- 'numfig': _Opt(False, 'env', ()),
260
- 'numfig_secnum_depth': _Opt(1, 'env', ()),
261
- 'numfig_format': _Opt({}, 'env', ()), # will be initialized in init_numfig_format()
287
+ 'numfig': _Opt(False, 'env', frozenset((bool,))),
288
+ 'numfig_secnum_depth': _Opt(1, 'env', frozenset((int, types.NoneType))),
289
+ # numfig_format will be initialized in init_numfig_format()
290
+ 'numfig_format': _Opt({}, 'env', frozenset((dict,))),
262
291
  'maximum_signature_line_length': _Opt(
263
- None, 'env', frozenset((int, types.NoneType))),
264
- 'math_number_all': _Opt(False, 'env', ()),
292
+ None, 'env', frozenset((int, types.NoneType))
293
+ ),
294
+ 'math_number_all': _Opt(False, 'env', frozenset((bool,))),
265
295
  'math_eqref_format': _Opt(None, 'env', frozenset((str,))),
266
- 'math_numfig': _Opt(True, 'env', ()),
296
+ 'math_numfig': _Opt(True, 'env', frozenset((bool,))),
267
297
  'math_numsep': _Opt('.', 'env', frozenset((str,))),
268
- 'tls_verify': _Opt(True, 'env', ()),
269
- 'tls_cacerts': _Opt(None, 'env', ()),
298
+ 'tls_verify': _Opt(True, 'env', frozenset((bool,))),
299
+ 'tls_cacerts': _Opt(None, 'env', frozenset((str, dict, types.NoneType))),
270
300
  'user_agent': _Opt(None, 'env', frozenset((str,))),
271
- 'smartquotes': _Opt(True, 'env', ()),
272
- 'smartquotes_action': _Opt('qDe', 'env', ()),
301
+ 'smartquotes': _Opt(True, 'env', frozenset((bool,))),
302
+ 'smartquotes_action': _Opt('qDe', 'env', frozenset((str,))),
273
303
  'smartquotes_excludes': _Opt(
274
- {'languages': ['ja', 'zh_CN', 'zh_TW'], 'builders': ['man', 'text']}, 'env', ()),
275
- 'option_emphasise_placeholders': _Opt(False, 'env', ()),
304
+ {'languages': ['ja', 'zh_CN', 'zh_TW'], 'builders': ['man', 'text']},
305
+ 'env',
306
+ frozenset((dict,)),
307
+ ),
308
+ 'option_emphasise_placeholders': _Opt(False, 'env', frozenset((bool,))),
276
309
  }
277
310
 
278
- def __init__(self, config: dict[str, Any] | None = None,
279
- overrides: dict[str, Any] | None = None) -> None:
311
+ def __init__(
312
+ self,
313
+ config: dict[str, Any] | None = None,
314
+ overrides: dict[str, Any] | None = None,
315
+ ) -> None:
280
316
  raw_config: dict[str, Any] = config or {}
281
317
  self._overrides = dict(overrides) if overrides is not None else {}
282
318
  self._options = Config.config_values.copy()
@@ -306,24 +342,33 @@ class Config:
306
342
  return self._overrides
307
343
 
308
344
  @classmethod
309
- def read(cls: type[Config], confdir: str | os.PathLike[str], overrides: dict | None = None,
310
- tags: Tags | None = None) -> Config:
345
+ def read(
346
+ cls: type[Config],
347
+ confdir: str | os.PathLike[str],
348
+ overrides: dict[str, Any] | None = None,
349
+ tags: Tags | None = None,
350
+ ) -> Config:
311
351
  """Create a Config object from configuration file."""
312
- filename = path.join(confdir, CONFIG_FILENAME)
313
- if not path.isfile(filename):
314
- raise ConfigError(__("config directory doesn't contain a conf.py file (%s)") %
315
- confdir)
352
+ filename = Path(confdir, CONFIG_FILENAME)
353
+ if not filename.is_file():
354
+ raise ConfigError(
355
+ __("config directory doesn't contain a conf.py file (%s)") % confdir
356
+ )
316
357
  namespace = eval_config_file(filename, tags)
317
358
 
318
359
  # Note: Old sphinx projects have been configured as "language = None" because
319
360
  # sphinx-quickstart previously generated this by default.
320
361
  # To keep compatibility, they should be fallback to 'en' for a while
321
362
  # (This conversion should not be removed before 2025-01-01).
322
- if namespace.get("language", ...) is None:
323
- logger.warning(__("Invalid configuration value found: 'language = None'. "
324
- "Update your configuration to a valid language code. "
325
- "Falling back to 'en' (English)."))
326
- namespace["language"] = "en"
363
+ if namespace.get('language', ...) is None:
364
+ logger.warning(
365
+ __(
366
+ "Invalid configuration value found: 'language = None'. "
367
+ 'Update your configuration to a valid language code. '
368
+ "Falling back to 'en' (English)."
369
+ )
370
+ )
371
+ namespace['language'] = 'en'
327
372
 
328
373
  return cls(namespace, overrides)
329
374
 
@@ -333,33 +378,47 @@ class Config:
333
378
  valid_types = opt.valid_types
334
379
  if valid_types == Any:
335
380
  return value
336
- if (type(default) is bool
337
- or (not isinstance(valid_types, ENUM)
338
- and len(valid_types) == 1 and bool in valid_types)):
339
- if isinstance(valid_types, ENUM) or len(valid_types) > 1:
340
- # if valid_types are given, and non-bool valid types exist,
341
- # return the value without coercing to a Boolean.
381
+ if isinstance(valid_types, ENUM):
382
+ if False in valid_types._candidates and value == '0':
383
+ return False
384
+ if True in valid_types._candidates and value == '1':
385
+ return True
386
+ return value
387
+ elif type(default) is bool or (bool in valid_types):
388
+ if value == '0':
389
+ return False
390
+ if value == '1':
391
+ return True
392
+ if len(valid_types) > 1:
342
393
  return value
343
- # given falsy string from a command line option
344
- return value not in {'0', ''}
394
+ msg = __("'%s' must be '0' or '1', got '%s'") % (name, value)
395
+ raise ConfigError(msg)
345
396
  if isinstance(default, dict):
346
- raise ValueError(__('cannot override dictionary config setting %r, '
347
- 'ignoring (use %r to set individual elements)') %
348
- (name, f'{name}.key=value'))
397
+ raise ValueError( # NoQA: TRY004
398
+ __(
399
+ 'cannot override dictionary config setting %r, '
400
+ 'ignoring (use %r to set individual elements)'
401
+ )
402
+ % (name, f'{name}.key=value')
403
+ )
349
404
  if isinstance(default, list):
350
405
  return value.split(',')
351
406
  if isinstance(default, int):
352
407
  try:
353
408
  return int(value)
354
409
  except ValueError as exc:
355
- raise ValueError(__('invalid number %r for config value %r, ignoring') %
356
- (value, name)) from exc
410
+ raise ValueError(
411
+ __('invalid number %r for config value %r, ignoring')
412
+ % (value, name)
413
+ ) from exc
357
414
  if callable(default):
358
415
  return value
359
416
  if isinstance(default, str) or default is None:
360
417
  return value
361
- raise ValueError(__('cannot override config setting %r with unsupported '
362
- 'type, ignoring') % name)
418
+ raise ValueError(
419
+ __('cannot override config setting %r with unsupported type, ignoring')
420
+ % name
421
+ )
363
422
 
364
423
  @staticmethod
365
424
  def pre_init_values() -> None:
@@ -379,7 +438,9 @@ class Config:
379
438
  def _report_override_warnings(self) -> None:
380
439
  for name in self._overrides:
381
440
  if name not in self._options:
382
- logger.warning(__('unknown config value %r in override, ignoring'), name)
441
+ logger.warning(
442
+ __('unknown config value %r in override, ignoring'), name
443
+ )
383
444
 
384
445
  def __repr__(self) -> str:
385
446
  values = []
@@ -388,7 +449,7 @@ class Config:
388
449
  opt_value = getattr(self, opt_name)
389
450
  except Exception:
390
451
  opt_value = '<error!>'
391
- values.append(f"{opt_name}={opt_value!r}")
452
+ values.append(f'{opt_name}={opt_value!r}')
392
453
  return self.__class__.__qualname__ + '(' + ', '.join(values) + ')'
393
454
 
394
455
  def __setattr__(self, key: str, value: object) -> None:
@@ -414,7 +475,7 @@ class Config:
414
475
  try:
415
476
  value = self.convert_overrides(name, value)
416
477
  except ValueError as exc:
417
- logger.warning("%s", exc)
478
+ logger.warning('%s', exc)
418
479
  else:
419
480
  self.__setattr__(name, value)
420
481
  return value
@@ -451,9 +512,14 @@ class Config:
451
512
  for name, opt in self._options.items():
452
513
  yield ConfigValue(name, getattr(self, name), opt.rebuild)
453
514
 
454
- def add(self, name: str, default: Any, rebuild: _ConfigRebuild,
455
- types: type | Collection[type] | ENUM,
456
- description: str = '') -> None:
515
+ def add(
516
+ self,
517
+ name: str,
518
+ default: Any,
519
+ rebuild: _ConfigRebuild,
520
+ types: type | Collection[type] | ENUM,
521
+ description: str = '',
522
+ ) -> None:
457
523
  if name in self._options:
458
524
  raise ExtensionError(__('Config value %r already present') % name)
459
525
 
@@ -470,7 +536,7 @@ class Config:
470
536
  return (value for value in self if value.rebuild == rebuild)
471
537
  return (value for value in self if value.rebuild in rebuild)
472
538
 
473
- def __getstate__(self) -> dict:
539
+ def __getstate__(self) -> dict[str, Any]:
474
540
  """Obtains serializable data for pickling."""
475
541
  # remove potentially pickling-problematic values from config
476
542
  __dict__ = {
@@ -478,7 +544,7 @@ class Config:
478
544
  for key, value in self.__dict__.items()
479
545
  if not key.startswith('_') and is_serializable(value)
480
546
  }
481
- # create a picklable copy of ``self._options``
547
+ # create a pickleable copy of ``self._options``
482
548
  __dict__['_options'] = _options = {}
483
549
  for name, opt in self._options.items():
484
550
  if not isinstance(opt, _Opt) and isinstance(opt, tuple) and len(opt) <= 3:
@@ -491,8 +557,10 @@ class Config:
491
557
  # will always mark the config value as changed,
492
558
  # and thus always invalidate the cache and perform a rebuild.
493
559
  logger.warning(
494
- __('cannot cache unpickable configuration value: %r '
495
- '(because it contains a function, class, or module object)'),
560
+ __(
561
+ 'cannot cache unpickleable configuration value: %r '
562
+ '(because it contains a function, class, or module object)'
563
+ ),
496
564
  name,
497
565
  type='config',
498
566
  subtype='cache',
@@ -505,50 +573,56 @@ class Config:
505
573
 
506
574
  return __dict__
507
575
 
508
- def __setstate__(self, state: dict) -> None:
576
+ def __setstate__(self, state: dict[str, Any]) -> None:
509
577
  self._overrides = {}
510
578
  self._options = {
511
- name: _Opt(real_value, rebuild, ())
579
+ name: _Opt(real_value, rebuild, frozenset())
512
580
  for name, (real_value, rebuild) in state.pop('_options').items()
513
581
  }
514
582
  self._raw_config = {}
515
583
  self.__dict__.update(state)
516
584
 
517
585
 
518
- def eval_config_file(filename: str, tags: Tags | None) -> dict[str, Any]:
586
+ def eval_config_file(
587
+ filename: str | os.PathLike[str], tags: Tags | None
588
+ ) -> dict[str, Any]:
519
589
  """Evaluate a config file."""
520
- namespace: dict[str, Any] = {}
521
- namespace['__file__'] = filename
522
- namespace['tags'] = tags
590
+ filename = Path(filename)
523
591
 
524
- with chdir(path.dirname(filename)):
592
+ namespace: dict[str, Any] = {
593
+ '__file__': str(filename),
594
+ 'tags': tags,
595
+ }
596
+
597
+ with chdir(filename.parent):
525
598
  # during executing config file, current dir is changed to ``confdir``.
526
599
  try:
527
- with open(filename, 'rb') as f:
528
- code = compile(f.read(), filename.encode(fs_encoding), 'exec')
529
- exec(code, namespace) # NoQA: S102
600
+ code = compile(filename.read_bytes(), filename, 'exec')
601
+ exec(code, namespace) # NoQA: S102
530
602
  except SyntaxError as err:
531
- msg = __("There is a syntax error in your configuration file: %s\n")
603
+ msg = __('There is a syntax error in your configuration file: %s\n')
532
604
  raise ConfigError(msg % err) from err
533
605
  except SystemExit as exc:
534
- msg = __("The configuration file (or one of the modules it imports) "
535
- "called sys.exit()")
606
+ msg = __(
607
+ 'The configuration file (or one of the modules it imports) '
608
+ 'called sys.exit()'
609
+ )
536
610
  raise ConfigError(msg) from exc
537
611
  except ConfigError:
538
612
  # pass through ConfigError from conf.py as is. It will be shown in console.
539
613
  raise
540
614
  except Exception as exc:
541
- msg = __("There is a programmable error in your configuration file:\n\n%s")
615
+ msg = __('There is a programmable error in your configuration file:\n\n%s')
542
616
  raise ConfigError(msg % traceback.format_exc()) from exc
543
617
 
544
618
  return namespace
545
619
 
546
620
 
547
621
  def _validate_valid_types(
548
- valid_types: type | Collection[type] | ENUM, /,
549
- ) -> tuple[()] | tuple[type, ...] | frozenset[type] | ENUM:
622
+ valid_types: type | Collection[type] | ENUM, /
623
+ ) -> frozenset[type] | ENUM:
550
624
  if not valid_types:
551
- return ()
625
+ return frozenset()
552
626
  if isinstance(valid_types, frozenset | ENUM):
553
627
  return valid_types
554
628
  if isinstance(valid_types, type):
@@ -557,16 +631,11 @@ def _validate_valid_types(
557
631
  return frozenset({Any}) # type: ignore[arg-type]
558
632
  if isinstance(valid_types, set):
559
633
  return frozenset(valid_types)
560
- if not isinstance(valid_types, tuple):
561
- try:
562
- valid_types = tuple(valid_types)
563
- except TypeError:
564
- logger.warning(__('Failed to convert %r to a set or tuple'), valid_types)
565
- return valid_types # type: ignore[return-value]
566
634
  try:
567
635
  return frozenset(valid_types)
568
636
  except TypeError:
569
- return valid_types
637
+ logger.warning(__('Failed to convert %r to a frozenset'), valid_types)
638
+ return frozenset()
570
639
 
571
640
 
572
641
  def convert_source_suffix(app: Sphinx, config: Config) -> None:
@@ -582,16 +651,24 @@ def convert_source_suffix(app: Sphinx, config: Config) -> None:
582
651
  # The default filetype is determined on later step.
583
652
  # By default, it is considered as restructuredtext.
584
653
  config.source_suffix = {source_suffix: 'restructuredtext'}
585
- logger.info(__("Converting `source_suffix = %r` to `source_suffix = %r`."),
586
- source_suffix, config.source_suffix)
654
+ logger.info(
655
+ __('Converting `source_suffix = %r` to `source_suffix = %r`.'),
656
+ source_suffix,
657
+ config.source_suffix,
658
+ )
587
659
  elif isinstance(source_suffix, list | tuple):
588
660
  # if list, considers as all of them are default filetype
589
661
  config.source_suffix = dict.fromkeys(source_suffix, 'restructuredtext')
590
- logger.info(__("Converting `source_suffix = %r` to `source_suffix = %r`."),
591
- source_suffix, config.source_suffix)
662
+ logger.info(
663
+ __('Converting `source_suffix = %r` to `source_suffix = %r`.'),
664
+ source_suffix,
665
+ config.source_suffix,
666
+ )
592
667
  elif not isinstance(source_suffix, dict):
593
- msg = __("The config value `source_suffix' expects a dictionary, "
594
- "a string, or a list of strings. Got `%r' instead (type %s).")
668
+ msg = __(
669
+ "The config value `source_suffix' expects a dictionary, "
670
+ "a string, or a list of strings. Got `%r' instead (type %s)."
671
+ )
595
672
  raise ConfigError(msg % (source_suffix, type(source_suffix)))
596
673
 
597
674
 
@@ -609,10 +686,12 @@ def convert_highlight_options(app: Sphinx, config: Config) -> None:
609
686
 
610
687
  def init_numfig_format(app: Sphinx, config: Config) -> None:
611
688
  """Initialize :confval:`numfig_format`."""
612
- numfig_format = {'section': _('Section %s'),
613
- 'figure': _('Fig. %s'),
614
- 'table': _('Table %s'),
615
- 'code-block': _('Listing %s')}
689
+ numfig_format = {
690
+ 'section': _('Section %s'),
691
+ 'figure': _('Fig. %s'),
692
+ 'table': _('Table %s'),
693
+ 'code-block': _('Listing %s'),
694
+ }
616
695
 
617
696
  # override default labels by configuration
618
697
  numfig_format.update(config.numfig_format)
@@ -719,10 +798,14 @@ def check_confval_types(app: Sphinx | None, config: Config) -> None:
719
798
 
720
799
  if isinstance(valid_types, ENUM):
721
800
  if not valid_types.match(value):
722
- msg = __("The config value `{name}` has to be a one of {candidates}, "
723
- "but `{current}` is given.")
801
+ msg = __(
802
+ 'The config value `{name}` has to be a one of {candidates}, '
803
+ 'but `{current}` is given.'
804
+ )
724
805
  logger.warning(
725
- msg.format(name=name, current=value, candidates=valid_types.candidates),
806
+ msg.format(
807
+ name=name, current=value, candidates=valid_types._candidates
808
+ ),
726
809
  once=True,
727
810
  )
728
811
  continue
@@ -734,30 +817,39 @@ def check_confval_types(app: Sphinx | None, config: Config) -> None:
734
817
  continue
735
818
 
736
819
  if type_value in valid_types: # check explicitly listed types
820
+ if frozenset in valid_types and type_value in {list, tuple, set}:
821
+ setattr(config, name, frozenset(value))
822
+ elif tuple in valid_types and type_value is list:
823
+ setattr(config, name, tuple(value))
737
824
  continue
738
825
 
739
- common_bases = ({*type_value.__bases__, type_value}
740
- & set(type_default.__bases__))
826
+ common_bases = {*type_value.__bases__, type_value} & set(type_default.__bases__)
741
827
  common_bases.discard(object)
742
828
  if common_bases:
743
829
  continue # at least we share a non-trivial base class
744
830
 
745
831
  if valid_types:
746
- msg = __("The config value `{name}' has type `{current.__name__}'; "
747
- "expected {permitted}.")
832
+ msg = __(
833
+ "The config value `{name}' has type `{current.__name__}'; "
834
+ 'expected {permitted}.'
835
+ )
748
836
  wrapped_valid_types = sorted(f"`{c.__name__}'" for c in valid_types)
749
837
  if len(wrapped_valid_types) > 2:
750
- permitted = (", ".join(wrapped_valid_types[:-1])
751
- + f", or {wrapped_valid_types[-1]}")
838
+ permitted = (
839
+ ', '.join(wrapped_valid_types[:-1])
840
+ + f', or {wrapped_valid_types[-1]}'
841
+ )
752
842
  else:
753
- permitted = " or ".join(wrapped_valid_types)
843
+ permitted = ' or '.join(wrapped_valid_types)
754
844
  logger.warning(
755
845
  msg.format(name=name, current=type_value, permitted=permitted),
756
846
  once=True,
757
847
  )
758
848
  else:
759
- msg = __("The config value `{name}' has type `{current.__name__}', "
760
- "defaults to `{default.__name__}'.")
849
+ msg = __(
850
+ "The config value `{name}' has type `{current.__name__}', "
851
+ "defaults to `{default.__name__}'."
852
+ )
761
853
  logger.warning(
762
854
  msg.format(name=name, current=type_value, default=type_default),
763
855
  once=True,
@@ -771,17 +863,27 @@ def check_primary_domain(app: Sphinx, config: Config) -> None:
771
863
  config.primary_domain = None
772
864
 
773
865
 
774
- def check_root_doc(app: Sphinx, env: BuildEnvironment, added: Set[str],
775
- changed: Set[str], removed: Set[str]) -> Iterable[str]:
776
- """Adjust root_doc to 'contents' to support an old project which does not have
777
- any root_doc setting.
778
- """
779
- if (app.config.root_doc == 'index' and
780
- 'index' not in app.project.docnames and
781
- 'contents' in app.project.docnames):
782
- logger.warning(__('Since v2.0, Sphinx uses "index" as root_doc by default. '
783
- 'Please add "root_doc = \'contents\'" to your conf.py.'))
784
- app.config.root_doc = "contents"
866
+ def check_master_doc(
867
+ app: Sphinx,
868
+ env: BuildEnvironment,
869
+ added: Set[str],
870
+ changed: Set[str],
871
+ removed: Set[str],
872
+ ) -> Iterable[str]:
873
+ """Sphinx 2.0 changed the default from 'contents' to 'index'."""
874
+ docnames = app.project.docnames
875
+ if (
876
+ app.config.master_doc == 'index'
877
+ and 'index' not in docnames
878
+ and 'contents' in docnames
879
+ ):
880
+ logger.warning(
881
+ __(
882
+ 'Sphinx now uses "index" as the master document by default. '
883
+ 'To keep pre-2.0 behaviour, set "master_doc = \'contents\'".'
884
+ )
885
+ )
886
+ app.config.master_doc = 'contents'
785
887
 
786
888
  return changed
787
889
 
@@ -794,7 +896,7 @@ def setup(app: Sphinx) -> ExtensionMetadata:
794
896
  app.connect('config-inited', correct_copyright_year, priority=800)
795
897
  app.connect('config-inited', check_confval_types, priority=800)
796
898
  app.connect('config-inited', check_primary_domain, priority=800)
797
- app.connect('env-get-outdated', check_root_doc)
899
+ app.connect('env-get-outdated', check_master_doc)
798
900
 
799
901
  return {
800
902
  'version': 'builtin',