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/util/inventory.py CHANGED
@@ -2,11 +2,13 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import os
5
+ import posixpath
6
6
  import re
7
+ import warnings
7
8
  import zlib
8
9
  from typing import TYPE_CHECKING
9
10
 
11
+ from sphinx.deprecation import RemovedInSphinx10Warning
10
12
  from sphinx.locale import __
11
13
  from sphinx.util import logging
12
14
 
@@ -14,127 +16,101 @@ BUFSIZE = 16 * 1024
14
16
  logger = logging.getLogger(__name__)
15
17
 
16
18
  if TYPE_CHECKING:
17
- from collections.abc import Callable, Iterator
19
+ import os
20
+ from collections.abc import Callable, Iterator, Sequence
21
+ from typing import NoReturn, Protocol
18
22
 
19
23
  from sphinx.builders import Builder
20
24
  from sphinx.environment import BuildEnvironment
21
- from sphinx.util.typing import Inventory, InventoryItem, _ReadableStream
22
-
23
-
24
- class InventoryFileReader:
25
- """A file reader for an inventory file.
26
-
27
- This reader supports mixture of texts and compressed texts.
28
- """
29
-
30
- def __init__(self, stream: _ReadableStream[bytes]) -> None:
31
- self.stream = stream
32
- self.buffer = b''
33
- self.eof = False
34
-
35
- def read_buffer(self) -> None:
36
- chunk = self.stream.read(BUFSIZE)
37
- if chunk == b'':
38
- self.eof = True
39
- self.buffer += chunk
40
-
41
- def readline(self) -> str:
42
- pos = self.buffer.find(b'\n')
43
- if pos != -1:
44
- line = self.buffer[:pos].decode()
45
- self.buffer = self.buffer[pos + 1 :]
46
- elif self.eof:
47
- line = self.buffer.decode()
48
- self.buffer = b''
49
- else:
50
- self.read_buffer()
51
- line = self.readline()
52
-
53
- return line
54
-
55
- def readlines(self) -> Iterator[str]:
56
- while not self.eof:
57
- line = self.readline()
58
- if line:
59
- yield line
60
-
61
- def read_compressed_chunks(self) -> Iterator[bytes]:
62
- decompressor = zlib.decompressobj()
63
- while not self.eof:
64
- self.read_buffer()
65
- yield decompressor.decompress(self.buffer)
66
- self.buffer = b''
67
- yield decompressor.flush()
68
-
69
- def read_compressed_lines(self) -> Iterator[str]:
70
- buf = b''
71
- for chunk in self.read_compressed_chunks():
72
- buf += chunk
73
- pos = buf.find(b'\n')
74
- while pos != -1:
75
- yield buf[:pos].decode()
76
- buf = buf[pos + 1 :]
77
- pos = buf.find(b'\n')
25
+ from sphinx.util.typing import Inventory
26
+
27
+ # Readable file stream for inventory loading
28
+ class _SupportsRead(Protocol):
29
+ def read(self, size: int = ...) -> bytes: ...
30
+
31
+ _JoinFunc = Callable[[str, str], str]
32
+
33
+
34
+ def __getattr__(name: str) -> object:
35
+ if name == 'InventoryFileReader':
36
+ from sphinx.util._inventory_file_reader import InventoryFileReader
37
+
38
+ return InventoryFileReader
39
+ msg = f'module {__name__!r} has no attribute {name!r}'
40
+ raise AttributeError(msg)
78
41
 
79
42
 
80
43
  class InventoryFile:
81
44
  @classmethod
82
- def load(
83
- cls: type[InventoryFile],
84
- stream: _ReadableStream[bytes],
45
+ def loads(
46
+ cls,
47
+ content: bytes,
48
+ *,
85
49
  uri: str,
86
- joinfunc: Callable[[str, str], str],
87
- ) -> Inventory:
88
- reader = InventoryFileReader(stream)
89
- line = reader.readline().rstrip()
90
- if line == '# Sphinx inventory version 1':
91
- return cls.load_v1(reader, uri, joinfunc)
92
- elif line == '# Sphinx inventory version 2':
93
- return cls.load_v2(reader, uri, joinfunc)
94
- else:
95
- raise ValueError('invalid inventory header: %s' % line)
50
+ ) -> _Inventory:
51
+ format_line, _, content = content.partition(b'\n')
52
+ format_line = format_line.rstrip() # remove trailing \r or spaces
53
+ if format_line == b'# Sphinx inventory version 2':
54
+ return cls._loads_v2(content, uri=uri)
55
+ if format_line == b'# Sphinx inventory version 1':
56
+ lines = content.decode().splitlines()
57
+ return cls._loads_v1(lines, uri=uri)
58
+ if format_line.startswith(b'# Sphinx inventory version '):
59
+ unknown_version = format_line[27:].decode()
60
+ msg = f'unknown or unsupported inventory version: {unknown_version!r}'
61
+ raise ValueError(msg)
62
+ msg = f'invalid inventory header: {format_line.decode()}'
63
+ raise ValueError(msg)
96
64
 
