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
sphinx/theming.py CHANGED
@@ -10,6 +10,7 @@ import os
10
10
  import shutil
11
11
  import sys
12
12
  import tempfile
13
+ from importlib.metadata import entry_points
13
14
  from os import path
14
15
  from typing import TYPE_CHECKING, Any
15
16
  from zipfile import ZipFile
@@ -26,10 +27,6 @@ if sys.version_info >= (3, 11):
26
27
  else:
27
28
  import tomli as tomllib
28
29
 
29
- if sys.version_info >= (3, 10):
30
- from importlib.metadata import entry_points
31
- else:
32
- from importlib_metadata import entry_points
33
30
 
34
31
  if TYPE_CHECKING:
35
32
  from collections.abc import Callable
@@ -121,17 +118,11 @@ class Theme:
121
118
  elif section == 'options':
122
119
  value = self._options.get(name, default)
123
120
  else:
124
- # https://github.com/sphinx-doc/sphinx/issues/12305
125
- # For backwards compatibility when attempting to read a value
126
- # from an unsupported configuration section.
127
- # xref: RemovedInSphinx80Warning
128
121
  msg = __(
129
122
  'Theme configuration sections other than [theme] and [options] '
130
- 'are not supported, returning the default value instead '
131
- '(tried to get a value from %r)'
123
+ 'are not supported (tried to get a value from %r).'
132
124
  )
133
- logger.info(msg, section)
134
- value = default
125
+ raise ThemeError(msg)
135
126
  if value is _NO_DEFAULT:
