Sphinx 7.1.2__py3-none-any.whl → 7.2.0__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 (313) hide show
  1. sphinx/__init__.py +6 -6
  2. sphinx/__main__.py +3 -1
  3. sphinx/addnodes.py +35 -22
  4. sphinx/application.py +40 -38
  5. sphinx/builders/__init__.py +16 -12
  6. sphinx/builders/_epub_base.py +15 -11
  7. sphinx/builders/changes.py +6 -4
  8. sphinx/builders/dirhtml.py +4 -2
  9. sphinx/builders/dummy.py +6 -4
  10. sphinx/builders/epub3.py +16 -8
  11. sphinx/builders/gettext.py +40 -43
  12. sphinx/builders/html/__init__.py +166 -196
  13. sphinx/builders/html/_assets.py +116 -0
  14. sphinx/builders/html/transforms.py +4 -2
  15. sphinx/builders/latex/__init__.py +12 -7
  16. sphinx/builders/latex/theming.py +5 -2
  17. sphinx/builders/latex/transforms.py +6 -3
  18. sphinx/builders/linkcheck.py +18 -11
  19. sphinx/builders/manpage.py +6 -4
  20. sphinx/builders/singlehtml.py +16 -9
  21. sphinx/builders/texinfo.py +11 -6
  22. sphinx/builders/text.py +8 -3
  23. sphinx/builders/xml.py +9 -4
  24. sphinx/cmd/build.py +27 -14
  25. sphinx/cmd/make_mode.py +13 -4
  26. sphinx/cmd/quickstart.py +13 -4
  27. sphinx/config.py +17 -14
  28. sphinx/deprecation.py +4 -2
  29. sphinx/directives/__init__.py +44 -12
  30. sphinx/directives/code.py +5 -4
  31. sphinx/directives/other.py +92 -44
  32. sphinx/directives/patches.py +1 -1
  33. sphinx/domains/__init__.py +11 -8
  34. sphinx/domains/c.py +67 -57
  35. sphinx/domains/changeset.py +3 -2
  36. sphinx/domains/citation.py +2 -1
  37. sphinx/domains/cpp.py +136 -93
  38. sphinx/domains/index.py +9 -5
  39. sphinx/domains/javascript.py +32 -19
  40. sphinx/domains/math.py +5 -3
  41. sphinx/domains/python.py +69 -57
  42. sphinx/domains/rst.py +20 -11
  43. sphinx/domains/std.py +21 -15
  44. sphinx/environment/__init__.py +97 -65
  45. sphinx/environment/adapters/indexentries.py +13 -10
  46. sphinx/environment/adapters/toctree.py +485 -308
  47. sphinx/environment/collectors/__init__.py +3 -4
  48. sphinx/environment/collectors/asset.py +10 -4
  49. sphinx/environment/collectors/dependencies.py +7 -4
  50. sphinx/environment/collectors/metadata.py +7 -5
  51. sphinx/environment/collectors/title.py +5 -3
  52. sphinx/environment/collectors/toctree.py +13 -8
  53. sphinx/errors.py +1 -1
  54. sphinx/events.py +5 -5
  55. sphinx/ext/apidoc.py +49 -27
  56. sphinx/ext/autodoc/__init__.py +179 -161
  57. sphinx/ext/autodoc/directive.py +10 -6
  58. sphinx/ext/autodoc/importer.py +22 -13
  59. sphinx/ext/autodoc/mock.py +4 -1
  60. sphinx/ext/autodoc/preserve_defaults.py +80 -12
  61. sphinx/ext/autodoc/type_comment.py +14 -10
  62. sphinx/ext/autodoc/typehints.py +7 -3
  63. sphinx/ext/autosectionlabel.py +6 -3
  64. sphinx/ext/autosummary/__init__.py +21 -15
  65. sphinx/ext/autosummary/generate.py +176 -126
  66. sphinx/ext/coverage.py +93 -8
  67. sphinx/ext/doctest.py +28 -17
  68. sphinx/ext/duration.py +19 -17
  69. sphinx/ext/extlinks.py +11 -6
  70. sphinx/ext/githubpages.py +8 -7
  71. sphinx/ext/graphviz.py +61 -17
  72. sphinx/ext/ifconfig.py +7 -4
  73. sphinx/ext/imgconverter.py +4 -2
  74. sphinx/ext/imgmath.py +29 -23
  75. sphinx/ext/inheritance_diagram.py +41 -27
  76. sphinx/ext/intersphinx.py +45 -38
  77. sphinx/ext/linkcode.py +8 -5
  78. sphinx/ext/mathjax.py +13 -9
  79. sphinx/ext/napoleon/__init__.py +3 -3
  80. sphinx/ext/napoleon/docstring.py +40 -31
  81. sphinx/ext/todo.py +10 -7
  82. sphinx/ext/viewcode.py +46 -25
  83. sphinx/extension.py +1 -1
  84. sphinx/highlighting.py +20 -12
  85. sphinx/io.py +5 -4
  86. sphinx/jinja2glue.py +24 -19
  87. sphinx/locale/__init__.py +8 -2
  88. sphinx/locale/ar/LC_MESSAGES/sphinx.mo +0 -0
  89. sphinx/locale/ar/LC_MESSAGES/sphinx.po +756 -740
  90. sphinx/locale/bg/LC_MESSAGES/sphinx.mo +0 -0
  91. sphinx/locale/bg/LC_MESSAGES/sphinx.po +754 -738
  92. sphinx/locale/bn/LC_MESSAGES/sphinx.mo +0 -0
  93. sphinx/locale/bn/LC_MESSAGES/sphinx.po +755 -739
  94. sphinx/locale/ca/LC_MESSAGES/sphinx.mo +0 -0
  95. sphinx/locale/ca/LC_MESSAGES/sphinx.po +768 -752
  96. sphinx/locale/cak/LC_MESSAGES/sphinx.mo +0 -0
  97. sphinx/locale/cak/LC_MESSAGES/sphinx.po +754 -738
  98. sphinx/locale/cs/LC_MESSAGES/sphinx.mo +0 -0
  99. sphinx/locale/cs/LC_MESSAGES/sphinx.po +758 -742
  100. sphinx/locale/cy/LC_MESSAGES/sphinx.mo +0 -0
  101. sphinx/locale/cy/LC_MESSAGES/sphinx.po +759 -743
  102. sphinx/locale/da/LC_MESSAGES/sphinx.mo +0 -0
  103. sphinx/locale/da/LC_MESSAGES/sphinx.po +760 -744
  104. sphinx/locale/de/LC_MESSAGES/sphinx.mo +0 -0
  105. sphinx/locale/de/LC_MESSAGES/sphinx.po +759 -743
  106. sphinx/locale/de_DE/LC_MESSAGES/sphinx.mo +0 -0
  107. sphinx/locale/de_DE/LC_MESSAGES/sphinx.po +754 -738
  108. sphinx/locale/el/LC_MESSAGES/sphinx.mo +0 -0
  109. sphinx/locale/el/LC_MESSAGES/sphinx.po +763 -747
  110. sphinx/locale/en_DE/LC_MESSAGES/sphinx.mo +0 -0
  111. sphinx/locale/en_DE/LC_MESSAGES/sphinx.po +754 -738
  112. sphinx/locale/en_FR/LC_MESSAGES/sphinx.mo +0 -0
  113. sphinx/locale/en_FR/LC_MESSAGES/sphinx.po +754 -738
  114. sphinx/locale/en_GB/LC_MESSAGES/sphinx.mo +0 -0
  115. sphinx/locale/en_GB/LC_MESSAGES/sphinx.po +768 -752
  116. sphinx/locale/en_HK/LC_MESSAGES/sphinx.mo +0 -0
  117. sphinx/locale/en_HK/LC_MESSAGES/sphinx.po +754 -738
  118. sphinx/locale/eo/LC_MESSAGES/sphinx.mo +0 -0
  119. sphinx/locale/eo/LC_MESSAGES/sphinx.po +754 -738
  120. sphinx/locale/es/LC_MESSAGES/sphinx.mo +0 -0
  121. sphinx/locale/es/LC_MESSAGES/sphinx.po +767 -751
  122. sphinx/locale/es_CO/LC_MESSAGES/sphinx.mo +0 -0
  123. sphinx/locale/es_CO/LC_MESSAGES/sphinx.po +754 -738
  124. sphinx/locale/et/LC_MESSAGES/sphinx.mo +0 -0
  125. sphinx/locale/et/LC_MESSAGES/sphinx.po +762 -746
  126. sphinx/locale/eu/LC_MESSAGES/sphinx.mo +0 -0
  127. sphinx/locale/eu/LC_MESSAGES/sphinx.po +755 -739
  128. sphinx/locale/fa/LC_MESSAGES/sphinx.mo +0 -0
  129. sphinx/locale/fa/LC_MESSAGES/sphinx.po +766 -750
  130. sphinx/locale/fi/LC_MESSAGES/sphinx.mo +0 -0
  131. sphinx/locale/fi/LC_MESSAGES/sphinx.po +754 -738
  132. sphinx/locale/fr/LC_MESSAGES/sphinx.mo +0 -0
  133. sphinx/locale/fr/LC_MESSAGES/sphinx.po +768 -752
  134. sphinx/locale/fr_FR/LC_MESSAGES/sphinx.mo +0 -0
  135. sphinx/locale/fr_FR/LC_MESSAGES/sphinx.po +754 -738
  136. sphinx/locale/gl/LC_MESSAGES/sphinx.js +60 -0
  137. sphinx/locale/gl/LC_MESSAGES/sphinx.mo +0 -0
  138. sphinx/locale/gl/LC_MESSAGES/sphinx.po +3695 -0
  139. sphinx/locale/he/LC_MESSAGES/sphinx.mo +0 -0
  140. sphinx/locale/he/LC_MESSAGES/sphinx.po +755 -739
  141. sphinx/locale/hi/LC_MESSAGES/sphinx.mo +0 -0
  142. sphinx/locale/hi/LC_MESSAGES/sphinx.po +763 -747
  143. sphinx/locale/hi_IN/LC_MESSAGES/sphinx.mo +0 -0
  144. sphinx/locale/hi_IN/LC_MESSAGES/sphinx.po +754 -738
  145. sphinx/locale/hr/LC_MESSAGES/sphinx.mo +0 -0
  146. sphinx/locale/hr/LC_MESSAGES/sphinx.po +760 -744
  147. sphinx/locale/hu/LC_MESSAGES/sphinx.mo +0 -0
  148. sphinx/locale/hu/LC_MESSAGES/sphinx.po +759 -743
  149. sphinx/locale/id/LC_MESSAGES/sphinx.mo +0 -0
  150. sphinx/locale/id/LC_MESSAGES/sphinx.po +765 -749
  151. sphinx/locale/is/LC_MESSAGES/sphinx.mo +0 -0
  152. sphinx/locale/is/LC_MESSAGES/sphinx.po +760 -744
  153. sphinx/locale/it/LC_MESSAGES/sphinx.mo +0 -0
  154. sphinx/locale/it/LC_MESSAGES/sphinx.po +760 -744
  155. sphinx/locale/ja/LC_MESSAGES/sphinx.mo +0 -0
  156. sphinx/locale/ja/LC_MESSAGES/sphinx.po +767 -751
  157. sphinx/locale/ka/LC_MESSAGES/sphinx.mo +0 -0
  158. sphinx/locale/ka/LC_MESSAGES/sphinx.po +759 -743
  159. sphinx/locale/ko/LC_MESSAGES/sphinx.mo +0 -0
  160. sphinx/locale/ko/LC_MESSAGES/sphinx.po +767 -751
  161. sphinx/locale/lt/LC_MESSAGES/sphinx.mo +0 -0
  162. sphinx/locale/lt/LC_MESSAGES/sphinx.po +755 -739
  163. sphinx/locale/lv/LC_MESSAGES/sphinx.mo +0 -0
  164. sphinx/locale/lv/LC_MESSAGES/sphinx.po +755 -739
  165. sphinx/locale/mk/LC_MESSAGES/sphinx.mo +0 -0
  166. sphinx/locale/mk/LC_MESSAGES/sphinx.po +754 -738
  167. sphinx/locale/nb_NO/LC_MESSAGES/sphinx.mo +0 -0
  168. sphinx/locale/nb_NO/LC_MESSAGES/sphinx.po +755 -739
  169. sphinx/locale/ne/LC_MESSAGES/sphinx.mo +0 -0
  170. sphinx/locale/ne/LC_MESSAGES/sphinx.po +755 -739
  171. sphinx/locale/nl/LC_MESSAGES/sphinx.mo +0 -0
  172. sphinx/locale/nl/LC_MESSAGES/sphinx.po +760 -744
  173. sphinx/locale/pl/LC_MESSAGES/sphinx.mo +0 -0
  174. sphinx/locale/pl/LC_MESSAGES/sphinx.po +762 -745
  175. sphinx/locale/pt/LC_MESSAGES/sphinx.mo +0 -0
  176. sphinx/locale/pt/LC_MESSAGES/sphinx.po +754 -738
  177. sphinx/locale/pt_BR/LC_MESSAGES/sphinx.mo +0 -0
  178. sphinx/locale/pt_BR/LC_MESSAGES/sphinx.po +768 -752
  179. sphinx/locale/pt_PT/LC_MESSAGES/sphinx.mo +0 -0
  180. sphinx/locale/pt_PT/LC_MESSAGES/sphinx.po +755 -739
  181. sphinx/locale/ro/LC_MESSAGES/sphinx.mo +0 -0
  182. sphinx/locale/ro/LC_MESSAGES/sphinx.po +759 -743
  183. sphinx/locale/ru/LC_MESSAGES/sphinx.mo +0 -0
  184. sphinx/locale/ru/LC_MESSAGES/sphinx.po +760 -744
  185. sphinx/locale/si/LC_MESSAGES/sphinx.mo +0 -0
  186. sphinx/locale/si/LC_MESSAGES/sphinx.po +754 -738
  187. sphinx/locale/sk/LC_MESSAGES/sphinx.mo +0 -0
  188. sphinx/locale/sk/LC_MESSAGES/sphinx.po +765 -749
  189. sphinx/locale/sl/LC_MESSAGES/sphinx.mo +0 -0
  190. sphinx/locale/sl/LC_MESSAGES/sphinx.po +755 -739
  191. sphinx/locale/sphinx.pot +748 -740
  192. sphinx/locale/sq/LC_MESSAGES/sphinx.mo +0 -0
  193. sphinx/locale/sq/LC_MESSAGES/sphinx.po +768 -752
  194. sphinx/locale/sr/LC_MESSAGES/sphinx.mo +0 -0
  195. sphinx/locale/sr/LC_MESSAGES/sphinx.po +754 -738
  196. sphinx/locale/sr@latin/LC_MESSAGES/sphinx.mo +0 -0
  197. sphinx/locale/sr@latin/LC_MESSAGES/sphinx.po +754 -738
  198. sphinx/locale/sr_RS/LC_MESSAGES/sphinx.mo +0 -0
  199. sphinx/locale/sr_RS/LC_MESSAGES/sphinx.po +754 -738
  200. sphinx/locale/sv/LC_MESSAGES/sphinx.mo +0 -0
  201. sphinx/locale/sv/LC_MESSAGES/sphinx.po +755 -739
  202. sphinx/locale/ta/LC_MESSAGES/sphinx.mo +0 -0
  203. sphinx/locale/ta/LC_MESSAGES/sphinx.po +754 -738
  204. sphinx/locale/te/LC_MESSAGES/sphinx.mo +0 -0
  205. sphinx/locale/te/LC_MESSAGES/sphinx.po +754 -738
  206. sphinx/locale/tr/LC_MESSAGES/sphinx.mo +0 -0
  207. sphinx/locale/tr/LC_MESSAGES/sphinx.po +763 -747
  208. sphinx/locale/uk_UA/LC_MESSAGES/sphinx.mo +0 -0
  209. sphinx/locale/uk_UA/LC_MESSAGES/sphinx.po +760 -749
  210. sphinx/locale/ur/LC_MESSAGES/sphinx.mo +0 -0
  211. sphinx/locale/ur/LC_MESSAGES/sphinx.po +759 -748
  212. sphinx/locale/vi/LC_MESSAGES/sphinx.mo +0 -0
  213. sphinx/locale/vi/LC_MESSAGES/sphinx.po +754 -738
  214. sphinx/locale/yue/LC_MESSAGES/sphinx.mo +0 -0
  215. sphinx/locale/yue/LC_MESSAGES/sphinx.po +754 -738
  216. sphinx/locale/zh_CN/LC_MESSAGES/sphinx.mo +0 -0
  217. sphinx/locale/zh_CN/LC_MESSAGES/sphinx.po +768 -752
  218. sphinx/locale/zh_HK/LC_MESSAGES/sphinx.mo +0 -0
  219. sphinx/locale/zh_HK/LC_MESSAGES/sphinx.po +754 -738
  220. sphinx/locale/zh_TW/LC_MESSAGES/sphinx.mo +0 -0
  221. sphinx/locale/zh_TW/LC_MESSAGES/sphinx.po +767 -751
  222. sphinx/locale/zh_TW.Big5/LC_MESSAGES/sphinx.mo +0 -0
  223. sphinx/locale/zh_TW.Big5/LC_MESSAGES/sphinx.po +754 -738
  224. sphinx/parsers.py +5 -4
  225. sphinx/project.py +52 -34
  226. sphinx/pycode/__init__.py +2 -1
  227. sphinx/pycode/ast.py +7 -13
  228. sphinx/pycode/parser.py +42 -38
  229. sphinx/registry.py +35 -29
  230. sphinx/roles.py +9 -4
  231. sphinx/search/__init__.py +5 -17
  232. sphinx/search/da.py +1 -1
  233. sphinx/search/de.py +1 -1
  234. sphinx/search/en.py +1 -1
  235. sphinx/search/es.py +1 -1
  236. sphinx/search/fi.py +1 -1
  237. sphinx/search/fr.py +1 -1
  238. sphinx/search/hu.py +1 -1
  239. sphinx/search/it.py +1 -1
  240. sphinx/search/ja.py +1 -1
  241. sphinx/search/nl.py +1 -1
  242. sphinx/search/no.py +1 -1
  243. sphinx/search/pt.py +1 -1
  244. sphinx/search/ro.py +1 -1
  245. sphinx/search/ru.py +1 -1
  246. sphinx/search/sv.py +1 -1
  247. sphinx/search/tr.py +1 -1
  248. sphinx/search/zh.py +1 -1
  249. sphinx/testing/fixtures.py +23 -30
  250. sphinx/testing/path.py +9 -0
  251. sphinx/testing/restructuredtext.py +13 -5
  252. sphinx/testing/util.py +20 -63
  253. sphinx/texinputs/sphinxlatexobjects.sty +15 -15
  254. sphinx/themes/agogo/static/agogo.css_t +10 -4
  255. sphinx/themes/basic/layout.html +1 -1
  256. sphinx/themes/basic/static/basic.css_t +4 -0
  257. sphinx/themes/basic/static/documentation_options.js_t +1 -2
  258. sphinx/themes/basic/static/searchtools.js +17 -9
  259. sphinx/themes/basic/static/sphinx_highlight.js +13 -3
  260. sphinx/themes/bizstyle/static/bizstyle.css_t +4 -0
  261. sphinx/themes/classic/theme.conf +1 -1
  262. sphinx/themes/epub/static/epub.css_t +6 -1
  263. sphinx/themes/haiku/theme.conf +1 -1
  264. sphinx/themes/nature/static/nature.css_t +4 -0
  265. sphinx/themes/nonav/static/nonav.css_t +6 -1
  266. sphinx/themes/pyramid/static/pyramid.css_t +4 -0
  267. sphinx/themes/scrolls/static/scrolls.css_t +4 -0
  268. sphinx/themes/scrolls/theme.conf +1 -1
  269. sphinx/themes/sphinxdoc/static/sphinxdoc.css_t +4 -0
  270. sphinx/theming.py +9 -7
  271. sphinx/transforms/__init__.py +79 -3
  272. sphinx/transforms/compact_bullet_list.py +6 -3
  273. sphinx/transforms/i18n.py +26 -10
  274. sphinx/transforms/post_transforms/__init__.py +21 -8
  275. sphinx/transforms/post_transforms/code.py +6 -3
  276. sphinx/transforms/post_transforms/images.py +13 -9
  277. sphinx/util/__init__.py +21 -92
  278. sphinx/util/cfamily.py +7 -4
  279. sphinx/util/display.py +3 -2
  280. sphinx/util/docfields.py +7 -6
  281. sphinx/util/docstrings.py +1 -1
  282. sphinx/util/docutils.py +41 -31
  283. sphinx/util/fileutil.py +9 -6
  284. sphinx/util/i18n.py +21 -18
  285. sphinx/util/images.py +2 -1
  286. sphinx/util/index_entries.py +27 -0
  287. sphinx/util/inspect.py +83 -67
  288. sphinx/util/inventory.py +4 -2
  289. sphinx/util/logging.py +9 -6
  290. sphinx/util/matching.py +5 -2
  291. sphinx/util/math.py +6 -3
  292. sphinx/util/nodes.py +70 -31
  293. sphinx/util/osutil.py +22 -40
  294. sphinx/util/parallel.py +4 -1
  295. sphinx/util/rst.py +7 -3
  296. sphinx/util/tags.py +11 -4
  297. sphinx/util/template.py +17 -14
  298. sphinx/util/typing.py +61 -20
  299. sphinx/versioning.py +6 -4
  300. sphinx/writers/html.py +1 -1
  301. sphinx/writers/html5.py +32 -24
  302. sphinx/writers/latex.py +67 -53
  303. sphinx/writers/manpage.py +9 -5
  304. sphinx/writers/texinfo.py +11 -9
  305. sphinx/writers/text.py +14 -9
  306. sphinx/writers/xml.py +3 -2
  307. {sphinx-7.1.2.dist-info → sphinx-7.2.0.dist-info}/METADATA +7 -5
  308. sphinx-7.2.0.dist-info/RECORD +568 -0
  309. sphinx/testing/comparer.py +0 -97
  310. sphinx-7.1.2.dist-info/RECORD +0 -564
  311. {sphinx-7.1.2.dist-info → sphinx-7.2.0.dist-info}/LICENSE +0 -0
  312. {sphinx-7.1.2.dist-info → sphinx-7.2.0.dist-info}/WHEEL +0 -0
  313. {sphinx-7.1.2.dist-info → sphinx-7.2.0.dist-info}/entry_points.txt +0 -0