97
65
  @classmethod
98
- def load_v1(
99
- cls: type[InventoryFile],
100
- stream: InventoryFileReader,
101
- uri: str,
102
- join: Callable[[str, str], str],
103
- ) -> Inventory:
104
- invdata: Inventory = {}
105
- projname = stream.readline().rstrip()[11:]
106
- version = stream.readline().rstrip()[11:]
107
- for line in stream.readlines():
108
- name, type, location = line.rstrip().split(None, 2)
109
- location = join(uri, location)
66
+ def load(cls, stream: _SupportsRead, uri: str, joinfunc: _JoinFunc) -> Inventory:
67
+ return cls.loads(stream.read(), uri=uri).data
68
+
69
+ @classmethod
70
+ def _loads_v1(cls, lines: Sequence[str], *, uri: str) -> _Inventory:
71
+ if len(lines) < 2:
72
+ msg = 'invalid inventory header: missing project name or version'
73
+ raise ValueError(msg)
74
+ inv = _Inventory({})
75
+ projname = lines[0].rstrip()[11:] # Project name
76
+ version = lines[1].rstrip()[11:] # Project version
77
+ for line in lines[2:]:
78
+ name, item_type, location = line.rstrip().split(None, 2)
79
+ location = posixpath.join(uri, location)
110
80
  # version 1 did not add anchors to the location
111
- if type == 'mod':
112
- type = 'py:module'
113
- location += '#module-' + name
81
+ if item_type == 'mod':
82
+ item_type = 'py:module'
83
+ location += f'#module-{name}'
114
84
  else:
115
- type = 'py:' + type
116
- location += '#' + name
117
- invdata.setdefault(type, {})[name] = (projname, version, location, '-')
118
- return invdata
85
+ item_type = f'py:{item_type}'
86
+ location += f'#{name}'
87
+ inv[item_type, name] = _InventoryItem(
88
+ project_name=projname,
89
+ project_version=version,
90
+ uri=location,
91
+ display_name='-',
92
+ )
93
+ return inv
119
94
 
120
95
  @classmethod
121
- def load_v2(
122
- cls: type[InventoryFile],
123
- stream: InventoryFileReader,
124
- uri: str,
125
- join: Callable[[str, str], str],
126
- ) -> Inventory:
127
- invdata: Inventory = {}
128
- projname = stream.readline().rstrip()[11:]
129
- version = stream.readline().rstrip()[11:]
130
- # definition -> priority, location, display name
96
+ def _loads_v2(cls, inv_data: bytes, *, uri: str) -> _Inventory:
97
+ try:
98
+ line_1, line_2, check_line, compressed = inv_data.split(b'\n', maxsplit=3)
99
+ except ValueError:
100
+ msg = 'invalid inventory header: missing project name or version'
101
+ raise ValueError(msg) from None
102
+ inv = _Inventory({})
103
+ projname = line_1.rstrip()[11:].decode() # Project name
104
+ version = line_2.rstrip()[11:].decode() # Project version
105
+ # definition -> (priority, location, display name)
131
106
  potential_ambiguities: dict[str, tuple[str, str, str]] = {}
132
107
  actual_ambiguities = set()
133
- line = stream.readline()
134
- if 'zlib' not in line:
135
- raise ValueError('invalid inventory header (not compressed): %s' % line)
108
+ if b'zlib' not in check_line: # '... compressed using zlib'
109
+ msg = f'invalid inventory header (not compressed): {check_line.decode()}'
110
+ raise ValueError(msg)
136
111
 
137
- for line in stream.read_compressed_lines():
112
+ decompressed_content = zlib.decompress(compressed)
113
+ for line in decompressed_content.decode().splitlines():
138
114
  # be careful to handle names with embedded spaces correctly
