Sphinx 7.4.7__py3-none-any.whl → 8.0.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 (107) hide show
  1. sphinx/__init__.py +2 -2
  2. sphinx/_cli/__init__.py +4 -4
  3. sphinx/application.py +7 -7
  4. sphinx/builders/__init__.py +2 -3
  5. sphinx/builders/_epub_base.py +33 -12
  6. sphinx/builders/changes.py +13 -5
  7. sphinx/builders/epub3.py +6 -2
  8. sphinx/builders/html/__init__.py +88 -58
  9. sphinx/builders/latex/__init__.py +38 -12
  10. sphinx/builders/latex/transforms.py +1 -1
  11. sphinx/builders/linkcheck.py +8 -49
  12. sphinx/builders/texinfo.py +12 -6
  13. sphinx/builders/text.py +7 -3
  14. sphinx/builders/xml.py +7 -3
  15. sphinx/cmd/quickstart.py +10 -20
  16. sphinx/config.py +12 -12
  17. sphinx/deprecation.py +8 -8
  18. sphinx/directives/other.py +2 -3
  19. sphinx/directives/patches.py +2 -2
  20. sphinx/domains/__init__.py +4 -2
  21. sphinx/domains/c/__init__.py +2 -2
  22. sphinx/domains/c/_ast.py +3 -2
  23. sphinx/domains/c/_parser.py +4 -3
  24. sphinx/domains/cpp/__init__.py +2 -2
  25. sphinx/domains/cpp/_ast.py +1 -2
  26. sphinx/domains/cpp/_parser.py +2 -2
  27. sphinx/domains/cpp/_symbol.py +2 -2
  28. sphinx/domains/math.py +1 -1
  29. sphinx/domains/python/_object.py +0 -1
  30. sphinx/domains/std/__init__.py +7 -8
  31. sphinx/environment/__init__.py +14 -32
  32. sphinx/environment/adapters/indexentries.py +4 -6
  33. sphinx/environment/adapters/toctree.py +4 -4
  34. sphinx/environment/collectors/title.py +1 -1
  35. sphinx/environment/collectors/toctree.py +1 -1
  36. sphinx/events.py +3 -1
  37. sphinx/ext/autodoc/__init__.py +17 -63
  38. sphinx/ext/autodoc/directive.py +7 -5
  39. sphinx/ext/autodoc/importer.py +2 -1
  40. sphinx/ext/autodoc/preserve_defaults.py +2 -2
  41. sphinx/ext/autosummary/__init__.py +7 -6
  42. sphinx/ext/autosummary/generate.py +5 -4
  43. sphinx/ext/doctest.py +5 -5
  44. sphinx/ext/graphviz.py +1 -1
  45. sphinx/ext/imgmath.py +1 -1
  46. sphinx/ext/inheritance_diagram.py +1 -1
  47. sphinx/ext/intersphinx/__init__.py +25 -5
  48. sphinx/ext/intersphinx/_cli.py +7 -6
  49. sphinx/ext/intersphinx/_load.py +240 -115
  50. sphinx/ext/intersphinx/_resolve.py +12 -11
  51. sphinx/ext/intersphinx/_shared.py +102 -9
  52. sphinx/ext/mathjax.py +1 -1
  53. sphinx/ext/napoleon/docstring.py +2 -2
  54. sphinx/ext/todo.py +2 -2
  55. sphinx/ext/viewcode.py +2 -1
  56. sphinx/highlighting.py +3 -3
  57. sphinx/io.py +2 -2
  58. sphinx/jinja2glue.py +13 -6
  59. sphinx/locale/__init__.py +4 -3
  60. sphinx/project.py +23 -19
  61. sphinx/pycode/ast.py +2 -2
  62. sphinx/pycode/parser.py +2 -2
  63. sphinx/pygments_styles.py +3 -3
  64. sphinx/registry.py +3 -8
  65. sphinx/search/__init__.py +1 -1
  66. sphinx/testing/path.py +2 -1
  67. sphinx/testing/util.py +1 -1
  68. sphinx/texinputs/Makefile.jinja +2 -1
  69. sphinx/texinputs_win/Makefile.jinja +2 -1
  70. sphinx/theming.py +3 -12
  71. sphinx/transforms/__init__.py +5 -5
  72. sphinx/transforms/references.py +1 -1
  73. sphinx/util/__init__.py +11 -35
  74. sphinx/util/_timestamps.py +12 -0
  75. sphinx/util/cfamily.py +5 -5
  76. sphinx/util/console.py +4 -3
  77. sphinx/util/display.py +3 -3
  78. sphinx/util/docfields.py +1 -1
  79. sphinx/util/docutils.py +44 -10
  80. sphinx/util/fileutil.py +25 -20
  81. sphinx/util/i18n.py +9 -4
  82. sphinx/util/images.py +3 -2
  83. sphinx/util/inspect.py +28 -43
  84. sphinx/util/inventory.py +2 -2
  85. sphinx/util/matching.py +2 -2
  86. sphinx/util/math.py +1 -1
  87. sphinx/util/nodes.py +8 -8
  88. sphinx/util/osutil.py +29 -28
  89. sphinx/util/parallel.py +2 -2
  90. sphinx/util/requests.py +1 -1
  91. sphinx/util/template.py +3 -3
  92. sphinx/util/typing.py +36 -72
  93. sphinx/writers/html.py +1 -1
  94. sphinx/writers/html5.py +1 -1
  95. sphinx/writers/latex.py +4 -4
  96. sphinx/writers/manpage.py +2 -2
  97. sphinx/writers/texinfo.py +5 -5
  98. sphinx/writers/text.py +4 -4
  99. sphinx/writers/xml.py +2 -2
  100. {sphinx-7.4.7.dist-info → sphinx-8.0.0rc1.dist-info}/METADATA +10 -9
  101. {sphinx-7.4.7.dist-info → sphinx-8.0.0rc1.dist-info}/RECORD +104 -106
  102. sphinx/templates/quickstart/Makefile.jinja +0 -98
  103. sphinx/templates/quickstart/make.bat.jinja +0 -110
  104. sphinx/util/_pathlib.py +0 -120
  105. {sphinx-7.4.7.dist-info → sphinx-8.0.0rc1.dist-info}/LICENSE.rst +0 -0
  106. {sphinx-7.4.7.dist-info → sphinx-8.0.0rc1.dist-info}/WHEEL +0 -0
  107. {sphinx-7.4.7.dist-info → sphinx-8.0.0rc1.dist-info}/entry_points.txt +0 -0