@@ -4,19 +4,22 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  import re
7
+ from hashlib import sha1
7
8
  from math import ceil
8
- from typing import Any
9
+ from typing import TYPE_CHECKING, Any
9
10
 
10
11
  from docutils import nodes
11
12
 
12
- from sphinx.application import Sphinx
13
13
  from sphinx.locale import __
14
14
  from sphinx.transforms import SphinxTransform
15
- from sphinx.util import logging, requests, sha1
15
+ from sphinx.util import logging, requests
16
16
  from sphinx.util.http_date import epoch_to_rfc1123, rfc1123_to_epoch
17
17
  from sphinx.util.images import get_image_extension, guess_mimetype, parse_data_uri
18
18
  from sphinx.util.osutil import ensuredir
19
19
 
20
+ if TYPE_CHECKING:
21
+ from sphinx.application import Sphinx
22
+
20
23
  logger = logging.getLogger(__name__)
21
24
 
22
25
  MAX_FILENAME_LEN = 32
@@ -57,13 +60,13 @@ class ImageDownloader(BaseImageConverter):
57
60
  basename = basename.split('?')[0]
58
61
  if basename == '' or len(basename) > MAX_FILENAME_LEN:
59
62
  filename, ext = os.path.splitext(node['uri'])
60
- basename = sha1(filename.encode()).hexdigest() + ext
63
+ basename = sha1(filename.encode(), usedforsecurity=False).hexdigest() + ext
61
64
  basename = re.sub(CRITICAL_PATH_CHAR_RE, "_", basename)