139
115
  m = re.match(
140
116
  r'(.+?)\s+(\S+)\s+(-?\d+)\s+?(\S*)\s+(.*)',
@@ -147,9 +123,10 @@ class InventoryFile:
147
123
  if ':' not in type:
148
124
  # wrong type value. type should be in the form of "{domain}:{objtype}"
149
125
  #
150
- # Note: To avoid the regex DoS, this is implemented in python (refs: #8175)
126
+ # Note: To avoid the regex DoS, this is implemented in Python
127
+ # See: https://github.com/sphinx-doc/sphinx/issues/8175
151
128
  continue
152
- if type == 'py:module' and type in invdata and name in invdata[type]:
129
+ if type == 'py:module' and (type, name) in inv:
153
130
  # due to a bug in 1.1 and below,
154
131
  # two inventory entries are created
155
132
  # for Python modules, and the first
@@ -177,9 +154,13 @@ class InventoryFile:
177
154
  potential_ambiguities[lowercase_definition] = content
178
155
  if location.endswith('$'):
179
156
  location = location[:-1] + name
180
- location = join(uri, location)
181
- inv_item: InventoryItem = projname, version, location, dispname
182
- invdata.setdefault(type, {})[name] = inv_item
157
+ location = posixpath.join(uri, location)
158
+ inv[type, name] = _InventoryItem(
159
+ project_name=projname,
160
+ project_version=version,
161
+ uri=location,
162
+ display_name=dispname,
163
+ )
183
164
  for ambiguity in actual_ambiguities:
184
165
  logger.info(
185
166
  __('inventory <%s> contains multiple definitions for %s'),
@@ -188,19 +169,16 @@ class InventoryFile:
188
169
  type='intersphinx',
189
170
  subtype='external',
190
171
  )
191
- return invdata
172
+ return inv
192
173
 
193
174
  @classmethod
194
175
  def dump(
195
- cls: type[InventoryFile],
196
- filename: str,
197
- env: BuildEnvironment,
198
- builder: Builder,
176
+ cls, filename: str | os.PathLike[str], env: BuildEnvironment, builder: Builder
199
177
  ) -> None:
200
178
  def escape(string: str) -> str:
201
179
  return re.sub('\\s+', ' ', string)
202
180
 
203
- with open(os.path.join(filename), 'wb') as f:
181
+ with open(filename, 'wb') as f:
204
182
  # header
205
183
  f.write(
206
184
  (
@@ -227,3 +205,124 @@ class InventoryFile:
227
205
  entry = f'{fullname} {domain.name}:{type} {prio} {uri} {dispname}\n'
228
206
  f.write(compressor.compress(entry.encode()))
229
207
  f.write(compressor.flush())
208
+
209
+
210
+ class _Inventory:
211
+ """Inventory data in memory."""
212
+
213
+ __slots__ = ('data',)
214
+
215
+ data: dict[str, dict[str, _InventoryItem]]
216
+
217
+ def __init__(self, data: dict[str, dict[str, _InventoryItem]], /) -> None:
218
+ # type -> name -> _InventoryItem
219
+ self.data: dict[str, dict[str, _InventoryItem]] = data
220
+
221
+ def __repr__(self) -> str:
222
+ return f'_Inventory({self.data!r})'
223
+
224
+ def __eq__(self, other: object) -> bool:
225
+ if not isinstance(other, _Inventory):
226
+ return NotImplemented
227
+ return self.data == other.data
228
+
229
+ def __hash__(self) -> int:
230
+ return hash(self.data)
231
+
232
+ def __getitem__(self, item: tuple[str, str]) -> _InventoryItem:
233
+ obj_type, name = item
234
+ return self.data.setdefault(obj_type, {})[name]
235
+
236
+ def __setitem__(self, item: tuple[str, str], value: _InventoryItem) -> None:
237
+ obj_type, name = item
238
+ self.data.setdefault(obj_type, {})[name] = value
239
+
240
+ def __contains__(self, item: tuple[str, str]) -> bool:
241
+ obj_type, name = item
242
+ return obj_type in self.data and name in self.data[obj_type]
243
+
244
+
245
+ class _InventoryItem:
246
+ __slots__ = 'project_name', 'project_version', 'uri', 'display_name'
247
+
248
+ project_name: str
249
+ project_version: str
250
+ uri: str
251
+ display_name: str
252
+
253
+ def __init__(
254
+ self,
255
+ *,
256
+ project_name: str,
257
+ project_version: str,
258
+ uri: str,
259
+ display_name: str,
260
+ ) -> None:
261
+ object.__setattr__(self, 'project_name', project_name)
262
+ object.__setattr__(self, 'project_version', project_version)
263
+ object.__setattr__(self, 'uri', uri)
264
+ object.__setattr__(self, 'display_name', display_name)
265
+
266
+ def __repr__(self) -> str:
267
+ return (
268
+ '_InventoryItem('
269
+ f'project_name={self.project_name!r}, '
270
+ f'project_version={self.project_version!r}, '
271
+ f'uri={self.uri!r}, '
272
+ f'display_name={self.display_name!r}'
273
+ ')'
274
+ )
275
+
276
+ def __eq__(self, other: object) -> bool:
277
+ if not isinstance(other, _InventoryItem):
278
+ return NotImplemented
279
+ return (
280
+ self.project_name == other.project_name
281
+ and self.project_version == other.project_version
282
+ and self.uri == other.uri
283
+ and self.display_name == other.display_name
284
+ )
285
+
286
+ def __hash__(self) -> int:
287
+ return hash((
288
+ self.project_name,
289
+ self.project_version,
290
+ self.uri,
291
+ self.display_name,
292
+ ))
293
+
294
+ def __setattr__(self, key: str, value: object) -> NoReturn:
295
+ msg = '_InventoryItem is immutable'
296
+ raise AttributeError(msg)
297
+
298
+ def __delattr__(self, key: str) -> NoReturn:
299
+ msg = '_InventoryItem is immutable'
300
+ raise AttributeError(msg)
301
+
302
+ def __getstate__(self) -> tuple[str, str, str, str]:
303
+ return self.project_name, self.project_version, self.uri, self.display_name
304
+
305
+ def __setstate__(self, state: tuple[str, str, str, str]) -> None:
306
+ project_name, project_version, uri, display_name = state
307
+ object.__setattr__(self, 'project_name', project_name)
308
+ object.__setattr__(self, 'project_version', project_version)
309
+ object.__setattr__(self, 'uri', uri)
310
+ object.__setattr__(self, 'display_name', display_name)
311
+
312
+ def __getitem__(self, key: int | slice) -> str | tuple[str, ...]:
313
+ warnings.warn(
314
+ 'The tuple interface for _InventoryItem objects is deprecated.',
315
+ RemovedInSphinx10Warning,
316
+ stacklevel=2,
317
+ )
318
+ tpl = self.project_name, self.project_version, self.uri, self.display_name
319
+ return tpl[key]
320
+
321
+ def __iter__(self) -> Iterator[str]:
322
+ warnings.warn(
323
+ 'The iter() interface for _InventoryItem objects is deprecated.',
324
+ RemovedInSphinx10Warning,
325
+ stacklevel=2,
326
+ )
327
+ tpl = self.project_name, self.project_version, self.uri, self.display_name
328
+ return iter(tpl)
sphinx/util/logging.py CHANGED
@@ -4,20 +4,20 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  import logging.handlers
7
+ import os.path
7
8
  from collections import defaultdict
8
9
  from contextlib import contextmanager, nullcontext
9
- from typing import IO, TYPE_CHECKING, Any
10
+ from typing import TYPE_CHECKING
10
11
 
11
12
  from docutils import nodes
12
13
  from docutils.utils import get_source_line
13
14
 
15
+ from sphinx._cli.util.colour import colourise
14
16
  from sphinx.errors import SphinxWarning
15
- from sphinx.util.console import colorize
16
- from sphinx.util.osutil import abspath
17
17
 
18
18
  if TYPE_CHECKING:
19
- from collections.abc import Iterator, Sequence, Set
20
- from typing import NoReturn
19
+ from collections.abc import Iterator, Mapping, Sequence, Set
20
+ from typing import IO, Any, NoReturn
21
21
 
22
22
  from docutils.nodes import Node
23
23
 
@@ -49,14 +49,11 @@ VERBOSITY_MAP: defaultdict[int, int] = defaultdict(
49
49
  },
50
50
  )
51
51
 
52
- COLOR_MAP: defaultdict[int, str] = defaultdict(
53
- lambda: 'blue',
54
- {
55
- logging.ERROR: 'darkred',
56
- logging.WARNING: 'red',
57
- logging.DEBUG: 'darkgray',
58
- },
59
- )
52
+ COLOR_MAP: dict[int, str] = {
53
+ logging.ERROR: 'darkred',
54
+ logging.WARNING: 'red',
55
+ logging.DEBUG: 'darkgray',
56
+ }
60
57
 
61
58
 
62
59
  def getLogger(name: str) -> SphinxLoggerAdapter:
@@ -129,7 +126,7 @@ class SphinxWarningLogRecord(SphinxLogRecord):
129
126
  return 'WARNING: '
130
127
 
131
128
 
132
- class SphinxLoggerAdapter(logging.LoggerAdapter):
129
+ class SphinxLoggerAdapter(logging.LoggerAdapter[logging.Logger]):
133
130
  """LoggerAdapter allowing ``type`` and ``subtype`` keywords."""
134
131
 
135
132
  KEYWORDS = ['type', 'subtype', 'location', 'nonl', 'color', 'once']
@@ -146,7 +143,7 @@ class SphinxLoggerAdapter(logging.LoggerAdapter):
146
143
  def verbose(self, msg: str, *args: Any, **kwargs: Any) -> None:
147
144
  self.log(VERBOSE, msg, *args, **kwargs)
148
145
 
149
- def process(self, msg: str, kwargs: dict) -> tuple[str, dict]: # type: ignore[override]
146
+ def process(self, msg: str, kwargs: dict[str, Any]) -> tuple[str, dict[str, Any]]: # type: ignore[override]
150
147
  extra = kwargs.setdefault('extra', {})
151
148
  for keyword in self.KEYWORDS:
152
149
  if keyword in kwargs:
@@ -161,9 +158,9 @@ class SphinxLoggerAdapter(logging.LoggerAdapter):
161
158
  self,
162
159
  msg: object,
163
160
  *args: object,
164
- type: None | str = None,
165
- subtype: None | str = None,
166
- location: None | str | tuple[str | None, int | None] | Node = None,
161
+ type: str | None = None,
162
+ subtype: str | None = None,
163
+ location: str | tuple[str | None, int | None] | Node | None = None,
167
164
  nonl: bool = True,
168
165
  color: str | None = None,
169
166
  once: bool = False,
@@ -204,13 +201,13 @@ class SphinxLoggerAdapter(logging.LoggerAdapter):
204
201
  )
205
202
 
206
203
 
207
- class WarningStreamHandler(logging.StreamHandler):
204
+ class WarningStreamHandler(logging.StreamHandler['SafeEncodingWriter']):
208
205
  """StreamHandler for warnings."""
209
206
 
210
207
  pass
211
208
 
212
209
 
213
- class NewLineStreamHandler(logging.StreamHandler):
210
+ class NewLineStreamHandler(logging.StreamHandler['SafeEncodingWriter']):
214
211
  """StreamHandler which switches line terminator by record.nonl flag."""
215
212
 
216
213
  def emit(self, record: logging.LogRecord) -> None:
@@ -471,7 +468,9 @@ class OnceFilter(logging.Filter):
471
468
 
472
469
  def __init__(self, name: str = '') -> None:
473
470
  super().__init__(name)
474
- self.messages: dict[str, list] = {}
471
+ self.messages: dict[
472
+ str, list[tuple[object, ...] | Mapping[str, object] | None]
473
+ ] = {}
475
474
 
476
475
  def filter(self, record: logging.LogRecord) -> bool:
477
476
  once = getattr(record, 'once', '')
@@ -555,9 +554,9 @@ class WarningLogRecordTranslator(SphinxLogRecordTranslator):
555
554
  def get_node_location(node: Node) -> str | None:
556
555
  source, line = get_source_line(node)
557
556
  if source and line:
558
- return f'{abspath(source)}:{line}'
557
+ return f'{os.path.abspath(source)}:{line}'
559
558
  if source:
560
- return f'{abspath(source)}:'
559
+ return f'{os.path.abspath(source)}:'
561
560
  if line:
562
561
  return f'<unknown>:{line}'
563
562
  return None
@@ -566,20 +565,21 @@ def get_node_location(node: Node) -> str | None:
566
565
  class ColorizeFormatter(logging.Formatter):
567
566
  def format(self, record: logging.LogRecord) -> str:
568
567
  message = super().format(record)
569
- color = getattr(record, 'color', None)
570
- if color is None:
571
- color = COLOR_MAP.get(record.levelno)
572
-
573
- if color:
574
- return colorize(color, message)
575
- else:
568
+ colour_name = getattr(record, 'color', '')
569
+ if not colour_name:
570
+ colour_name = COLOR_MAP.get(record.levelno, '')
571
+ if not colour_name:
572
+ return message
573
+ try:
574
+ return colourise(colour_name, message)
575
+ except ValueError:
576
576
  return message
577
577
 
578
578
 
579
579
  class SafeEncodingWriter:
580
580
  """Stream writer which ignores UnicodeEncodeError silently"""
581
581
 
582
- def __init__(self, stream: IO) -> None:
582
+ def __init__(self, stream: IO[str]) -> None:
583
583
  self.stream = stream
584
584
  self.encoding = getattr(stream, 'encoding', 'ascii') or 'ascii'
585
585
 
@@ -601,14 +601,14 @@ class SafeEncodingWriter:
601
601
  class LastMessagesWriter:
602
602
  """Stream writer storing last 10 messages in memory to save trackback"""
603
603
 
604
- def __init__(self, app: Sphinx, stream: IO) -> None:
604
+ def __init__(self, app: Sphinx, stream: IO[str]) -> None:
605
605
  self.app = app
606
606
 
607
607
  def write(self, data: str) -> None:
608
608
  self.app.messagelog.append(data)
609
609
 
610
610
 
611
- def setup(app: Sphinx, status: IO, warning: IO) -> None:
611
+ def setup(app: Sphinx, status: IO[str], warning: IO[str]) -> None:
612
612
  """Setup root logger for Sphinx"""
613
613
  logger = logging.getLogger(NAMESPACE)
614
614
  logger.setLevel(logging.DEBUG)
sphinx/util/matching.py CHANGED
@@ -4,9 +4,11 @@ from __future__ import annotations
4
4
 
5
5
  import os.path
6
6
  import re
7
+ import unicodedata
8
+ from pathlib import Path
7
9
  from typing import TYPE_CHECKING
8
10
 
9
- from sphinx.util.osutil import canon_path, path_stabilize
11
+ from sphinx.util.osutil import canon_path
10
12
 
11
13
  if TYPE_CHECKING:
12
14
  from collections.abc import Callable, Iterable, Iterator
@@ -125,7 +127,7 @@ def get_matching_files(
125
127
 
126
128
  """
127
129
  # dirname is a normalized absolute path.
128
- dirname = os.path.normpath(os.path.abspath(dirname))
130
+ dirname = Path(dirname).resolve()
129
131
 
130
132
  exclude_matchers = compile_matchers(exclude_patterns)
131
133
  include_matchers = compile_matchers(include_patterns)
@@ -134,11 +136,12 @@ def get_matching_files(
134
136
  relative_root = os.path.relpath(root, dirname)
135
137
  if relative_root == '.':
136
138
  relative_root = '' # suppress dirname for files on the target dir
139
+ relative_root_path = Path(relative_root)
137
140
 
138
141
  # Filter files
139
142
  included_files = []
140
143
  for entry in sorted(files):
141
- entry = path_stabilize(os.path.join(relative_root, entry))
144
+ entry = _unicode_nfc((relative_root_path / entry).as_posix())
142
145
  keep = False
143
146
  for matcher in include_matchers:
144
147
  if matcher(entry):
@@ -156,7 +159,7 @@ def get_matching_files(
156
159
  # Filter directories
157
160
  filtered_dirs = []
158
161
  for dir_name in sorted(dirs):
159
- normalised = path_stabilize(os.path.join(relative_root, dir_name))
162
+ normalised = _unicode_nfc((relative_root_path / dir_name).as_posix())
160
163
  for matcher in exclude_matchers:
161
164
  if matcher(normalised):
162
165
  break # break the inner loop
@@ -168,3 +171,8 @@ def get_matching_files(
168
171
 
169
172
  # Yield filtered files
170
173
  yield from included_files
174
+
175
+
176
+ def _unicode_nfc(s: str, /) -> str:
177
+ """Normalise the string to NFC form."""
178
+ return unicodedata.normalize('NFC', s)