@@ -28,10 +28,11 @@ if TYPE_CHECKING:
28
28
  from sphinx.application import Sphinx
29
29
  from sphinx.domains import Domain
30
30
  from sphinx.environment import BuildEnvironment
31
+ from sphinx.ext.intersphinx._shared import InventoryName
31
32
  from sphinx.util.typing import Inventory, InventoryItem, RoleFunction
32
33
 
33
34
 
34
- def _create_element_from_result(domain: Domain, inv_name: str | None,
35
+ def _create_element_from_result(domain: Domain, inv_name: InventoryName | None,
35
36
  data: InventoryItem,
36
37
  node: pending_xref, contnode: TextElement) -> nodes.reference:
37
38
  proj, version, uri, dispname = data
@@ -61,7 +62,7 @@ def _create_element_from_result(domain: Domain, inv_name: str | None,
61
62
 
62
63
 
63
64
  def _resolve_reference_in_domain_by_target(
64
- inv_name: str | None, inventory: Inventory,
65
+ inv_name: InventoryName | None, inventory: Inventory,
65
66
  domain: Domain, objtypes: Iterable[str],
66
67
  target: str,
67
68
  node: pending_xref, contnode: TextElement) -> nodes.reference | None:
@@ -100,7 +101,7 @@ def _resolve_reference_in_domain_by_target(
100
101
 
101
102
 
102
103
  def _resolve_reference_in_domain(env: BuildEnvironment,
103
- inv_name: str | None, inventory: Inventory,
104
+ inv_name: InventoryName | None, inventory: Inventory,
104
105
  honor_disabled_refs: bool,
105
106
  domain: Domain, objtypes: Iterable[str],
106
107
  node: pending_xref, contnode: TextElement,
@@ -142,20 +143,21 @@ def _resolve_reference_in_domain(env: BuildEnvironment,
142
143
  full_qualified_name, node, contnode)
143
144
 
144
145
 
145
- def _resolve_reference(env: BuildEnvironment, inv_name: str | None, inventory: Inventory,
146
+ def _resolve_reference(env: BuildEnvironment,
147
+ inv_name: InventoryName | None, inventory: Inventory,
146
148
  honor_disabled_refs: bool,
147
149
  node: pending_xref, contnode: TextElement) -> nodes.reference | None:
148
150
  # disabling should only be done if no inventory is given
149
151
  honor_disabled_refs = honor_disabled_refs and inv_name is None
152
+ intersphinx_disabled_reftypes = env.config.intersphinx_disabled_reftypes
150
153
 
151
- if honor_disabled_refs and '*' in env.config.intersphinx_disabled_reftypes:
154
+ if honor_disabled_refs and '*' in intersphinx_disabled_reftypes:
152
155
  return None
153
156
 
154
157
  typ = node['reftype']
155
158
  if typ == 'any':
156
159
  for domain_name, domain in env.domains.items():
157
- if (honor_disabled_refs
158
- and (domain_name + ':*') in env.config.intersphinx_disabled_reftypes):
160
+ if honor_disabled_refs and f'{domain_name}:*' in intersphinx_disabled_reftypes:
159
161
  continue
160
162
  objtypes: Iterable[str] = domain.object_types.keys()
161
163
  res = _resolve_reference_in_domain(env, inv_name, inventory,
@@ -170,8 +172,7 @@ def _resolve_reference(env: BuildEnvironment, inv_name: str | None, inventory: I
170
172
  if not domain_name:
171
173
  # only objects in domains are in the inventory
172
174
  return None
173
- if (honor_disabled_refs
174
- and (domain_name + ':*') in env.config.intersphinx_disabled_reftypes):
175
+ if honor_disabled_refs and f'{domain_name}:*' in intersphinx_disabled_reftypes:
175
176
  return None
176
177
  domain = env.get_domain(domain_name)
177
178
  objtypes = domain.objtypes_for_role(typ) or ()
@@ -183,12 +184,12 @@ def _resolve_reference(env: BuildEnvironment, inv_name: str | None, inventory: I
183
184
  node, contnode)
184
185
 
185
186
 
186
- def inventory_exists(env: BuildEnvironment, inv_name: str) -> bool:
187
+ def inventory_exists(env: BuildEnvironment, inv_name: InventoryName) -> bool:
187
188
  return inv_name in InventoryAdapter(env).named_inventory
188
189
 
189
190
 
190
191
  def resolve_reference_in_inventory(env: BuildEnvironment,
191
- inv_name: str,
192
+ inv_name: InventoryName,
192
193
  node: pending_xref, contnode: TextElement,
193
194
  ) -> nodes.reference | None:
194
195
  """Attempt to resolve a missing reference via intersphinx references.
@@ -2,19 +2,113 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Final, Union
5
+ from typing import TYPE_CHECKING, Any, Final, NoReturn
6
6
 
7
7
  from sphinx.util import logging
8
8
 
9
9
  if TYPE_CHECKING:
10
+ from collections.abc import Sequence
11
+ from typing import TypeAlias
12
+
10
13
  from sphinx.environment import BuildEnvironment
11
14
  from sphinx.util.typing import Inventory
12
15
 
13
- InventoryCacheEntry = tuple[Union[str, None], int, Inventory]
16
+ #: The inventory project URL to which links are resolved.
17
+ #:
18
+ #: This value is unique in :confval:`intersphinx_mapping`.
19
+ InventoryURI = str
20
+
21
+ #: The inventory (non-empty) name.
22
+ #:
23
+ #: It is unique and in bijection with an inventory remote URL.
24
+ InventoryName = str
25
+
26
+ #: A target (local or remote) containing the inventory data to fetch.
27
+ #:
28
+ #: Empty strings are not expected and ``None`` indicates the default
29
+ #: inventory file name :data:`~sphinx.builder.html.INVENTORY_FILENAME`.
30
+ InventoryLocation = str | None
31
+
32
+ #: Inventory cache entry. The integer field is the cache expiration time.
33
+ InventoryCacheEntry: TypeAlias = tuple[InventoryName, int, Inventory]
34
+
35
+ #: The type of :confval:`intersphinx_mapping` *after* normalisation.
36
+ IntersphinxMapping = dict[
37
+ InventoryName,
38
+ tuple[InventoryName, tuple[InventoryURI, tuple[InventoryLocation, ...]]],
39
+ ]
14
40
 
15
41
  LOGGER: Final[logging.SphinxLoggerAdapter] = logging.getLogger('sphinx.ext.intersphinx')
16
42
 
17
43
 
44
+ class _IntersphinxProject:
45
+ name: InventoryName
46
+ target_uri: InventoryURI
47
+ locations: tuple[InventoryLocation, ...]
48
+
49
+ __slots__ = {
50
+ 'name': 'The inventory name. '
51
+ 'It is unique and in bijection with an remote inventory URL.',
52
+ 'target_uri': 'The inventory project URL to which links are resolved. '
53
+ 'It is unique and in bijection with an inventory name.',
54
+ 'locations': 'A tuple of local or remote targets containing '
55
+ 'the inventory data to fetch. '
56
+ 'None indicates the default inventory file name.',
57
+ }
58
+
59
+ def __init__(
60
+ self,
61
+ *,
62
+ name: InventoryName,
63
+ target_uri: InventoryURI,
64
+ locations: Sequence[InventoryLocation],
65
+ ) -> None:
66
+ if not name or not isinstance(name, str):
67
+ msg = 'name must be a non-empty string'
68
+ raise ValueError(msg)
69
+ if not target_uri or not isinstance(target_uri, str):
70
+ msg = 'target_uri must be a non-empty string'
71
+ raise ValueError(msg)
72
+ if not locations or not isinstance(locations, tuple):
73
+ msg = 'locations must be a non-empty tuple'
74
+ raise ValueError(msg)
75
+ if any(
76
+ location is not None and (not location or not isinstance(location, str))
77
+ for location in locations
78
+ ):
79
+ msg = 'locations must be a tuple of strings or None'
80
+ raise ValueError(msg)
81
+ object.__setattr__(self, 'name', name)
82
+ object.__setattr__(self, 'target_uri', target_uri)
83
+ object.__setattr__(self, 'locations', tuple(locations))
84
+
85
+ def __repr__(self) -> str:
86
+ return (f'{self.__class__.__name__}('
87
+ f'name={self.name!r}, '
88
+ f'target_uri={self.target_uri!r}, '
89
+ f'locations={self.locations!r})')
90
+
91
+ def __eq__(self, other: object) -> bool:
92
+ if not isinstance(other, _IntersphinxProject):
93
+ return NotImplemented
94
+ return (
95
+ self.name == other.name
96
+ and self.target_uri == other.target_uri
97
+ and self.locations == other.locations
98
+ )
99
+
100
+ def __hash__(self) -> int:
101
+ return hash((self.name, self.target_uri, self.locations))
102
+
103
+ def __setattr__(self, key: str, value: Any) -> NoReturn:
104
+ msg = f'{self.__class__.__name__} is immutable'
105
+ raise AttributeError(msg)
106
+
107
+ def __delattr__(self, key: str) -> NoReturn:
108
+ msg = f'{self.__class__.__name__} is immutable'
109
+ raise AttributeError(msg)
110
+
111
+
18
112
  class InventoryAdapter:
19
113
  """Inventory adapter for environment"""
20
114
 
@@ -29,14 +123,13 @@ class InventoryAdapter:
29
123
  self.env.intersphinx_named_inventory = {} # type: ignore[attr-defined]
30
124
 
31
125
  @property
32
- def cache(self) -> dict[str, InventoryCacheEntry]:
126
+ def cache(self) -> dict[InventoryURI, InventoryCacheEntry]:
33
127
  """Intersphinx cache.
34
128
 
35
- - Key is the URI of the remote inventory
36
- - Element one is the key given in the Sphinx intersphinx_mapping
37
- configuration value
38
- - Element two is a time value for cache invalidation, a float
39
- - Element three is the loaded remote inventory, type Inventory
129
+ - Key is the URI of the remote inventory.
130
+ - Element one is the key given in the Sphinx :confval:`intersphinx_mapping`.
131
+ - Element two is a time value for cache invalidation, an integer.
132
+ - Element three is the loaded remote inventory of type :class:`!Inventory`.
40
133
  """
41
134
  return self.env.intersphinx_cache # type: ignore[attr-defined]
42
135
 
@@ -45,7 +138,7 @@ class InventoryAdapter:
45
138
  return self.env.intersphinx_inventory # type: ignore[attr-defined]
46
139
 
47
140
  @property
48
- def named_inventory(self) -> dict[str, Inventory]:
141
+ def named_inventory(self) -> dict[InventoryName, Inventory]:
49
142
  return self.env.intersphinx_named_inventory # type: ignore[attr-defined]
50
143
 
51
144
  def clear(self) -> None:
sphinx/ext/mathjax.py CHANGED
@@ -22,7 +22,7 @@ from sphinx.util.math import get_node_equation_number
22
22
  if TYPE_CHECKING:
23
23
  from sphinx.application import Sphinx
24
24
  from sphinx.util.typing import ExtensionMetadata
25
- from sphinx.writers.html import HTML5Translator
25
+ from sphinx.writers.html5 import HTML5Translator
26
26
 
27
27
  # more information for mathjax secure url is here:
28
28
  # https://docs.mathjax.org/en/latest/web/start.html#using-mathjax-from-a-content-delivery-network-cdn
@@ -8,14 +8,14 @@ import inspect
8
8
  import re
9
9
  from functools import partial
10
10
  from itertools import starmap
11
- from typing import TYPE_CHECKING, Any, Callable
11
+ from typing import TYPE_CHECKING, Any
12
12
 
13
13
  from sphinx.locale import _, __
14
14
  from sphinx.util import logging
15
15
  from sphinx.util.typing import get_type_hints, stringify_annotation
16
16
 
17
17
  if TYPE_CHECKING:
18
- from collections.abc import Iterator
18
+ from collections.abc import Callable, Iterator
19
19
 
20
20
  from sphinx.application import Sphinx
21
21
  from sphinx.config import Config as SphinxConfig
sphinx/ext/todo.py CHANGED
@@ -29,7 +29,7 @@ if TYPE_CHECKING:
29
29
  from sphinx.application import Sphinx
30
30
  from sphinx.environment import BuildEnvironment
31
31
  from sphinx.util.typing import ExtensionMetadata, OptionSpec
32
- from sphinx.writers.html import HTML5Translator
32
+ from sphinx.writers.html5 import HTML5Translator
33
33
  from sphinx.writers.latex import LaTeXTranslator
34
34
 
35
35
  logger = logging.getLogger(__name__)
@@ -43,7 +43,7 @@ class todolist(nodes.General, nodes.Element):
43
43
  pass
44
44
 
45
45
 
46
- class Todo(BaseAdmonition, SphinxDirective):
46
+ class Todo(BaseAdmonition, SphinxDirective): # type: ignore[misc]
47
47
  """
48
48
  A todo entry, displayed (if configured) in the form of an admonition.
49
49
  """
sphinx/ext/viewcode.py CHANGED
@@ -21,6 +21,7 @@ from sphinx.transforms.post_transforms import SphinxPostTransform
21
21
  from sphinx.util import logging
22
22
  from sphinx.util.display import status_iterator
23
23
  from sphinx.util.nodes import make_refnode
24
+ from sphinx.util.osutil import _last_modified_time
24
25
 
25
26
  if TYPE_CHECKING:
26
27
  from collections.abc import Iterable, Iterator
@@ -231,7 +232,7 @@ def should_generate_module_page(app: Sphinx, modname: str) -> bool:
231
232
  page_filename = path.join(app.outdir, '_modules/', basename)
232
233
 
233
234
  try:
234
- if path.getmtime(module_filename) <= path.getmtime(page_filename):
235
+ if _last_modified_time(module_filename) <= _last_modified_time(page_filename):
235
236
  # generation is not needed if the HTML page is newer than module file.
236
237
  return False
237
238
  except OSError:
sphinx/highlighting.py CHANGED
@@ -86,8 +86,8 @@ _LATEX_ADD_STYLES = r"""
86
86
  class PygmentsBridge:
87
87
  # Set these attributes if you want to have different Pygments formatters
88
88
  # than the default ones.
89
- html_formatter = HtmlFormatter
90
- latex_formatter = LatexFormatter
89
+ html_formatter = HtmlFormatter[str]
90
+ latex_formatter = LatexFormatter[str]
91
91
 
92
92
  def __init__(
93
93
  self, dest: str = 'html', stylename: str = 'sphinx', latex_engine: str | None = None
@@ -98,7 +98,7 @@ class PygmentsBridge:
98
98
  style = self.get_style(stylename)
99
99
  self.formatter_args: dict[str, Any] = {'style': style}
100
100
  if dest == 'html':
101
- self.formatter = self.html_formatter
101
+ self.formatter: type[Formatter[str]] = self.html_formatter
102
102
  else:
103
103
  self.formatter = self.latex_formatter
104
104
  self.formatter_args['commandprefix'] = 'PYG'
sphinx/io.py CHANGED
@@ -35,7 +35,7 @@ if TYPE_CHECKING:
35
35
  logger = logging.getLogger(__name__)
36
36
 
37
37
 
38
- class SphinxBaseReader(standalone.Reader):
38
+ class SphinxBaseReader(standalone.Reader): # type: ignore[misc]
39
39
  """
40
40
  A base class of readers for Sphinx.
41
41
 
@@ -143,7 +143,7 @@ class SphinxI18nReader(SphinxBaseReader):
143
143
  self.transforms.remove(transform)
144
144
 
145
145
 
146
- class SphinxDummyWriter(UnfilteredWriter):
146
+ class SphinxDummyWriter(UnfilteredWriter): # type: ignore[misc]
147
147
  """Dummy writer module used for generating doctree."""
148
148
 
149
149
  supported = ('html',) # needed to keep "meta" nodes
sphinx/jinja2glue.py CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
5
6
  from os import path
6
7
  from pprint import pformat
7
- from typing import TYPE_CHECKING, Any, Callable
8
+ from typing import TYPE_CHECKING, Any
8
9
 
9
10
  from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound
10
11
  from jinja2.sandbox import SandboxedEnvironment
@@ -12,10 +13,10 @@ from jinja2.utils import open_if_exists, pass_context
12
13
 
13
14
  from sphinx.application import TemplateBridge
14
15
  from sphinx.util import logging
15
- from sphinx.util.osutil import mtimes_of_files
16
+ from sphinx.util.osutil import _last_modified_time
16
17
 
17
18
  if TYPE_CHECKING:
18
- from collections.abc import Iterator
19
+ from collections.abc import Callable, Iterator
19
20
 
20
21
  from jinja2.environment import Environment
21
22
 
@@ -127,11 +128,11 @@ class SphinxFileSystemLoader(FileSystemLoader):
127
128
  with f:
128
129
  contents = f.read().decode(self.encoding)
129
130
 
130
- mtime = path.getmtime(filename)
131
+ mtime = _last_modified_time(filename)
131
132
 
132
133
  def uptodate() -> bool:
133
134
  try:
134
- return path.getmtime(filename) == mtime
135
+ return _last_modified_time(filename) == mtime
135
136
  except OSError:
136
137
  return False
137
138
 
@@ -203,7 +204,13 @@ class BuiltinTemplateLoader(TemplateBridge, BaseLoader):
203
204
  return self.environment.from_string(source).render(context)
204
205
 
205
206
  def newest_template_mtime(self) -> float:
206
- return max(mtimes_of_files(self.pathchain, '.html'))
207
+ return max(
208
+ os.stat(os.path.join(root, sfile)).st_mtime_ns / 10**9
209
+ for dirname in self.pathchain
210
+ for root, _dirs, files in os.walk(dirname)
211
+ for sfile in files
212
+ if sfile.endswith('.html')
213
+ )
207
214
 
208
215
  # Loader interface
209
216
 
sphinx/locale/__init__.py CHANGED
@@ -9,8 +9,9 @@ from os import path
9
9
  from typing import TYPE_CHECKING
10
10
 
11
11
  if TYPE_CHECKING:
12
- from collections.abc import Iterable
13
- from typing import Any, Callable
12
+ import os
13
+ from collections.abc import Callable, Iterable
14
+ from typing import Any
14
15
 
15
16
 
16
17
  class _TranslationProxy:
@@ -98,7 +99,7 @@ translators: dict[tuple[str, str], NullTranslations] = {}
98
99
 
99
100
 
100
101
  def init(
101
- locale_dirs: Iterable[str | None],
102
+ locale_dirs: Iterable[str | os.PathLike[str] | None],
102
103
  language: str | None,
103
104
  catalog: str = 'sphinx',
104
105
  namespace: str = 'general',
sphinx/project.py CHANGED
@@ -4,13 +4,13 @@ from __future__ import annotations
4
4
 
5
5
  import contextlib
6
6
  import os
7
- from glob import glob
7
+ from pathlib import Path
8
8
  from typing import TYPE_CHECKING
9
9
 
10
10
  from sphinx.locale import __
11
11
  from sphinx.util import logging
12
12
  from sphinx.util.matching import get_matching_files
13
- from sphinx.util.osutil import path_stabilize, relpath
13
+ from sphinx.util.osutil import path_stabilize
14
14
 
15
15
  if TYPE_CHECKING:
16
16
  from collections.abc import Iterable
@@ -24,7 +24,7 @@ class Project:
24
24
 
25
25
  def __init__(self, srcdir: str | os.PathLike[str], source_suffix: Iterable[str]) -> None:
26
26
  #: Source directory.
27
- self.srcdir = srcdir
27
+ self.srcdir = Path(srcdir)
28
28
 
29
29
  #: source_suffix. Same as :confval:`source_suffix`.
30
30
  self.source_suffix = tuple(source_suffix)
@@ -34,8 +34,8 @@ class Project:
34
34
  self.docnames: set[str] = set()
35
35
 
36
36
  # Bijective mapping between docnames and (srcdir relative) paths.
37
- self._path_to_docname: dict[str, str] = {}
38
- self._docname_to_path: dict[str, str] = {}
37
+ self._path_to_docname: dict[Path, str] = {}
38
+ self._docname_to_path: dict[str, Path] = {}
39
39
 
40
40
  def restore(self, other: Project) -> None:
41
41
  """Take over a result of last build."""
@@ -60,22 +60,25 @@ class Project:
60
60
  ):
61
61
  if docname := self.path2doc(filename):
62
62
  if docname in self.docnames:
63
- pattern = os.path.join(self.srcdir, docname) + '.*'
64
- files = [relpath(f, self.srcdir) for f in glob(pattern)]
63
+ files = [
64
+ str(f.relative_to(self.srcdir))
65
+ for f in self.srcdir.glob(f'{docname}.*')
66
+ ]
65
67
  logger.warning(
66
68
  __(
67
- 'multiple files found for the document "%s": %r\n'
69
+ 'multiple files found for the document "%s": %s\n'
68
70
  'Use %r for the build.'
69
71
  ),
70
72
  docname,
71
- files,
73
+ ', '.join(files),
72
74
  self.doc2path(docname, absolute=True),
73
75
  once=True,
74
76
  )
75
- elif os.access(os.path.join(self.srcdir, filename), os.R_OK):
77
+ elif os.access(self.srcdir / filename, os.R_OK):
76
78
  self.docnames.add(docname)
77
- self._path_to_docname[filename] = docname
78
- self._docname_to_path[docname] = filename
79
+ path = Path(filename)
80
+ self._path_to_docname[path] = docname
81
+ self._docname_to_path[docname] = path
79
82
  else:
80
83
  logger.warning(
81
84
  __('Ignored unreadable document %r.'), filename, location=docname
@@ -91,18 +94,19 @@ class Project:
91
94
  try:
92
95
  return self._path_to_docname[filename] # type: ignore[index]
93
96
  except KeyError:
94
- if os.path.isabs(filename):
97
+ path = Path(filename)
98
+ if path.is_absolute():
95
99
  with contextlib.suppress(ValueError):
96
- filename = os.path.relpath(filename, self.srcdir)
100
+ path = path.relative_to(self.srcdir)
97
101
 
98
102
  for suffix in self.source_suffix:
99
- if os.path.basename(filename).endswith(suffix):
100
- return path_stabilize(filename).removesuffix(suffix)
103
+ if path.name.endswith(suffix):
104
+ return path_stabilize(path).removesuffix(suffix)
101
105
 
102
106
  # the file does not have a docname
103
107
  return None
104
108
 
105
- def doc2path(self, docname: str, absolute: bool) -> str:
109
+ def doc2path(self, docname: str, absolute: bool) -> Path:
106
110
  """Return the filename for the document name.
107
111
 
108
112
  If *absolute* is True, return as an absolute path.
@@ -112,8 +116,8 @@ class Project:
112
116
  filename = self._docname_to_path[docname]
113
117
  except KeyError:
114
118
  # Backwards compatibility: the document does not exist
115
- filename = docname + self._first_source_suffix
119
+ filename = Path(docname + self._first_source_suffix)
116
120
 
117
121
  if absolute:
118
- return os.path.join(self.srcdir, filename)
122
+ return self.srcdir / filename
119
123
  return filename
sphinx/pycode/ast.py CHANGED
@@ -130,7 +130,7 @@ class _UnparseVisitor(ast.NodeVisitor):
130
130
  def visit_Constant(self, node: ast.Constant) -> str:
131
131
  if node.value is Ellipsis:
132
132
  return "..."
133
- elif isinstance(node.value, (int, float, complex)):
133
+ elif isinstance(node.value, int | float | complex):
134
134
  if self.code:
135
135
  return ast.get_source_segment(self.code, node) or repr(node.value)
136
136
  else:
@@ -141,7 +141,7 @@ class _UnparseVisitor(ast.NodeVisitor):
141
141
  def visit_Dict(self, node: ast.Dict) -> str:
142
142
  keys = (self.visit(k) for k in node.keys if k is not None)
143
143
  values = (self.visit(v) for v in node.values)
144
- items = (k + ": " + v for k, v in zip(keys, values))
144
+ items = (k + ": " + v for k, v in zip(keys, values, strict=True))
145
145
  return "{" + ", ".join(items) + "}"
146
146
 
147
147
  def visit_Lambda(self, node: ast.Lambda) -> str:
sphinx/pycode/parser.py CHANGED
@@ -108,7 +108,7 @@ class Token:
108
108
  return self.kind == other
109
109
  elif isinstance(other, str):
110
110
  return self.value == other
111
- elif isinstance(other, (list, tuple)):
111
+ elif isinstance(other, list | tuple):
112
112
  return [self.kind, self.value] == list(other)
113
113
  elif other is None:
114
114
  return False
@@ -404,7 +404,7 @@ class VariableCommentPicker(ast.NodeVisitor):
404
404
 
405
405
  def visit_Expr(self, node: ast.Expr) -> None:
406
406
  """Handles Expr node and pick up a comment if string."""
407
- if (isinstance(self.previous, (ast.Assign, ast.AnnAssign)) and
407
+ if (isinstance(self.previous, ast.Assign | ast.AnnAssign) and
408
408
  isinstance(node.value, ast.Constant) and isinstance(node.value.value, str)):
409
409
  try:
410
410
  targets = get_assign_targets(self.previous)
sphinx/pygments_styles.py CHANGED
@@ -28,12 +28,12 @@ class SphinxStyle(Style):
28
28
  background_color = '#eeffcc'
29
29
  default_style = ''
30
30
 
31
- styles = FriendlyStyle.styles
32
- styles.update({
31
+ styles = {
32
+ **FriendlyStyle.styles,
33
33
  Generic.Output: '#333',
34
34
  Comment: 'italic #408090',
35
35
  Number: '#208050',
36
- })
36
+ }
37
37
 
38
38
 
39
39
  class PyramidStyle(Style):
sphinx/registry.py CHANGED
@@ -2,16 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import sys
6
5
  import traceback
7
6
  from importlib import import_module
7
+ from importlib.metadata import entry_points
8
8
  from types import MethodType
9
- from typing import TYPE_CHECKING, Any, Callable
10
-
11
- if sys.version_info >= (3, 10):
12
- from importlib.metadata import entry_points
13
- else:
14
- from importlib_metadata import entry_points
9
+ from typing import TYPE_CHECKING, Any
15
10
 
16
11
  from sphinx.domains import Domain, Index, ObjType
17
12
  from sphinx.domains.std import GenericObject, Target
@@ -25,7 +20,7 @@ from sphinx.util import logging
25
20
  from sphinx.util.logging import prefixed_warnings
26
21
 
27
22
  if TYPE_CHECKING:
28
- from collections.abc import Iterator, Sequence
23
+ from collections.abc import Callable, Iterator, Sequence
29
24
 
30
25
  from docutils import nodes
31
26
  from docutils.core import Publisher
sphinx/search/__init__.py CHANGED
@@ -487,7 +487,7 @@ class IndexBuilder:
487
487
  self._index_entries[docname] = sorted(_index_entries)
488
488
 
489
489
  def _word_collector(self, doctree: nodes.document) -> WordStore:
490
- def _visit_nodes(node):
490
+ def _visit_nodes(node: nodes.Node) -> None:
491
491
  if isinstance(node, nodes.comment):
492
492
  return
493
493
  elif isinstance(node, nodes.raw):
sphinx/testing/path.py CHANGED
@@ -4,12 +4,13 @@ import os
4
4
  import shutil
5
5
  import sys
6
6
  import warnings
7
- from typing import IO, TYPE_CHECKING, Any, Callable
7
+ from typing import IO, TYPE_CHECKING, Any
8
8
 
9
9
  from sphinx.deprecation import RemovedInSphinx90Warning
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  import builtins
13
+ from collections.abc import Callable
13
14
 
14
15
  warnings.warn("'sphinx.testing.path' is deprecated. "
15
16
  "Use 'os.path' or 'pathlib' instead.",
sphinx/testing/util.py CHANGED
@@ -43,7 +43,7 @@ def assert_node(node: Node, cls: Any = None, xpath: str = "", **kwargs: Any) ->
43
43
  'The node%s has %d child nodes, not one' % (xpath, len(node))
44
44
  assert_node(node[0], cls[1:], xpath=xpath + "[0]", **kwargs)
45
45
  elif isinstance(cls, tuple):
46
- assert isinstance(node, (list, nodes.Element)), \
46
+ assert isinstance(node, list | nodes.Element), \
47
47
  'The node%s does not have any items' % xpath
48
48
  assert len(node) == len(cls), \
49
49
  'The node%s has %d child nodes, not %r' % (xpath, len(node), len(cls))
@@ -77,7 +77,8 @@ tar: all-$(FMT)
77
77
  rm -r $(ARCHIVEPREFIX)docs-$(FMT)
78
78
 
79
79
  gz: tar
80
- gzip -9 < $(ARCHIVEPREFIX)docs-$(FMT).tar > $(ARCHIVEPREFIX)docs-$(FMT).tar.gz
80
+ # -n to omit mtime from gzip headers
81
+ gzip -n -9 < $(ARCHIVEPREFIX)docs-$(FMT).tar > $(ARCHIVEPREFIX)docs-$(FMT).tar.gz
81
82
 
82
83
  bz2: tar
83
84
  bzip2 -9 -k $(ARCHIVEPREFIX)docs-$(FMT).tar
@@ -49,7 +49,8 @@ tar: all-$(FMT)
49
49
  rm -r $(ARCHIVEPREFIX)docs-$(FMT)
50
50
 
51
51
  gz: tar
52
- gzip -9 < $(ARCHIVEPREFIX)docs-$(FMT).tar > $(ARCHIVEPREFIX)docs-$(FMT).tar.gz
52
+ # -n to omit mtime from gzip headers
53
+ gzip -n -9 < $(ARCHIVEPREFIX)docs-$(FMT).tar > $(ARCHIVEPREFIX)docs-$(FMT).tar.gz
53
54
 
54
55
  bz2: tar
55
56
  bzip2 -9 -k $(ARCHIVEPREFIX)docs-$(FMT).tar