62
65
 
63
66
  dirname = node['uri'].replace('://', '/').translate({ord("?"): "/",
64
67
  ord("&"): "/"})
65
68
  if len(dirname) > MAX_FILENAME_LEN:
66
- dirname = sha1(dirname.encode()).hexdigest()
69
+ dirname = sha1(dirname.encode(), usedforsecurity=False).hexdigest()
67
70
  ensuredir(os.path.join(self.imagedir, dirname))
68
71
  path = os.path.join(self.imagedir, dirname, basename)
69
72
 
@@ -125,7 +128,7 @@ class DataURIExtractor(BaseImageConverter):
125
128
  return
126
129
 
127
130
  ensuredir(os.path.join(self.imagedir, 'embeded'))
128
- digest = sha1(image.data).hexdigest()
131
+ digest = sha1(image.data, usedforsecurity=False).hexdigest()
129
132
  path = os.path.join(self.imagedir, 'embeded', digest + ext)
130
133
  self.app.env.original_image_uri[path] = node['uri']
131
134
 
@@ -217,11 +220,12 @@ class ImageConverter(BaseImageConverter):
217
220
  if rule in self.conversion_rules:
218
221
  return rule
219
222
 
220
- raise ValueError('No conversion rule found')
223
+ msg = 'No conversion rule found'
224
+ raise ValueError(msg)
221
225
 