136
127
  msg = __('setting %s.%s occurs in none of the searched theme configs') % (
137
128
  section,
@@ -22,10 +22,10 @@ from sphinx.util.nodes import apply_source_workaround, is_smartquotable
22
22
 
23
23
  if TYPE_CHECKING:
24
24
  from collections.abc import Iterator
25
- from typing import Literal
25
+ from typing import Literal, TypeAlias
26
26
 
27
27
  from docutils.nodes import Node, Text
28
- from typing_extensions import TypeAlias, TypeIs
28
+ from typing_extensions import TypeIs
29
29
 
30
30
  from sphinx.application import Sphinx
31
31
  from sphinx.config import Config
@@ -247,7 +247,7 @@ class ApplySourceWorkaround(SphinxTransform):
247
247
 
248
248
  def apply(self, **kwargs: Any) -> None:
249
249
  for node in self.document.findall(): # type: Node
250
- if isinstance(node, (nodes.TextElement, nodes.image, nodes.topic)):
250
+ if isinstance(node, nodes.TextElement | nodes.image | nodes.topic):
251
251
  apply_source_workaround(node)
252
252
 
253
253
 
@@ -364,7 +364,7 @@ class SphinxSmartQuotes(SmartQuotes, SphinxTransform):
364
364
  # override default settings with :confval:`smartquotes_action`
365
365
  self.smartquotes_action = self.config.smartquotes_action
366
366
 
367
- super().apply()
367
+ super().apply() # type: ignore[no-untyped-call]
368
368
 
369
369
  def is_available(self) -> bool:
370
370
  builders = self.config.smartquotes_excludes.get('builders', [])
@@ -477,7 +477,7 @@ def _reorder_index_target_nodes(start_node: nodes.target) -> None:
477
477
  # as we want *consecutive* target & index nodes.
478
478
  node: nodes.Node
479
479
  for node in start_node.findall(descend=False, siblings=True):
480
- if isinstance(node, (nodes.target, addnodes.index)):
480
+ if isinstance(node, nodes.target | addnodes.index):
481
481
  nodes_to_reorder.append(node)
482
482
  continue
483
483
  break # must be a consecutive run of target or index nodes
@@ -23,7 +23,7 @@ class SphinxDanglingReferences(DanglingReferences):
23
23
 
24
24
  # suppress INFO level messages for a while
25
25
  reporter.report_level = max(reporter.WARNING_LEVEL, reporter.report_level)
26
- super().apply()
26
+ super().apply() # type: ignore[no-untyped-call]
27
27
  finally:
28
28
  reporter.report_level = report_level
29
29
 
sphinx/util/__init__.py CHANGED
@@ -13,12 +13,8 @@ from urllib.parse import parse_qsl, quote_plus, urlencode, urlsplit, urlunsplit
13
13
 
14
14
  from sphinx.errors import ExtensionError, FiletypeNotFoundError
15
15
  from sphinx.locale import __
16
- from sphinx.util import display as _display
17
- from sphinx.util import exceptions as _exceptions
18
- from sphinx.util import http_date as _http_date
19
16
  from sphinx.util import index_entries as _index_entries
20
17
  from sphinx.util import logging
21
- from sphinx.util import osutil as _osutil
22
18
  from sphinx.util.console import strip_colors # NoQA: F401
23
19
  from sphinx.util.matching import patfilter # NoQA: F401
24
20
  from sphinx.util.nodes import ( # NoQA: F401
@@ -33,10 +29,8 @@ from sphinx.util.nodes import ( # NoQA: F401
33
29
  from sphinx.util.osutil import ( # NoQA: F401
34
30
  SEP,
35
31
  copyfile,
36
- copytimes,
37
32
  ensuredir,
38
33
  make_filename,
39
- mtimes_of_files,
40
34
  os_path,
41
35
  relative_uri,
42
36
  )
@@ -54,9 +48,9 @@ def docname_join(basedocname: str, docname: str) -> str:
54
48
  return posixpath.normpath(posixpath.join('/' + basedocname, '..', docname))[1:]
55
49
 
56
50
 
57
- def get_filetype(source_suffix: dict[str, str], filename: str) -> str:
51
+ def get_filetype(source_suffix: dict[str, str], filename: str | os.PathLike) -> str:
58
52
  for suffix, filetype in source_suffix.items():
59
- if filename.endswith(suffix):
53
+ if os.fspath(filename).endswith(suffix):
60
54
  # If default filetype (None), considered as restructuredtext.
61
55
  return filetype or 'restructuredtext'
62
56
  raise FiletypeNotFoundError
@@ -258,32 +252,16 @@ def isurl(url: str) -> bool:
258
252
  return bool(url) and '://' in url
259
253
 
260
254
 
261
- def _xml_name_checker() -> re.Pattern[str]:
262
- # to prevent import cycles
263
- from sphinx.builders.epub3 import _XML_NAME_PATTERN
264
-
265
- return _XML_NAME_PATTERN
266
-
267
-
268
255
  # deprecated name -> (object to return, canonical path or empty string)
269
- _DEPRECATED_OBJECTS: dict[str, tuple[Any, str] | tuple[Any, str, tuple[int, int]]] = {
270
- 'path_stabilize': (_osutil.path_stabilize, 'sphinx.util.osutil.path_stabilize'),
271
- 'display_chunk': (_display.display_chunk, 'sphinx.util.display.display_chunk'),
272
- 'status_iterator': (_display.status_iterator, 'sphinx.util.display.status_iterator'),
273
- 'SkipProgressMessage': (_display.SkipProgressMessage,
274
- 'sphinx.util.display.SkipProgressMessage'),
275
- 'progress_message': (_display.progress_message, 'sphinx.util.display.progress_message'),
276
- 'epoch_to_rfc1123': (_http_date.epoch_to_rfc1123, 'sphinx.http_date.epoch_to_rfc1123'),
277
- 'rfc1123_to_epoch': (_http_date.rfc1123_to_epoch, 'sphinx.http_date.rfc1123_to_epoch'),
278
- 'save_traceback': (_exceptions.save_traceback, 'sphinx.exceptions.save_traceback'),
279
- 'format_exception_cut_frames': (_exceptions.format_exception_cut_frames,
280
- 'sphinx.exceptions.format_exception_cut_frames'),
281
- 'xmlname_checker': (_xml_name_checker, 'sphinx.builders.epub3._XML_NAME_PATTERN'),
256
+ _DEPRECATED_OBJECTS: dict[str, tuple[Any, str, tuple[int, int]]] = {
282
257
  'split_index_msg': (_index_entries.split_index_msg,
283
- 'sphinx.util.index_entries.split_index_msg'),
284
- 'split_into': (_index_entries.split_index_msg, 'sphinx.util.index_entries.split_into'),
285
- 'md5': (_md5, ''),
286
- 'sha1': (_sha1, ''),
258
+ 'sphinx.util.index_entries.split_index_msg',
259
+ (9, 0)),
260
+ 'split_into': (_index_entries.split_index_msg,
261
+ 'sphinx.util.index_entries.split_into',
262
+ (9, 0)),
263
+ 'md5': (_md5, '', (9, 0)),
264
+ 'sha1': (_sha1, '', (9, 0)),
287
265
  }
288
266
 
289
267
 
@@ -294,8 +272,6 @@ def __getattr__(name: str) -> Any:
294
272
 
295
273
  from sphinx.deprecation import _deprecation_warning
296
274
 
297
- info = _DEPRECATED_OBJECTS[name]
298
- deprecated_object, canonical_name = info[:2]
299
- remove = info[2] if len(info) == 3 else (8, 0)
275
+ deprecated_object, canonical_name, remove = _DEPRECATED_OBJECTS[name]
300
276
  _deprecation_warning(__name__, name, canonical_name, remove=remove)
301
277
  return deprecated_object
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+
5
+
6
+ def _format_rfc3339_microseconds(timestamp: int, /) -> str:
7
+ """Return an RFC 3339 formatted string representing the given timestamp.
8
+
9
+ :param timestamp: The timestamp to format, in microseconds.
10
+ """
11
+ seconds, fraction = divmod(timestamp, 10**6)
12
+ return time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(seconds)) + f'.{fraction // 1_000}'
sphinx/util/cfamily.py CHANGED
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import re
6
6
  from copy import deepcopy
7
- from typing import TYPE_CHECKING, Any, Callable
7
+ from typing import TYPE_CHECKING
8
8
 
9
9
  from docutils import nodes
10
10
 
@@ -12,16 +12,16 @@ from sphinx import addnodes
12
12
  from sphinx.util import logging
13
13
 
14
14
  if TYPE_CHECKING:
15
- from collections.abc import Sequence
15
+ from collections.abc import Callable, Sequence
16
+ from typing import Any, TypeAlias
16
17
 
17
18
  from docutils.nodes import TextElement
18
19
 
19
20
  from sphinx.config import Config
20
21
 
21
- logger = logging.getLogger(__name__)
22
-
23
- StringifyTransform = Callable[[Any], str]
22
+ StringifyTransform: TypeAlias = Callable[[Any], str]
24
23
 
24
+ logger = logging.getLogger(__name__)
25
25
 
26
26
  _whitespace_re = re.compile(r'\s+')
27
27
  anon_identifier_re = re.compile(r'(@[a-zA-Z0-9_])[a-zA-Z0-9_]*\b')
sphinx/util/console.py CHANGED
@@ -41,8 +41,9 @@ if TYPE_CHECKING:
41
41
  try:
42
42
  # check if colorama is installed to support color on Windows
43
43
  import colorama
44
+ COLORAMA_AVAILABLE = True
44
45
  except ImportError:
45
- colorama = None
46
+ COLORAMA_AVAILABLE = False
46
47
 
47
48
  _CSI: Final[str] = re.escape('\x1b[') # 'ESC [': Control Sequence Introducer
48
49
 
@@ -92,7 +93,7 @@ def term_width_line(text: str) -> str:
92
93
  def color_terminal() -> bool:
93
94
  if 'NO_COLOR' in os.environ:
94
95
  return False
95
- if sys.platform == 'win32' and colorama is not None:
96
+ if sys.platform == 'win32' and COLORAMA_AVAILABLE:
96
97
  colorama.just_fix_windows_console()
97
98
  return True
98
99
  if 'FORCE_COLOR' in os.environ:
@@ -108,7 +109,7 @@ def color_terminal() -> bool:
108
109
 
109
110
 
110
111
  def nocolor() -> None:
111
- if sys.platform == 'win32' and colorama is not None:
112
+ if sys.platform == 'win32' and COLORAMA_AVAILABLE:
112
113
  colorama.deinit()
113
114
  codes.clear()
114
115
 
sphinx/util/display.py CHANGED
@@ -7,9 +7,9 @@ from sphinx.util import logging
7
7
  from sphinx.util.console import bold, color_terminal
8
8
 
9
9
  if False:
10
- from collections.abc import Iterable, Iterator
10
+ from collections.abc import Callable, Iterable, Iterator
11
11
  from types import TracebackType
12
- from typing import Any, Callable, TypeVar
12
+ from typing import Any, TypeVar
13
13
 
14
14
  from typing_extensions import ParamSpec
15
15
 
@@ -21,7 +21,7 @@ logger = logging.getLogger(__name__)
21
21
 
22
22
 
23
23
  def display_chunk(chunk: Any) -> str:
24
- if isinstance(chunk, (list, tuple)):
24
+ if isinstance(chunk, list | tuple):
25
25
  if len(chunk) == 1:
26
26
  return str(chunk[0])
27
27
  return f'{chunk[0]} .. {chunk[-1]}'
sphinx/util/docfields.py CHANGED
@@ -356,7 +356,7 @@ class DocFieldTransformer:
356
356
  if is_typefield:
357
357
  # filter out only inline nodes; others will result in invalid
358
358
  # markup being written out
359
- content = [n for n in content if isinstance(n, (nodes.Inline, nodes.Text))]
359
+ content = [n for n in content if isinstance(n, nodes.Inline | nodes.Text)]
360
360
  if content:
361
361
  types.setdefault(typename, {})[fieldarg] = content
362
362
  continue
sphinx/util/docutils.py CHANGED
@@ -8,7 +8,7 @@ from collections.abc import Sequence # NoQA: TCH003
8
8
  from contextlib import contextmanager
9
9
  from copy import copy
10
10
  from os import path
11
- from typing import IO, TYPE_CHECKING, Any, Callable, cast
11
+ from typing import IO, TYPE_CHECKING, Any, cast
12
12
 
13
13
  import docutils
14
14
  from docutils import nodes
@@ -27,7 +27,7 @@ logger = logging.getLogger(__name__)
27
27
  report_re = re.compile('^(.+?:(?:\\d+)?): \\((DEBUG|INFO|WARNING|ERROR|SEVERE)/(\\d+)?\\) ')
28
28
 
29
29
  if TYPE_CHECKING:
30
- from collections.abc import Iterator
30
+ from collections.abc import Callable, Iterator # NoQA: TCH003
31
31
  from types import ModuleType
32
32
 
33
33
  from docutils.frontend import Values
@@ -366,30 +366,47 @@ class SphinxDirective(Directive):
366
366
 
367
367
  This class provides helper methods for Sphinx directives.
368
368
 
369
+ .. versionadded:: 1.8
370
+
369
371
  .. note:: The subclasses of this class might not work with docutils.
370
372
  This class is strongly coupled with Sphinx.
371
373
  """
372
374
 
373
375
  @property
374
376
  def env(self) -> BuildEnvironment:
375
- """Reference to the :class:`.BuildEnvironment` object."""
377
+ """Reference to the :class:`.BuildEnvironment` object.
378
+
379
+ .. versionadded:: 1.8
380
+ """
376
381
  return self.state.document.settings.env
377
382
 
378
383
  @property
379
384
  def config(self) -> Config:
380
- """Reference to the :class:`.Config` object."""
385
+ """Reference to the :class:`.Config` object.
386
+
387
+ .. versionadded:: 1.8
388
+ """
381
389
  return self.env.config
382
390
 
383
391
  def get_source_info(self) -> tuple[str, int]:
384
- """Get source and line number."""
392
+ """Get source and line number.
393
+
394
+ .. versionadded:: 3.0
395
+ """
385
396
  return self.state_machine.get_source_and_line(self.lineno)
386
397
 
387
398
  def set_source_info(self, node: Node) -> None:
388
- """Set source and line number to the node."""
399
+ """Set source and line number to the node.
400
+
401
+ .. versionadded:: 2.1
402
+ """
389
403
  node.source, node.line = self.get_source_info()
390
404
 
391
405
  def get_location(self) -> str:
392
- """Get current location info for logging."""
406
+ """Get current location info for logging.
407
+
408
+ .. versionadded:: 4.2
409
+ """
393
410
  source, line = self.get_source_info()
394
411
  if source and line:
395
412
  return f'{source}:{line}'
@@ -473,6 +490,8 @@ class SphinxRole:
473
490
 
474
491
  This class provides helper methods for Sphinx roles.
475
492
 
493
+ .. versionadded:: 2.0
494
+
476
495
  .. note:: The subclasses of this class might not work with docutils.
477
496
  This class is strongly coupled with Sphinx.
478
497
  """
@@ -517,24 +536,35 @@ class SphinxRole:
517
536
 
518
537
  @property
519
538
  def env(self) -> BuildEnvironment:
520
- """Reference to the :class:`.BuildEnvironment` object."""
539
+ """Reference to the :class:`.BuildEnvironment` object.
540
+
541
+ .. versionadded:: 2.0
542
+ """
521
543
  return self.inliner.document.settings.env
522
544
 
523
545
  @property
524
546
  def config(self) -> Config:
525
- """Reference to the :class:`.Config` object."""
547
+ """Reference to the :class:`.Config` object.
548
+
549
+ .. versionadded:: 2.0
550
+ """
526
551
  return self.env.config
527
552
 
528
553
  def get_source_info(self, lineno: int | None = None) -> tuple[str, int]:
554
+ # .. versionadded:: 3.0
529
555
  if lineno is None:
530
556
  lineno = self.lineno
531
557
  return self.inliner.reporter.get_source_and_line(lineno) # type: ignore[attr-defined]
532
558
 
533
559
  def set_source_info(self, node: Node, lineno: int | None = None) -> None:
560
+ # .. versionadded:: 2.0
534
561
  node.source, node.line = self.get_source_info(lineno)
535
562
 
536
563
  def get_location(self) -> str:
537
- """Get current location info for logging."""
564
+ """Get current location info for logging.
565
+
566
+ .. versionadded:: 4.2
567
+ """
538
568
  source, line = self.get_source_info()
539
569
  if source and line:
540
570
  return f'{source}:{line}'
@@ -551,6 +581,8 @@ class ReferenceRole(SphinxRole):
551
581
  The reference roles can accept ``link title <target>`` style as a text for
552
582
  the role. The parsed result; link title and target will be stored to
553
583
  ``self.title`` and ``self.target``.
584
+
585
+ .. versionadded:: 2.0
554
586
  """
555
587
 
556
588
  has_explicit_title: bool #: A boolean indicates the role has explicit title or not.
@@ -591,6 +623,8 @@ class SphinxTranslator(nodes.NodeVisitor):
591
623
 
592
624
  It also provides helper methods for Sphinx translators.
593
625
 
626
+ .. versionadded:: 2.0
627
+
594
628
  .. note:: The subclasses of this class might not work with docutils.
595
629
  This class is strongly coupled with Sphinx.
596
630
  """
sphinx/util/fileutil.py CHANGED
@@ -4,14 +4,17 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  import posixpath
7
- from typing import TYPE_CHECKING, Any, Callable
7
+ from typing import TYPE_CHECKING, Any
8
8
 
9
9
  from docutils.utils import relative_path
10
10
 
11
+ from sphinx.locale import __
11
12
  from sphinx.util import logging
12
13
  from sphinx.util.osutil import copyfile, ensuredir
13
14
 
14
15
  if TYPE_CHECKING:
16
+ from collections.abc import Callable
17
+
15
18
  from sphinx.util.template import BaseRenderer
16
19
  from sphinx.util.typing import PathMatcher
17
20
 
@@ -34,7 +37,8 @@ def _template_basename(filename: str | os.PathLike[str]) -> str | None:
34
37
  def copy_asset_file(source: str | os.PathLike[str], destination: str | os.PathLike[str],
35
38
  context: dict[str, Any] | None = None,
36
39
  renderer: BaseRenderer | None = None,
37
- *, __overwrite_warning__: bool = True) -> None:
40
+ *,
41
+ force: bool = False) -> None:
38
42
  """Copy an asset file to destination.
39
43
 
40
44
  On copying, it expands the template variables if context argument is given and
@@ -44,6 +48,7 @@ def copy_asset_file(source: str | os.PathLike[str], destination: str | os.PathLi
44
48
  :param destination: The path to destination file or directory
45
49
  :param context: The template variables. If not given, template files are simply copied
46
50
  :param renderer: The template engine. If not given, SphinxRenderer is used by default
51
+ :param bool force: Overwrite the destination file even if it exists.
47
52
  """
48
53
  if not os.path.exists(source):
49
54
  return
@@ -64,45 +69,42 @@ def copy_asset_file(source: str | os.PathLike[str], destination: str | os.PathLi
64
69
  rendered_template = renderer.render_string(template_content, context)
65
70
 
66
71
  if (
67
- __overwrite_warning__
72
+ not force
68
73
  and os.path.exists(destination)
69
74
  and template_content != rendered_template
70
75
  ):
71
- # Consider raising an error in Sphinx 8.
72
- # Certainly make overwriting user content opt-in.
73
- # xref: RemovedInSphinx80Warning
74
- # xref: https://github.com/sphinx-doc/sphinx/issues/12096
75
- msg = ('Copying the rendered template %s to %s will overwrite data, '
76
- 'as a file already exists at the destination path '
77
- 'and the content does not match.')
78
- # See https://github.com/sphinx-doc/sphinx/pull/12627#issuecomment-2241144330
79
- # for the rationale for logger.info().
80
- logger.info(msg, os.fsdecode(source), os.fsdecode(destination),
81
- type='misc', subtype='copy_overwrite')
76
+ msg = __('Aborted attempted copy from rendered template %s to %s '
77
+ '(the destination path has existing data).')
78
+ logger.warning(msg, os.fsdecode(source), os.fsdecode(destination),
79
+ type='misc', subtype='copy_overwrite')
80
+ return
82
81
 
83
82
  destination = _template_basename(destination) or destination
84
83
  with open(destination, 'w', encoding='utf-8') as fdst:
85
84
  fdst.write(rendered_template)
86
85
  else:
87
- copyfile(source, destination, __overwrite_warning__=__overwrite_warning__)
86
+ copyfile(source, destination, force=force)
88
87
 
89
88
 
90
89
  def copy_asset(source: str | os.PathLike[str], destination: str | os.PathLike[str],
91
90
  excluded: PathMatcher = lambda path: False,
92
91
  context: dict[str, Any] | None = None, renderer: BaseRenderer | None = None,
93
92
  onerror: Callable[[str, Exception], None] | None = None,
94
- *, __overwrite_warning__: bool = True) -> None:
93
+ *, force: bool = False) -> None:
95
94
  """Copy asset files to destination recursively.
96
95
 
97
96
  On copying, it expands the template variables if context argument is given and
98
97
  the asset is a template file.
99
98
 
99
+ Use ``copy_asset_file`` instead to copy a single file.
100
+
100
101
  :param source: The path to source file or directory
101
102
  :param destination: The path to destination directory
102
103
  :param excluded: The matcher to determine the given path should be copied or not
103
104
  :param context: The template variables. If not given, template files are simply copied
104
105
  :param renderer: The template engine. If not given, SphinxRenderer is used by default
105
106
  :param onerror: The error handler.
107
+ :param bool force: Overwrite the destination file even if it exists.
106
108
  """
107
109
  if not os.path.exists(source):
108
110
  return
@@ -113,8 +115,10 @@ def copy_asset(source: str | os.PathLike[str], destination: str | os.PathLike[st
113
115
 
114
116
  ensuredir(destination)
115
117
  if os.path.isfile(source):
116
- copy_asset_file(source, destination, context, renderer,
117
- __overwrite_warning__=__overwrite_warning__)
118
+ copy_asset_file(source, destination,
119
+ context=context,
120
+ renderer=renderer,
121
+ force=force)
118
122
  return
119
123
 
120
124
  for root, dirs, files in os.walk(source, followlinks=True):
@@ -130,8 +134,9 @@ def copy_asset(source: str | os.PathLike[str], destination: str | os.PathLike[st
130
134
  try:
131
135
  copy_asset_file(posixpath.join(root, filename),
132
136
  posixpath.join(destination, reldir),
133
- context, renderer,
134
- __overwrite_warning__=__overwrite_warning__)
137
+ context=context,
138
+ renderer=renderer,
139
+ force=force)
135
140
  except Exception as exc:
136
141
  if onerror:
137
142
  onerror(posixpath.join(root, filename), exc)
sphinx/util/i18n.py CHANGED
@@ -15,12 +15,17 @@ from babel.messages.pofile import read_po
15
15
  from sphinx.errors import SphinxError
16
16
  from sphinx.locale import __
17
17
  from sphinx.util import logging
18
- from sphinx.util.osutil import SEP, canon_path, relpath
18
+ from sphinx.util.osutil import (
19
+ SEP,
20
+ _last_modified_time,
21
+ canon_path,
22
+ relpath,
23
+ )
19
24
 
20
25
  if TYPE_CHECKING:
21
26
  import datetime as dt
22
27
  from collections.abc import Iterator
23
- from typing import Protocol, Union
28
+ from typing import Protocol, TypeAlias
24
29
 
25
30
  from babel.core import Locale
26
31
 
@@ -52,7 +57,7 @@ if TYPE_CHECKING:
52
57
  locale: str | Locale | None = ...,
53
58
  ) -> str: ...
54
59
 
55
- Formatter = Union[DateFormatter, TimeFormatter, DatetimeFormatter]
60
+ Formatter: TypeAlias = DateFormatter | TimeFormatter | DatetimeFormatter
56
61
 
57
62
  logger = logging.getLogger(__name__)
58
63
 
@@ -84,7 +89,7 @@ class CatalogInfo(LocaleFileInfoBase):
84
89
  def is_outdated(self) -> bool:
85
90
  return (
86
91
  not path.exists(self.mo_path) or
87
- path.getmtime(self.mo_path) < path.getmtime(self.po_path))
92
+ _last_modified_time(self.mo_path) < _last_modified_time(self.po_path))
88
93
 
89
94
  def write_mo(self, locale: str, use_fuzzy: bool = False) -> None:
90
95
  with open(self.po_path, encoding=self.charset) as file_po:
sphinx/util/images.py CHANGED
@@ -13,8 +13,9 @@ if TYPE_CHECKING:
13
13
 
14
14
  try:
15
15
  from PIL import Image
16
+ PILLOW_AVAILABLE = True
16
17
  except ImportError:
17
- Image = None
18
+ PILLOW_AVAILABLE = False
18
19
 
19
20
  mime_suffixes = {
20
21
  '.gif': 'image/gif',
@@ -43,7 +44,7 @@ def get_image_size(filename: str) -> tuple[int, int] | None:
43
44
  elif isinstance(size[0], float) or isinstance(size[1], float):
44
45
  size = (int(size[0]), int(size[1]))
45
46
 
46
- if size is None and Image: # fallback to Pillow
47
+ if size is None and PILLOW_AVAILABLE: # fallback to Pillow
47
48
  with Image.open(filename) as im:
48
49
  size = im.size
49
50