222
226
  def is_available(self) -> bool:
223
227
  """Return the image converter is available or not."""
224
- raise NotImplementedError()
228
+ raise NotImplementedError
225
229
 
226
230
  def guess_mimetypes(self, node: nodes.image) -> list[str]:
227
231
  if '?' in node['candidates']:
@@ -262,7 +266,7 @@ class ImageConverter(BaseImageConverter):
262
266
  *_from* is a path of the source image file, and *_to* is a path
263
267
  of the destination file.
264
268
  """
265
- raise NotImplementedError()
269
+ raise NotImplementedError
266
270
 
267
271
 
268
272
  def setup(app: Sphinx) -> dict[str, Any]:
sphinx/util/__init__.py CHANGED
@@ -6,7 +6,6 @@ import hashlib
6
6
  import os
7
7
  import posixpath
8
8
  import re
9
- import sys
10
9
  from importlib import import_module
11
10
  from os import path
12
11
  from typing import IO, Any
@@ -17,6 +16,7 @@ from sphinx.locale import __
17
16
  from sphinx.util import display as _display
18
17
  from sphinx.util import exceptions as _exceptions
19
18
  from sphinx.util import http_date as _http_date
19
+ from sphinx.util import index_entries as _index_entries
20
20
  from sphinx.util import logging
21
21
  from sphinx.util import osutil as _osutil
22
22
  from sphinx.util.console import strip_colors # NoQA: F401
@@ -51,8 +51,7 @@ url_re: re.Pattern[str] = re.compile(r'(?P<schema>.+)://.*')
51
51
  # High-level utility functions.
52
52
 
53
53
  def docname_join(basedocname: str, docname: str) -> str:
54
- return posixpath.normpath(
55
- posixpath.join('/' + basedocname, '..', docname))[1:]
54
+ return posixpath.normpath(posixpath.join('/' + basedocname, '..', docname))[1:]
56
55
 
57
56
 
58
57
  def get_filetype(source_suffix: dict[str, str], filename: str) -> str:
@@ -105,26 +104,20 @@ class FilenameUniqDict(dict):
105
104
  self._existing = state
106
105
 
107
106
 
108
- def md5(data=b'', **kwargs):
109
- """Wrapper around hashlib.md5
107
+ def _md5(data=b'', **_kw):
108
+ """Deprecated wrapper around hashlib.md5
110
109
 
111
- Attempt call with 'usedforsecurity=False' if supported.
110
+ To be removed in Sphinx 9.0
112
111
  """
112
+ return hashlib.md5(data, usedforsecurity=False)
113
113
 
114
- if sys.version_info[:2] > (3, 8):
115
- return hashlib.md5(data, usedforsecurity=False)
116
- return hashlib.md5(data, **kwargs)
117
114
 
115
+ def _sha1(data=b'', **_kw):
116
+ """Deprecated wrapper around hashlib.sha1
118
117
 
119
- def sha1(data=b'', **kwargs):
120
- """Wrapper around hashlib.sha1
121
-
122
- Attempt call with 'usedforsecurity=False' if supported.
118
+ To be removed in Sphinx 9.0
123
119
  """
124
-
125
- if sys.version_info[:2] > (3, 8):
126
- return hashlib.sha1(data, usedforsecurity=False)
127
- return hashlib.sha1(data, **kwargs)
120
+ return hashlib.sha1(data, usedforsecurity=False)
128
121
 
129
122
 
130
123
  class DownloadFiles(dict):
@@ -136,7 +129,7 @@ class DownloadFiles(dict):
136
129
 
137
130
  def add_file(self, docname: str, filename: str) -> str:
138
131
  if filename not in self:
139
- digest = md5(filename.encode()).hexdigest()
132
+ digest = hashlib.md5(filename.encode(), usedforsecurity=False).hexdigest()
140
133
  dest = f'{digest}/{os.path.basename(filename)}'
141
134
  self[filename] = (set(), dest)
142
135
 
@@ -155,23 +148,6 @@ class DownloadFiles(dict):
155
148
  self.add_file(docname, filename)
156
149
 
157
150
 
158
- def get_full_modname(modname: str, attribute: str) -> str | None:
159
- if modname is None:
160
- # Prevents a TypeError: if the last getattr() call will return None
161
- # then it's better to return it directly
162
- return None
163
- module = import_module(modname)
164
-
165
- # Allow an attribute to have multiple parts and incidentally allow
166
- # repeated .s in the attribute.
167
- value = module
168
- for attr in attribute.split('.'):
169
- if attr:
170
- value = getattr(value, attr)
171
-
172
- return getattr(value, '__module__', None)
173
-
174
-
175
151
  # a regex to recognize coding cookies
176
152
  _coding_re = re.compile(r'coding[:=]\s*([-\w.]+)')
177
153
 
@@ -238,36 +214,13 @@ def parselinenos(spec: str, total: int) -> list[int]:
238
214
  items.extend(range(start - 1, end))
239
215
  else:
240
216
  raise ValueError
241
- except Exception as exc:
242
- raise ValueError('invalid line number spec: %r' % spec) from exc
217
+ except ValueError as exc:
218
+ msg = f'invalid line number spec: {spec!r}'
219
+ raise ValueError(msg) from exc
243
220
 
244
221
  return items
245
222
 
246
223
 
247
- def split_into(n: int, type: str, value: str) -> list[str]:
248
- """Split an index entry into a given number of parts at semicolons."""
249
- parts = [x.strip() for x in value.split(';', n - 1)]
250
- if len(list(filter(None, parts))) < n:
251
- raise ValueError(f'invalid {type} index entry {value!r}')
252
- return parts
253
-
254
-
255
- def split_index_msg(entry_type: str, value: str) -> list[str]:
256
- # new entry types must be listed in util/nodes.py!
257
- if entry_type == 'single':
258
- try:
259
- return split_into(2, 'single', value)
260
- except ValueError:
261
- return split_into(1, 'single', value)
262
- if entry_type == 'pair':
263
- return split_into(2, 'pair', value)
264
- if entry_type == 'triple':
265
- return split_into(3, 'triple', value)
266
- if entry_type in {'see', 'seealso'}:
267
- return split_into(2, 'see', value)
268
- raise ValueError(f'invalid {entry_type} index entry {value!r}')
269
-
270
-
271
224
  def import_object(objname: str, source: str | None = None) -> Any:
272
225
  """Import python object by qualname."""
273
226
  try:
@@ -289,36 +242,6 @@ def import_object(objname: str, source: str | None = None) -> Any:
289
242
  raise ExtensionError('Could not import %s' % objname, exc) from exc
290
243
 
291
244
 
292
- def split_full_qualified_name(name: str) -> tuple[str | None, str]:
293
- """Split full qualified name to a pair of modname and qualname.
294
-
295
- A qualname is an abbreviation for "Qualified name" introduced at PEP-3155
296
- (https://peps.python.org/pep-3155/). It is a dotted path name
297
- from the module top-level.
298
-
299
- A "full" qualified name means a string containing both module name and
300
- qualified name.
301
-
302
- .. note:: This function actually imports the module to check its existence.
303
- Therefore you need to mock 3rd party modules if needed before
304
- calling this function.
305
- """
306
- parts = name.split('.')
307
- for i, _part in enumerate(parts, 1):
308
- try:
309
- modname = ".".join(parts[:i])
310
- import_module(modname)
311
- except ImportError:
312
- if parts[:i - 1]:
313
- return ".".join(parts[:i - 1]), ".".join(parts[i - 1:])
314
- else:
315
- return None, ".".join(parts)
316
- except IndexError:
317
- pass
318
-
319
- return name, ""
320
-
321
-
322
245
  def encode_uri(uri: str) -> str:
323
246
  split = list(urlsplit(uri))
324
247
  split[1] = split[1].encode('idna').decode('ascii')
@@ -354,12 +277,18 @@ _DEPRECATED_OBJECTS = {
354
277
  'format_exception_cut_frames': (_exceptions.format_exception_cut_frames,
355
278
  'sphinx.exceptions.format_exception_cut_frames'),
356
279
  'xmlname_checker': (_xml_name_checker, 'sphinx.builders.epub3._XML_NAME_PATTERN'),
280
+ 'split_index_msg': (_index_entries.split_index_msg,
281
+ 'sphinx.util.index_entries.split_index_msg'),
282
+ 'split_into': (_index_entries.split_index_msg, 'sphinx.util.index_entries.split_into'),
283
+ 'md5': (_md5, ''),
284
+ 'sha1': (_sha1, ''),
357
285
  }
358
286
 
359
287
 
360
288
  def __getattr__(name):
361
289
  if name not in _DEPRECATED_OBJECTS:
362
- raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
290
+ msg = f'module {__name__!r} has no attribute {name!r}'
291
+ raise AttributeError(msg)
363
292
 
364
293
  from sphinx.deprecation import _deprecation_warning
365
294
 
sphinx/util/cfamily.py CHANGED
@@ -4,15 +4,18 @@ from __future__ import annotations
4
4
 
5
5
  import re
6
6
  from copy import deepcopy
7
- from typing import Any, Callable
7
+ from typing import TYPE_CHECKING, Any, Callable
8
8
 
9
9
  from docutils import nodes
10
- from docutils.nodes import TextElement
11
10
 
12
11
  from sphinx import addnodes
13
- from sphinx.config import Config
14
12
  from sphinx.util import logging
15
13
 
14
+ if TYPE_CHECKING:
15
+ from docutils.nodes import TextElement
16
+
17
+ from sphinx.config import Config
18
+
16
19
  logger = logging.getLogger(__name__)
17
20
 
18
21
  StringifyTransform = Callable[[Any], str]
@@ -281,7 +284,7 @@ class BaseParser:
281
284
  def status(self, msg: str) -> None:
282
285
  # for debugging
283
286
  indicator = '-' * self.pos + '^'
284
- print(f"{msg}\n{self.definition}\n{indicator}")
287
+ logger.debug(f"{msg}\n{self.definition}\n{indicator}") # NoQA: G004
285
288
 
286
289
  def fail(self, msg: str) -> None:
287
290
  errors = []
sphinx/util/display.py CHANGED
@@ -1,13 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import functools
4
- from typing import Any, Callable, Iterable, Iterator, TypeVar
4
+ from typing import Any, Callable, TypeVar
5
5
 
6
6
  from sphinx.locale import __
7
7
  from sphinx.util import logging
8
- from sphinx.util.console import bold # type: ignore
8
+ from sphinx.util.console import bold # type: ignore[attr-defined]
9
9
 
10
10
  if False:
11
+ from collections.abc import Iterable, Iterator
11
12
  from types import TracebackType
12
13
 
13
14
  logger = logging.getLogger(__name__)
sphinx/util/docfields.py CHANGED
@@ -6,21 +6,22 @@ be domain-specifically transformed to a more appealing presentation.
6
6
  from __future__ import annotations
7
7
 
8
8
  import contextlib
9
- from typing import TYPE_CHECKING, Any, List, Tuple, cast
9
+ from typing import TYPE_CHECKING, Any, cast
10
10
 
11
11
  from docutils import nodes
12
12
  from docutils.nodes import Element, Node
13
- from docutils.parsers.rst.states import Inliner
14
13
 
15
14
  from sphinx import addnodes
16
- from sphinx.environment import BuildEnvironment
17
15
  from sphinx.locale import __
18
16
  from sphinx.util import logging
19
17
  from sphinx.util.nodes import get_node_line
20
- from sphinx.util.typing import TextlikeNode
21
18
 
22
19
  if TYPE_CHECKING:
20
+ from docutils.parsers.rst.states import Inliner
21
+
23
22
  from sphinx.directives import ObjectDescription
23
+ from sphinx.environment import BuildEnvironment
24
+ from sphinx.util.typing import TextlikeNode
24
25
 
25
26
  logger = logging.getLogger(__name__)
26
27
 
@@ -294,7 +295,7 @@ class DocFieldTransformer:
294
295
  types: dict[str, dict] = {}
295
296
 
296
297
  # step 1: traverse all fields and collect field types and content
297
- for field in cast(List[nodes.field], node):
298
+ for field in cast(list[nodes.field], node):
298
299
  assert len(field) == 2
299
300
  field_name = cast(nodes.field_name, field[0])
300
301
  field_body = cast(nodes.field_body, field[1])
@@ -378,7 +379,7 @@ class DocFieldTransformer:
378
379
  # get one entry per field
379
380
  if typedesc.is_grouped:
380
381
  if typename in groupindices:
381
- group = cast(Tuple[Field, List, Node], entries[groupindices[typename]])
382
+ group = cast(tuple[Field, list, Node], entries[groupindices[typename]])
382
383
  else:
383
384
  groupindices[typename] = len(entries)
384
385
  group = (typedesc, [], field)
sphinx/util/docstrings.py CHANGED
@@ -10,7 +10,7 @@ from docutils.parsers.rst.states import Body
10
10
  field_list_item_re = re.compile(Body.patterns['field_marker'])
11
11
 
12
12
 
13
- def separate_metadata(s: str) -> tuple[str, dict[str, str]]:
13
+ def separate_metadata(s: str | None) -> tuple[str | None, dict[str, str]]:
14
14
  """Separate docstring into metadata and others."""
15
15
  in_other_element = False
16
16
  metadata: dict[str, str] = {}
sphinx/util/docutils.py CHANGED
@@ -4,17 +4,17 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  import re
7
+ from collections.abc import Sequence # NoQA: TCH003
7
8
  from contextlib import contextmanager
8
9
  from copy import copy
9
10
  from os import path
10
- from typing import IO, TYPE_CHECKING, Any, Callable, Generator, cast
11
+ from typing import IO, TYPE_CHECKING, Any, Callable, cast
11
12
 
12
13
  import docutils
13
14
  from docutils import nodes
14
15
  from docutils.io import FileOutput
15
- from docutils.nodes import Element, Node, system_message
16
16
  from docutils.parsers.rst import Directive, directives, roles
17
- from docutils.parsers.rst.states import Inliner
17
+ from docutils.parsers.rst.states import Inliner # NoQA: TCH002
18
18
  from docutils.statemachine import State, StateMachine, StringList
19
19
  from docutils.utils import Reporter, unescape
20
20
  from docutils.writers._html_base import HTMLTranslator
@@ -22,19 +22,21 @@ from docutils.writers._html_base import HTMLTranslator
22
22
  from sphinx.errors import SphinxError
23
23
  from sphinx.locale import _, __
24
24
  from sphinx.util import logging
25
- from sphinx.util.typing import RoleFunction
26
25
 
27
26
  logger = logging.getLogger(__name__)
28
27
  report_re = re.compile('^(.+?:(?:\\d+)?): \\((DEBUG|INFO|WARNING|ERROR|SEVERE)/(\\d+)?\\) ')
29
28
 
30
29
  if TYPE_CHECKING:
30
+ from collections.abc import Generator
31
31
  from types import ModuleType
32
32
 
33
33
  from docutils.frontend import Values
34
+ from docutils.nodes import Element, Node, system_message
34
35
 
35
36
  from sphinx.builders import Builder
36
37
  from sphinx.config import Config
37
38
  from sphinx.environment import BuildEnvironment
39
+ from sphinx.util.typing import RoleFunction
38
40
 
39
41
  # deprecated name -> (object to return, canonical path or empty string)
40
42
  _DEPRECATED_OBJECTS = {
@@ -44,7 +46,8 @@ _DEPRECATED_OBJECTS = {
44
46
 
45
47
  def __getattr__(name):
46
48
  if name not in _DEPRECATED_OBJECTS:
47
- raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
49
+ msg = f'module {__name__!r} has no attribute {name!r}'
50
+ raise AttributeError(msg)
48
51
 
49
52
  from sphinx.deprecation import _deprecation_warning
50
53
 
@@ -60,13 +63,13 @@ additional_nodes: set[type[Element]] = set()
60
63
  def docutils_namespace() -> Generator[None, None, None]:
61
64
  """Create namespace for reST parsers."""
62
65
  try:
63
- _directives = copy(directives._directives) # type: ignore
64
- _roles = copy(roles._roles) # type: ignore
66
+ _directives = copy(directives._directives) # type: ignore[attr-defined]
67
+ _roles = copy(roles._roles) # type: ignore[attr-defined]
65
68
 
66
69
  yield
67
70
  finally:
68
- directives._directives = _directives # type: ignore
69
- roles._roles = _roles # type: ignore
71
+ directives._directives = _directives # type: ignore[attr-defined]
72
+ roles._roles = _roles # type: ignore[attr-defined]
70
73
 
71
74
  for node in list(additional_nodes):
72
75
  unregister_node(node)
@@ -75,7 +78,7 @@ def docutils_namespace() -> Generator[None, None, None]:
75
78
 
76
79
  def is_directive_registered(name: str) -> bool:
77
80
  """Check the *name* directive is already registered."""
78
- return name in directives._directives # type: ignore
81
+ return name in directives._directives # type: ignore[attr-defined]
79
82
 
80
83
 
81
84
  def register_directive(name: str, directive: type[Directive]) -> None:
@@ -89,7 +92,7 @@ def register_directive(name: str, directive: type[Directive]) -> None:
89
92
 
90
93
  def is_role_registered(name: str) -> bool:
91
94
  """Check the *name* role is already registered."""
92
- return name in roles._roles # type: ignore
95
+ return name in roles._roles # type: ignore[attr-defined]
93
96
 
94
97
 
95
98
  def register_role(name: str, role: RoleFunction) -> None:
@@ -103,7 +106,7 @@ def register_role(name: str, role: RoleFunction) -> None:
103
106
 
104
107
  def unregister_role(name: str) -> None:
105
108
  """Unregister a role from docutils."""
106
- roles._roles.pop(name, None) # type: ignore
109
+ roles._roles.pop(name, None) # type: ignore[attr-defined]
107
110
 
108
111
 
109
112
  def is_node_registered(node: type[Element]) -> bool:
@@ -118,7 +121,7 @@ def register_node(node: type[Element]) -> None:
118
121
  inside ``docutils_namespace()`` to prevent side-effects.
119
122
  """
120
123
  if not hasattr(nodes.GenericNodeVisitor, 'visit_' + node.__name__):
121
- nodes._add_node_class_names([node.__name__]) # type: ignore
124
+ nodes._add_node_class_names([node.__name__]) # type: ignore[attr-defined]
122
125
  additional_nodes.add(node)
123
126
 
124
127
 
@@ -275,7 +278,8 @@ class CustomReSTDispatcher:
275
278
  def role(
276
279
  self, role_name: str, language_module: ModuleType, lineno: int, reporter: Reporter,
277
280
  ) -> tuple[RoleFunction, list[system_message]]:
278
- return self.role_func(role_name, language_module, lineno, reporter)
281
+ return self.role_func(role_name, language_module, # type: ignore[return-value]
282
+ lineno, reporter)
279
283
 
280
284
 
281
285
  class ElementLookupError(Exception):
@@ -375,17 +379,17 @@ def switch_source_input(state: State, content: StringList) -> Generator[None, No
375
379
  """Switch current source input of state temporarily."""
376
380
  try:
377
381
  # remember the original ``get_source_and_line()`` method
378
- get_source_and_line = state.memo.reporter.get_source_and_line # type: ignore
382
+ gsal = state.memo.reporter.get_source_and_line # type: ignore[attr-defined]
379
383
 
380
384
  # replace it by new one
381
385
  state_machine = StateMachine([], None) # type: ignore[arg-type]
382
386
  state_machine.input_lines = content
383
- state.memo.reporter.get_source_and_line = state_machine.get_source_and_line # type: ignore # noqa: E501
387
+ state.memo.reporter.get_source_and_line = state_machine.get_source_and_line # type: ignore[attr-defined] # noqa: E501
384
388
 
385
389
  yield
386
390
  finally:
387
391
  # restore the method
388
- state.memo.reporter.get_source_and_line = get_source_and_line # type: ignore
392
+ state.memo.reporter.get_source_and_line = gsal # type: ignore[attr-defined]
389
393
 
390
394
 
391
395
  class SphinxFileOutput(FileOutput):
@@ -447,24 +451,26 @@ class SphinxRole:
447
451
  .. note:: The subclasses of this class might not work with docutils.
448
452
  This class is strongly coupled with Sphinx.
449
453
  """
450
- name: str #: The role name actually used in the document.
451
- rawtext: str #: A string containing the entire interpreted text input.
452
- text: str #: The interpreted text content.
453
- lineno: int #: The line number where the interpreted text begins.
454
- inliner: Inliner #: The ``docutils.parsers.rst.states.Inliner`` object.
455
- options: dict #: A dictionary of directive options for customization
456
- #: (from the "role" directive).
457
- content: list[str] #: A list of strings, the directive content for customization
458
- #: (from the "role" directive).
454
+ name: str #: The role name actually used in the document.
455
+ rawtext: str #: A string containing the entire interpreted text input.
456
+ text: str #: The interpreted text content.
457
+ lineno: int #: The line number where the interpreted text begins.
458
+ inliner: Inliner #: The ``docutils.parsers.rst.states.Inliner`` object.
459
+ #: A dictionary of directive options for customisation
460
+ #: (from the "role" directive).
461
+ options: dict[str, Any]
462
+ #: A list of strings, the directive content for customisation
463
+ #: (from the "role" directive).
464
+ content: Sequence[str]
459
465
 
460
466
  def __call__(self, name: str, rawtext: str, text: str, lineno: int,
461
- inliner: Inliner, options: dict = {}, content: list[str] = [],
467
+ inliner: Inliner, options: dict | None = None, content: Sequence[str] = (),
462
468
  ) -> tuple[list[Node], list[system_message]]:
463
469
  self.rawtext = rawtext
464
470
  self.text = unescape(text)
465
471
  self.lineno = lineno
466
472
  self.inliner = inliner
467
- self.options = options
473
+ self.options = options if options is not None else {}
468
474
  self.content = content
469
475
 
470
476
  # guess role type
@@ -475,7 +481,8 @@ class SphinxRole:
475
481
  if not self.name:
476
482
  self.name = self.env.config.default_role
477
483
  if not self.name:
478
- raise SphinxError('cannot determine default role!')
484
+ msg = 'cannot determine default role!'
485
+ raise SphinxError(msg)
479
486
 
480
487
  return self.run()
481
488
 
@@ -495,7 +502,7 @@ class SphinxRole:
495
502
  def get_source_info(self, lineno: int | None = None) -> tuple[str, int]:
496
503
  if lineno is None:
497
504
  lineno = self.lineno
498
- return self.inliner.reporter.get_source_and_line(lineno) # type: ignore
505
+ return self.inliner.reporter.get_source_and_line(lineno) # type: ignore[attr-defined]
499
506
 
500
507
  def set_source_info(self, node: Node, lineno: int | None = None) -> None:
501
508
  node.source, node.line = self.get_source_info(lineno)
@@ -521,8 +528,11 @@ class ReferenceRole(SphinxRole):
521
528
  explicit_title_re = re.compile(r'^(.+?)\s*(?<!\x00)<(.*?)>$', re.DOTALL)
522
529
 
523
530
  def __call__(self, name: str, rawtext: str, text: str, lineno: int,
524
- inliner: Inliner, options: dict = {}, content: list[str] = [],
531
+ inliner: Inliner, options: dict | None = None, content: Sequence[str] = (),
525
532
  ) -> tuple[list[Node], list[system_message]]:
533
+ if options is None:
534
+ options = {}
535
+
526
536
  # if the first character is a bang, don't cross-reference at all
527
537
  self.disabled = text.startswith('!')
528
538
 
sphinx/util/fileutil.py CHANGED
@@ -9,13 +9,13 @@ from typing import TYPE_CHECKING, Callable
9
9
  from docutils.utils import relative_path
10
10
 
11
11
  from sphinx.util.osutil import copyfile, ensuredir
12
- from sphinx.util.typing import PathMatcher
13
12
 
14
13
  if TYPE_CHECKING:
15
14
  from sphinx.util.template import BaseRenderer
15
+ from sphinx.util.typing import PathMatcher
16
16
 
17
17
 
18
- def copy_asset_file(source: str, destination: str,
18
+ def copy_asset_file(source: str | os.PathLike[str], destination: str | os.PathLike[str],
19
19
  context: dict | None = None,
20
20
  renderer: BaseRenderer | None = None) -> None:
21
21
  """Copy an asset file to destination.
@@ -34,14 +34,16 @@ def copy_asset_file(source: str, destination: str,
34
34
  if os.path.isdir(destination):
35
35
  # Use source filename if destination points a directory
36
36
  destination = os.path.join(destination, os.path.basename(source))
37
+ else:
38
+ destination = str(destination)
37
39
 
38
- if source.lower().endswith('_t') and context is not None:
40
+ if os.path.basename(source).endswith(('_t', '_T')) and context is not None:
39
41
  if renderer is None:
40
42
  from sphinx.util.template import SphinxRenderer
41
43
  renderer = SphinxRenderer()
42
44
 
43
45
  with open(source, encoding='utf-8') as fsrc:
44
- if destination.lower().endswith('_t'):
46
+ if destination.endswith(('_t', '_T')):
45
47
  destination = destination[:-2]
46
48
  with open(destination, 'w', encoding='utf-8') as fdst:
47
49
  fdst.write(renderer.render_string(fsrc.read(), context))
@@ -49,7 +51,8 @@ def copy_asset_file(source: str, destination: str,
49
51
  copyfile(source, destination)
50
52
 
51
53
 
52
- def copy_asset(source: str, destination: str, excluded: PathMatcher = lambda path: False,
54
+ def copy_asset(source: str | os.PathLike[str], destination: str | os.PathLike[str],
55
+ excluded: PathMatcher = lambda path: False,
53
56
  context: dict | None = None, renderer: BaseRenderer | None = None,
54
57
  onerror: Callable[[str, Exception], None] | None = None) -> None:
55
58
  """Copy asset files to destination recursively.
@@ -77,7 +80,7 @@ def copy_asset(source: str, destination: str, excluded: PathMatcher = lambda pat
77
80
  return
78
81
 
79
82
  for root, dirs, files in os.walk(source, followlinks=True):
80
- reldir = relative_path(source, root)
83
+ reldir = relative_path(source, root) # type: ignore[arg-type]
81
84
  for dir in dirs[:]:
82
85
  if excluded(posixpath.join(reldir, dir)):
83
86
  dirs.remove(dir)