Sphinx 8.1.2__py3-none-any.whl → 8.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.
- sphinx/__init__.py +8 -4
- sphinx/__main__.py +2 -0
- sphinx/_cli/__init__.py +2 -5
- sphinx/_cli/util/colour.py +34 -11
- sphinx/_cli/util/errors.py +128 -61
- sphinx/addnodes.py +51 -35
- sphinx/application.py +362 -230
- sphinx/builders/__init__.py +87 -64
- sphinx/builders/_epub_base.py +65 -56
- sphinx/builders/changes.py +17 -23
- sphinx/builders/dirhtml.py +8 -13
- sphinx/builders/epub3.py +70 -38
- sphinx/builders/gettext.py +93 -73
- sphinx/builders/html/__init__.py +240 -186
- sphinx/builders/html/_assets.py +9 -2
- sphinx/builders/html/_build_info.py +3 -0
- sphinx/builders/latex/__init__.py +64 -54
- sphinx/builders/latex/constants.py +14 -11
- sphinx/builders/latex/nodes.py +2 -0
- sphinx/builders/latex/theming.py +8 -9
- sphinx/builders/latex/transforms.py +7 -5
- sphinx/builders/linkcheck.py +193 -149
- sphinx/builders/manpage.py +17 -17
- sphinx/builders/singlehtml.py +28 -16
- sphinx/builders/texinfo.py +28 -21
- sphinx/builders/text.py +10 -15
- sphinx/builders/xml.py +10 -19
- sphinx/cmd/build.py +49 -119
- sphinx/cmd/make_mode.py +35 -31
- sphinx/cmd/quickstart.py +78 -62
- sphinx/config.py +265 -163
- sphinx/directives/__init__.py +51 -54
- sphinx/directives/admonitions.py +107 -0
- sphinx/directives/code.py +24 -19
- sphinx/directives/other.py +21 -42
- sphinx/directives/patches.py +28 -16
- sphinx/domains/__init__.py +54 -31
- sphinx/domains/_domains_container.py +22 -17
- sphinx/domains/_index.py +5 -8
- sphinx/domains/c/__init__.py +366 -245
- sphinx/domains/c/_ast.py +378 -256
- sphinx/domains/c/_ids.py +89 -31
- sphinx/domains/c/_parser.py +283 -214
- sphinx/domains/c/_symbol.py +269 -198
- sphinx/domains/changeset.py +39 -24
- sphinx/domains/citation.py +54 -24
- sphinx/domains/cpp/__init__.py +517 -362
- sphinx/domains/cpp/_ast.py +999 -682
- sphinx/domains/cpp/_ids.py +133 -65
- sphinx/domains/cpp/_parser.py +746 -588
- sphinx/domains/cpp/_symbol.py +692 -489
- sphinx/domains/index.py +10 -8
- sphinx/domains/javascript.py +152 -74
- sphinx/domains/math.py +50 -40
- sphinx/domains/python/__init__.py +402 -211
- sphinx/domains/python/_annotations.py +134 -61
- sphinx/domains/python/_object.py +155 -68
- sphinx/domains/rst.py +94 -49
- sphinx/domains/std/__init__.py +510 -249
- sphinx/environment/__init__.py +345 -61
- sphinx/environment/adapters/asset.py +7 -1
- sphinx/environment/adapters/indexentries.py +15 -20
- sphinx/environment/adapters/toctree.py +19 -9
- sphinx/environment/collectors/__init__.py +3 -1
- sphinx/environment/collectors/asset.py +18 -15
- sphinx/environment/collectors/dependencies.py +8 -10
- sphinx/environment/collectors/metadata.py +6 -4
- sphinx/environment/collectors/title.py +3 -1
- sphinx/environment/collectors/toctree.py +4 -4
- sphinx/errors.py +1 -3
- sphinx/events.py +4 -4
- sphinx/ext/apidoc/__init__.py +66 -0
- sphinx/ext/apidoc/__main__.py +9 -0
- sphinx/ext/apidoc/_cli.py +356 -0
- sphinx/ext/apidoc/_extension.py +262 -0
- sphinx/ext/apidoc/_generate.py +356 -0
- sphinx/ext/apidoc/_shared.py +99 -0
- sphinx/ext/autodoc/__init__.py +837 -483
- sphinx/ext/autodoc/directive.py +57 -21
- sphinx/ext/autodoc/importer.py +184 -67
- sphinx/ext/autodoc/mock.py +25 -10
- sphinx/ext/autodoc/preserve_defaults.py +17 -9
- sphinx/ext/autodoc/type_comment.py +56 -29
- sphinx/ext/autodoc/typehints.py +49 -26
- sphinx/ext/autosectionlabel.py +28 -11
- sphinx/ext/autosummary/__init__.py +281 -142
- sphinx/ext/autosummary/generate.py +121 -51
- sphinx/ext/coverage.py +152 -91
- sphinx/ext/doctest.py +169 -101
- sphinx/ext/duration.py +12 -6
- sphinx/ext/extlinks.py +33 -21
- sphinx/ext/githubpages.py +8 -8
- sphinx/ext/graphviz.py +175 -109
- sphinx/ext/ifconfig.py +11 -6
- sphinx/ext/imgconverter.py +48 -25
- sphinx/ext/imgmath.py +127 -97
- sphinx/ext/inheritance_diagram.py +177 -103
- sphinx/ext/intersphinx/__init__.py +22 -13
- sphinx/ext/intersphinx/__main__.py +3 -1
- sphinx/ext/intersphinx/_cli.py +18 -14
- sphinx/ext/intersphinx/_load.py +91 -82
- sphinx/ext/intersphinx/_resolve.py +108 -74
- sphinx/ext/intersphinx/_shared.py +2 -2
- sphinx/ext/linkcode.py +28 -12
- sphinx/ext/mathjax.py +60 -29
- sphinx/ext/napoleon/__init__.py +19 -7
- sphinx/ext/napoleon/docstring.py +229 -231
- sphinx/ext/todo.py +44 -49
- sphinx/ext/viewcode.py +105 -57
- sphinx/extension.py +3 -1
- sphinx/highlighting.py +13 -7
- sphinx/io.py +9 -13
- sphinx/jinja2glue.py +29 -26
- sphinx/locale/__init__.py +8 -9
- sphinx/locale/ar/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/ar/LC_MESSAGES/sphinx.po +2155 -2050
- sphinx/locale/bg/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/bg/LC_MESSAGES/sphinx.po +2045 -1940
- sphinx/locale/bn/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/bn/LC_MESSAGES/sphinx.po +2175 -2070
- sphinx/locale/ca/LC_MESSAGES/sphinx.js +3 -3
- sphinx/locale/ca/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/ca/LC_MESSAGES/sphinx.po +2690 -2585
- sphinx/locale/ca@valencia/LC_MESSAGES/sphinx.js +63 -0
- sphinx/locale/ca@valencia/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/ca@valencia/LC_MESSAGES/sphinx.po +4216 -0
- sphinx/locale/cak/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/cak/LC_MESSAGES/sphinx.po +2096 -1991
- sphinx/locale/cs/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/cs/LC_MESSAGES/sphinx.po +2248 -2143
- sphinx/locale/cy/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/cy/LC_MESSAGES/sphinx.po +2201 -2096
- sphinx/locale/da/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/da/LC_MESSAGES/sphinx.po +2282 -2177
- sphinx/locale/de/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/de/LC_MESSAGES/sphinx.po +2261 -2156
- sphinx/locale/de_DE/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/de_DE/LC_MESSAGES/sphinx.po +2045 -1940
- sphinx/locale/el/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/el/LC_MESSAGES/sphinx.po +2604 -2499
- sphinx/locale/en_DE/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/en_DE/LC_MESSAGES/sphinx.po +2045 -1940
- sphinx/locale/en_FR/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/en_FR/LC_MESSAGES/sphinx.po +2045 -1940
- sphinx/locale/en_GB/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/en_GB/LC_MESSAGES/sphinx.po +2631 -2526
- sphinx/locale/en_HK/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/en_HK/LC_MESSAGES/sphinx.po +2045 -1940
- sphinx/locale/eo/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/eo/LC_MESSAGES/sphinx.po +2078 -1973
- sphinx/locale/es/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/es/LC_MESSAGES/sphinx.po +2633 -2528
- sphinx/locale/es_CO/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/es_CO/LC_MESSAGES/sphinx.po +2045 -1940
- sphinx/locale/et/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/et/LC_MESSAGES/sphinx.po +2449 -2344
- sphinx/locale/eu/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/eu/LC_MESSAGES/sphinx.po +2241 -2136
- sphinx/locale/fa/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/fa/LC_MESSAGES/sphinx.po +504 -500
- sphinx/locale/fi/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/fi/LC_MESSAGES/sphinx.po +499 -495
- sphinx/locale/fr/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/fr/LC_MESSAGES/sphinx.po +513 -509
- sphinx/locale/fr_FR/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/fr_FR/LC_MESSAGES/sphinx.po +499 -495
- sphinx/locale/gl/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/gl/LC_MESSAGES/sphinx.po +2644 -2539
- sphinx/locale/he/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/he/LC_MESSAGES/sphinx.po +499 -495
- sphinx/locale/hi/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/hi/LC_MESSAGES/sphinx.po +504 -500
- sphinx/locale/hi_IN/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/hi_IN/LC_MESSAGES/sphinx.po +499 -495
- sphinx/locale/hr/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/hr/LC_MESSAGES/sphinx.po +501 -497
- sphinx/locale/hu/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/hu/LC_MESSAGES/sphinx.po +499 -495
- sphinx/locale/id/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/id/LC_MESSAGES/sphinx.po +2609 -2504
- sphinx/locale/is/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/is/LC_MESSAGES/sphinx.po +499 -495
- sphinx/locale/it/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/it/LC_MESSAGES/sphinx.po +2265 -2160
- sphinx/locale/ja/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/ja/LC_MESSAGES/sphinx.po +2621 -2516
- sphinx/locale/ka/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/ka/LC_MESSAGES/sphinx.po +2567 -2462
- sphinx/locale/ko/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/ko/LC_MESSAGES/sphinx.po +2631 -2526
- sphinx/locale/lt/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/lt/LC_MESSAGES/sphinx.po +2214 -2109
- sphinx/locale/lv/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/lv/LC_MESSAGES/sphinx.po +2218 -2113
- sphinx/locale/mk/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/mk/LC_MESSAGES/sphinx.po +2088 -1983
- sphinx/locale/nb_NO/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/nb_NO/LC_MESSAGES/sphinx.po +2247 -2142
- sphinx/locale/ne/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/ne/LC_MESSAGES/sphinx.po +2227 -2122
- sphinx/locale/nl/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/nl/LC_MESSAGES/sphinx.po +2316 -2211
- sphinx/locale/pl/LC_MESSAGES/sphinx.js +2 -2
- sphinx/locale/pl/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/pl/LC_MESSAGES/sphinx.po +2442 -2336
- sphinx/locale/pt/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/pt/LC_MESSAGES/sphinx.po +2045 -1940
- sphinx/locale/pt_BR/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/pt_BR/LC_MESSAGES/sphinx.po +2657 -2552
- sphinx/locale/pt_PT/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/pt_PT/LC_MESSAGES/sphinx.po +2243 -2138
- sphinx/locale/ro/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/ro/LC_MESSAGES/sphinx.po +2244 -2139
- sphinx/locale/ru/LC_MESSAGES/sphinx.js +1 -1
- sphinx/locale/ru/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/ru/LC_MESSAGES/sphinx.po +2660 -2555
- sphinx/locale/si/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/si/LC_MESSAGES/sphinx.po +2134 -2029
- sphinx/locale/sk/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/sk/LC_MESSAGES/sphinx.po +2614 -2509
- sphinx/locale/sl/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/sl/LC_MESSAGES/sphinx.po +2167 -2062
- sphinx/locale/sphinx.pot +2069 -1964
- sphinx/locale/sq/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/sq/LC_MESSAGES/sphinx.po +2661 -2556
- sphinx/locale/sr/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/sr/LC_MESSAGES/sphinx.po +2213 -2108
- sphinx/locale/sv/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/sv/LC_MESSAGES/sphinx.po +2229 -2124
- sphinx/locale/te/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/te/LC_MESSAGES/sphinx.po +2045 -1940
- sphinx/locale/tr/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/tr/LC_MESSAGES/sphinx.po +2608 -2503
- sphinx/locale/uk_UA/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/uk_UA/LC_MESSAGES/sphinx.po +2167 -2062
- sphinx/locale/ur/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/ur/LC_MESSAGES/sphinx.po +2045 -1940
- sphinx/locale/vi/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/vi/LC_MESSAGES/sphinx.po +2204 -2099
- sphinx/locale/yue/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/yue/LC_MESSAGES/sphinx.po +2045 -1940
- sphinx/locale/zh_HK/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/zh_HK/LC_MESSAGES/sphinx.po +2045 -1940
- sphinx/locale/zh_TW/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/zh_TW/LC_MESSAGES/sphinx.po +2659 -2554
- sphinx/locale/zh_TW.Big5/LC_MESSAGES/sphinx.mo +0 -0
- sphinx/locale/zh_TW.Big5/LC_MESSAGES/sphinx.po +2045 -1940
- sphinx/parsers.py +8 -7
- sphinx/project.py +2 -2
- sphinx/pycode/__init__.py +31 -21
- sphinx/pycode/ast.py +6 -3
- sphinx/pycode/parser.py +14 -8
- sphinx/pygments_styles.py +4 -5
- sphinx/registry.py +192 -92
- sphinx/roles.py +58 -7
- sphinx/search/__init__.py +75 -54
- sphinx/search/en.py +11 -13
- sphinx/search/fi.py +1 -1
- sphinx/search/ja.py +8 -6
- sphinx/search/nl.py +1 -1
- sphinx/search/zh.py +19 -21
- sphinx/testing/fixtures.py +26 -29
- sphinx/testing/path.py +26 -62
- sphinx/testing/restructuredtext.py +14 -8
- sphinx/testing/util.py +21 -19
- sphinx/texinputs/make.bat.jinja +50 -50
- sphinx/texinputs/sphinx.sty +4 -3
- sphinx/texinputs/sphinxlatexadmonitions.sty +1 -1
- sphinx/texinputs/sphinxlatexobjects.sty +29 -10
- sphinx/themes/basic/static/searchtools.js +8 -5
- sphinx/theming.py +49 -61
- sphinx/transforms/__init__.py +17 -38
- sphinx/transforms/compact_bullet_list.py +5 -3
- sphinx/transforms/i18n.py +8 -21
- sphinx/transforms/post_transforms/__init__.py +142 -93
- sphinx/transforms/post_transforms/code.py +5 -5
- sphinx/transforms/post_transforms/images.py +28 -24
- sphinx/transforms/references.py +3 -1
- sphinx/util/__init__.py +109 -60
- sphinx/util/_files.py +39 -23
- sphinx/util/_importer.py +4 -1
- sphinx/util/_inventory_file_reader.py +76 -0
- sphinx/util/_io.py +2 -2
- sphinx/util/_lines.py +6 -3
- sphinx/util/_pathlib.py +40 -2
- sphinx/util/build_phase.py +2 -0
- sphinx/util/cfamily.py +19 -14
- sphinx/util/console.py +44 -179
- sphinx/util/display.py +9 -10
- sphinx/util/docfields.py +140 -122
- sphinx/util/docstrings.py +1 -1
- sphinx/util/docutils.py +118 -77
- sphinx/util/fileutil.py +25 -26
- sphinx/util/http_date.py +2 -0
- sphinx/util/i18n.py +77 -64
- sphinx/util/images.py +8 -6
- sphinx/util/inspect.py +147 -38
- sphinx/util/inventory.py +215 -116
- sphinx/util/logging.py +33 -33
- sphinx/util/matching.py +12 -4
- sphinx/util/nodes.py +18 -13
- sphinx/util/osutil.py +38 -39
- sphinx/util/parallel.py +22 -13
- sphinx/util/parsing.py +2 -1
- sphinx/util/png.py +6 -2
- sphinx/util/requests.py +33 -2
- sphinx/util/rst.py +3 -2
- sphinx/util/tags.py +1 -1
- sphinx/util/template.py +18 -10
- sphinx/util/texescape.py +8 -6
- sphinx/util/typing.py +148 -122
- sphinx/versioning.py +3 -3
- sphinx/writers/html.py +3 -1
- sphinx/writers/html5.py +63 -52
- sphinx/writers/latex.py +83 -67
- sphinx/writers/manpage.py +19 -38
- sphinx/writers/texinfo.py +47 -47
- sphinx/writers/text.py +50 -32
- sphinx/writers/xml.py +11 -8
- {sphinx-8.1.2.dist-info → sphinx-8.2.0.dist-info}/LICENSE.rst +1 -1
- {sphinx-8.1.2.dist-info → sphinx-8.2.0.dist-info}/METADATA +25 -15
- sphinx-8.2.0.dist-info/RECORD +606 -0
- {sphinx-8.1.2.dist-info → sphinx-8.2.0.dist-info}/WHEEL +1 -1
- sphinx/builders/html/transforms.py +0 -90
- sphinx/ext/apidoc.py +0 -721
- sphinx/util/exceptions.py +0 -74
- sphinx-8.1.2.dist-info/RECORD +0 -598
- {sphinx-8.1.2.dist-info → sphinx-8.2.0.dist-info}/entry_points.txt +0 -0
sphinx/ext/doctest.py
CHANGED
|
@@ -6,12 +6,12 @@ The extension automatically execute code snippets and checks their results.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import doctest
|
|
9
|
+
import os.path
|
|
9
10
|
import re
|
|
10
11
|
import sys
|
|
11
12
|
import time
|
|
12
13
|
from io import StringIO
|
|
13
|
-
from
|
|
14
|
-
from typing import TYPE_CHECKING, Any, ClassVar
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
15
|
|
|
16
16
|
from docutils import nodes
|
|
17
17
|
from docutils.parsers.rst import directives
|
|
@@ -19,15 +19,16 @@ from packaging.specifiers import InvalidSpecifier, SpecifierSet
|
|
|
19
19
|
from packaging.version import Version
|
|
20
20
|
|
|
21
21
|
import sphinx
|
|
22
|
+
from sphinx._cli.util.colour import bold
|
|
22
23
|
from sphinx.builders import Builder
|
|
23
24
|
from sphinx.locale import __
|
|
24
25
|
from sphinx.util import logging
|
|
25
|
-
from sphinx.util.console import bold
|
|
26
26
|
from sphinx.util.docutils import SphinxDirective
|
|
27
27
|
from sphinx.util.osutil import relpath
|
|
28
28
|
|
|
29
29
|
if TYPE_CHECKING:
|
|
30
30
|
from collections.abc import Callable, Set
|
|
31
|
+
from typing import Any, ClassVar
|
|
31
32
|
|
|
32
33
|
from docutils.nodes import Element, Node, TextElement
|
|
33
34
|
|
|
@@ -38,7 +39,7 @@ if TYPE_CHECKING:
|
|
|
38
39
|
logger = logging.getLogger(__name__)
|
|
39
40
|
|
|
40
41
|
blankline_re = re.compile(r'^\s*<BLANKLINE>', re.MULTILINE)
|
|
41
|
-
doctestopt_re = re.compile(r'
|
|
42
|
+
doctestopt_re = re.compile(r'[ \t]*#\s*doctest:.+$', re.MULTILINE)
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
def is_allowed_version(spec: str, version: str) -> bool:
|
|
@@ -61,10 +62,9 @@ def is_allowed_version(spec: str, version: str) -> bool:
|
|
|
61
62
|
|
|
62
63
|
# set up the necessary directives
|
|
63
64
|
|
|
65
|
+
|
|
64
66
|
class TestDirective(SphinxDirective):
|
|
65
|
-
"""
|
|
66
|
-
Base class for doctest-related directives.
|
|
67
|
-
"""
|
|
67
|
+
"""Base class for doctest-related directives."""
|
|
68
68
|
|
|
69
69
|
has_content = True
|
|
70
70
|
required_arguments = 0
|
|
@@ -81,12 +81,15 @@ class TestDirective(SphinxDirective):
|
|
|
81
81
|
# convert <BLANKLINE>s to ordinary blank lines for presentation
|
|
82
82
|
test = code
|
|
83
83
|
code = blankline_re.sub('', code)
|
|
84
|
-
if
|
|
84
|
+
if (
|
|
85
|
+
doctestopt_re.search(code)
|
|
86
|
+
and 'no-trim-doctest-flags' not in self.options
|
|
87
|
+
):
|
|
85
88
|
if not test:
|
|
86
89
|
test = code
|
|
87
90
|
code = doctestopt_re.sub('', code)
|
|
88
91
|
nodetype: type[TextElement] = nodes.literal_block
|
|
89
|
-
if self.name in
|
|
92
|
+
if self.name in {'testsetup', 'testcleanup'} or 'hide' in self.options:
|
|
90
93
|
nodetype = nodes.comment
|
|
91
94
|
if self.arguments:
|
|
92
95
|
groups = [x.strip() for x in self.arguments[0].split(',')]
|
|
@@ -105,7 +108,7 @@ class TestDirective(SphinxDirective):
|
|
|
105
108
|
# don't try to highlight output
|
|
106
109
|
node['language'] = 'none'
|
|
107
110
|
node['options'] = {}
|
|
108
|
-
if self.name in
|
|
111
|
+
if self.name in {'doctest', 'testoutput'} and 'options' in self.options:
|
|
109
112
|
# parse doctest-like output comparison flags
|
|
110
113
|
option_strings = self.options['options'].replace(',', ' ').split()
|
|
111
114
|
for option in option_strings:
|
|
@@ -113,15 +116,17 @@ class TestDirective(SphinxDirective):
|
|
|
113
116
|
if prefix not in '+-':
|
|
114
117
|
self.state.document.reporter.warning(
|
|
115
118
|
__("missing '+' or '-' in '%s' option.") % option,
|
|
116
|
-
line=self.lineno
|
|
119
|
+
line=self.lineno,
|
|
120
|
+
)
|
|
117
121
|
continue
|
|
118
122
|
if option_name not in doctest.OPTIONFLAGS_BY_NAME:
|
|
119
123
|
self.state.document.reporter.warning(
|
|
120
124
|
__("'%s' is not a valid option.") % option_name,
|
|
121
|
-
line=self.lineno
|
|
125
|
+
line=self.lineno,
|
|
126
|
+
)
|
|
122
127
|
continue
|
|
123
128
|
flag = doctest.OPTIONFLAGS_BY_NAME[option[1:]]
|
|
124
|
-
node['options'][flag] =
|
|
129
|
+
node['options'][flag] = option[0] == '+'
|
|
125
130
|
if self.name == 'doctest' and 'pyversion' in self.options:
|
|
126
131
|
try:
|
|
127
132
|
spec = self.options['pyversion']
|
|
@@ -131,8 +136,8 @@ class TestDirective(SphinxDirective):
|
|
|
131
136
|
node['options'][flag] = True # Skip the test
|
|
132
137
|
except InvalidSpecifier:
|
|
133
138
|
self.state.document.reporter.warning(
|
|
134
|
-
__("'%s' is not a valid pyversion option") % spec,
|
|
135
|
-
|
|
139
|
+
__("'%s' is not a valid pyversion option") % spec, line=self.lineno
|
|
140
|
+
)
|
|
136
141
|
if 'skipif' in self.options:
|
|
137
142
|
node['skipif'] = self.options['skipif']
|
|
138
143
|
if 'trim-doctest-flags' in self.options:
|
|
@@ -191,6 +196,7 @@ parser = doctest.DocTestParser()
|
|
|
191
196
|
|
|
192
197
|
# helper classes
|
|
193
198
|
|
|
199
|
+
|
|
194
200
|
class TestGroup:
|
|
195
201
|
def __init__(self, name: str) -> None:
|
|
196
202
|
self.name = name
|
|
@@ -220,27 +226,38 @@ class TestGroup:
|
|
|
220
226
|
raise RuntimeError(__('invalid TestCode type'))
|
|
221
227
|
|
|
222
228
|
def __repr__(self) -> str:
|
|
223
|
-
return (
|
|
224
|
-
|
|
229
|
+
return (
|
|
230
|
+
f'TestGroup(name={self.name!r}, setup={self.setup!r}, '
|
|
231
|
+
f'cleanup={self.cleanup!r}, tests={self.tests!r})'
|
|
232
|
+
)
|
|
225
233
|
|
|
226
234
|
|
|
227
235
|
class TestCode:
|
|
228
|
-
def __init__(
|
|
229
|
-
|
|
236
|
+
def __init__(
|
|
237
|
+
self,
|
|
238
|
+
code: str,
|
|
239
|
+
type: str,
|
|
240
|
+
filename: str,
|
|
241
|
+
lineno: int,
|
|
242
|
+
options: dict[int, bool] | None = None,
|
|
243
|
+
) -> None:
|
|
230
244
|
self.code = code
|
|
231
245
|
self.type = type
|
|
232
246
|
self.filename = filename
|
|
233
247
|
self.lineno = lineno
|
|
234
|
-
self.options = options or {}
|
|
248
|
+
self.options: dict[int, bool] = options or {}
|
|
235
249
|
|
|
236
250
|
def __repr__(self) -> str:
|
|
237
|
-
return (
|
|
238
|
-
|
|
251
|
+
return (
|
|
252
|
+
f'TestCode({self.code!r}, {self.type!r}, filename={self.filename!r}, '
|
|
253
|
+
f'lineno={self.lineno!r}, options={self.options!r})'
|
|
254
|
+
)
|
|
239
255
|
|
|
240
256
|
|
|
241
257
|
class SphinxDocTestRunner(doctest.DocTestRunner):
|
|
242
|
-
def summarize(
|
|
243
|
-
|
|
258
|
+
def summarize( # type: ignore[override]
|
|
259
|
+
self, out: Callable[[str], None], verbose: bool | None = None
|
|
260
|
+
) -> tuple[int, int]:
|
|
244
261
|
string_io = StringIO()
|
|
245
262
|
old_stdout = sys.stdout
|
|
246
263
|
sys.stdout = string_io
|
|
@@ -251,11 +268,11 @@ class SphinxDocTestRunner(doctest.DocTestRunner):
|
|
|
251
268
|
out(string_io.getvalue())
|
|
252
269
|
return res
|
|
253
270
|
|
|
254
|
-
def _DocTestRunner__patched_linecache_getlines(
|
|
255
|
-
|
|
271
|
+
def _DocTestRunner__patched_linecache_getlines(
|
|
272
|
+
self, filename: str, module_globals: Any = None
|
|
273
|
+
) -> Any:
|
|
256
274
|
# this is overridden from DocTestRunner adding the try-except below
|
|
257
|
-
m = self._DocTestRunner__LINECACHE_FILENAME_RE.match( # type: ignore[attr-defined]
|
|
258
|
-
filename)
|
|
275
|
+
m = self._DocTestRunner__LINECACHE_FILENAME_RE.match(filename) # type: ignore[attr-defined]
|
|
259
276
|
if m and m.group('name') == self.test.name:
|
|
260
277
|
try:
|
|
261
278
|
example = self.test.examples[int(m.group('examplenum'))]
|
|
@@ -266,20 +283,20 @@ class SphinxDocTestRunner(doctest.DocTestRunner):
|
|
|
266
283
|
pass
|
|
267
284
|
else:
|
|
268
285
|
return example.source.splitlines(True)
|
|
269
|
-
return self.save_linecache_getlines( # type: ignore[attr-defined]
|
|
270
|
-
filename, module_globals)
|
|
286
|
+
return self.save_linecache_getlines(filename, module_globals) # type: ignore[attr-defined]
|
|
271
287
|
|
|
272
288
|
|
|
273
289
|
# the new builder -- use sphinx-build.py -b doctest to run
|
|
274
290
|
|
|
291
|
+
|
|
275
292
|
class DocTestBuilder(Builder):
|
|
276
|
-
"""
|
|
277
|
-
Runs test snippets in the documentation.
|
|
278
|
-
"""
|
|
293
|
+
"""Runs test snippets in the documentation."""
|
|
279
294
|
|
|
280
295
|
name = 'doctest'
|
|
281
|
-
epilog = __(
|
|
282
|
-
|
|
296
|
+
epilog = __(
|
|
297
|
+
'Testing of doctests in the sources finished, look at the '
|
|
298
|
+
'results in %(outdir)s/output.txt.'
|
|
299
|
+
)
|
|
283
300
|
|
|
284
301
|
def init(self) -> None:
|
|
285
302
|
# default options
|
|
@@ -306,10 +323,12 @@ class DocTestBuilder(Builder):
|
|
|
306
323
|
date = time.strftime('%Y-%m-%d %H:%M:%S')
|
|
307
324
|
|
|
308
325
|
outpath = self.outdir.joinpath('output.txt')
|
|
309
|
-
self.outfile = outpath.open('w', encoding='utf-8')
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
326
|
+
self.outfile = outpath.open('w', encoding='utf-8')
|
|
327
|
+
line = '=' * len(date)
|
|
328
|
+
self.outfile.write(
|
|
329
|
+
f'Results of doctest builder run on {date}\n'
|
|
330
|
+
f'=================================={line}\n'
|
|
331
|
+
)
|
|
313
332
|
|
|
314
333
|
def __del__(self) -> None:
|
|
315
334
|
# free resources upon destruction (the file handler might not be
|
|
@@ -338,18 +357,17 @@ class DocTestBuilder(Builder):
|
|
|
338
357
|
# write executive summary
|
|
339
358
|
def s(v: int) -> str:
|
|
340
359
|
return 's' if v != 1 else ''
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
self.cleanup_failures, s(self.cleanup_failures))
|
|
345
|
-
self._out('''
|
|
360
|
+
|
|
361
|
+
self._out(
|
|
362
|
+
f"""
|
|
346
363
|
Doctest summary
|
|
347
364
|
===============
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
365
|
+
{self.total_tries:5} test{s(self.total_tries)}
|
|
366
|
+
{self.total_failures:5} failure{s(self.total_failures)} in tests
|
|
367
|
+
{self.setup_failures:5} failure{s(self.setup_failures)} in setup code
|
|
368
|
+
{self.cleanup_failures:5} failure{s(self.cleanup_failures)} in cleanup code
|
|
369
|
+
"""
|
|
370
|
+
)
|
|
353
371
|
self.outfile.close()
|
|
354
372
|
|
|
355
373
|
if self.total_failures or self.setup_failures or self.cleanup_failures:
|
|
@@ -367,17 +385,17 @@ Doctest summary
|
|
|
367
385
|
filename of the document it's included in.
|
|
368
386
|
"""
|
|
369
387
|
try:
|
|
370
|
-
filename = relpath(node.source, self.env.srcdir)
|
|
388
|
+
filename = relpath(node.source, self.env.srcdir) # type: ignore[arg-type]
|
|
389
|
+
return filename.rsplit(':docstring of ', maxsplit=1)[0]
|
|
371
390
|
except Exception:
|
|
372
|
-
|
|
373
|
-
return filename
|
|
391
|
+
return str(self.env.doc2path(docname, False))
|
|
374
392
|
|
|
375
393
|
@staticmethod
|
|
376
394
|
def get_line_number(node: Node) -> int | None:
|
|
377
395
|
"""Get the real line number or admit we don't know."""
|
|
378
396
|
# TODO: Work out how to store or calculate real (file-relative)
|
|
379
397
|
# line numbers for doctest blocks in docstrings.
|
|
380
|
-
if ':docstring of ' in path.basename(node.source or ''):
|
|
398
|
+
if ':docstring of ' in os.path.basename(node.source or ''):
|
|
381
399
|
# The line number is given relative to the stripped docstring,
|
|
382
400
|
# not the file. This is correct where it is set, in
|
|
383
401
|
# `docutils.nodes.Node.setup_child`, but Sphinx should report
|
|
@@ -404,25 +422,29 @@ Doctest summary
|
|
|
404
422
|
def test_doc(self, docname: str, doctree: Node) -> None:
|
|
405
423
|
groups: dict[str, TestGroup] = {}
|
|
406
424
|
add_to_all_groups = []
|
|
407
|
-
self.setup_runner = SphinxDocTestRunner(verbose=False,
|
|
408
|
-
|
|
409
|
-
self.
|
|
410
|
-
optionflags=self.opt)
|
|
411
|
-
self.cleanup_runner = SphinxDocTestRunner(verbose=False,
|
|
412
|
-
optionflags=self.opt)
|
|
425
|
+
self.setup_runner = SphinxDocTestRunner(verbose=False, optionflags=self.opt)
|
|
426
|
+
self.test_runner = SphinxDocTestRunner(verbose=False, optionflags=self.opt)
|
|
427
|
+
self.cleanup_runner = SphinxDocTestRunner(verbose=False, optionflags=self.opt)
|
|
413
428
|
|
|
414
429
|
self.test_runner._fakeout = self.setup_runner._fakeout # type: ignore[attr-defined]
|
|
415
430
|
self.cleanup_runner._fakeout = self.setup_runner._fakeout # type: ignore[attr-defined]
|
|
416
431
|
|
|
417
432
|
if self.config.doctest_test_doctest_blocks:
|
|
433
|
+
|
|
418
434
|
def condition(node: Node) -> bool:
|
|
419
|
-
return (
|
|
420
|
-
|
|
421
|
-
|
|
435
|
+
return (
|
|
436
|
+
isinstance(node, nodes.literal_block | nodes.comment)
|
|
437
|
+
and 'testnodetype' in node
|
|
438
|
+
) or isinstance(node, nodes.doctest_block)
|
|
439
|
+
|
|
422
440
|
else:
|
|
441
|
+
|
|
423
442
|
def condition(node: Node) -> bool:
|
|
424
|
-
return
|
|
443
|
+
return (
|
|
444
|
+
isinstance(node, nodes.literal_block | nodes.comment)
|
|
425
445
|
and 'testnodetype' in node
|
|
446
|
+
)
|
|
447
|
+
|
|
426
448
|
for node in doctree.findall(condition):
|
|
427
449
|
if self.skipped(node): # type: ignore[arg-type]
|
|
428
450
|
continue
|
|
@@ -431,12 +453,19 @@ Doctest summary
|
|
|
431
453
|
filename = self.get_filename_for_node(node, docname)
|
|
432
454
|
line_number = self.get_line_number(node)
|
|
433
455
|
if not source:
|
|
434
|
-
logger.warning(
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
456
|
+
logger.warning(
|
|
457
|
+
__('no code/output in %s block at %s:%s'),
|
|
458
|
+
node.get('testnodetype', 'doctest'), # type: ignore[attr-defined]
|
|
459
|
+
filename,
|
|
460
|
+
line_number,
|
|
461
|
+
)
|
|
462
|
+
code = TestCode(
|
|
463
|
+
source,
|
|
464
|
+
type=node.get('testnodetype', 'doctest'), # type: ignore[attr-defined]
|
|
465
|
+
filename=filename,
|
|
466
|
+
lineno=line_number, # type: ignore[arg-type]
|
|
467
|
+
options=node.get('options'), # type: ignore[attr-defined]
|
|
468
|
+
)
|
|
440
469
|
node_groups = node.get('groups', ['default']) # type: ignore[attr-defined]
|
|
441
470
|
if '*' in node_groups:
|
|
442
471
|
add_to_all_groups.append(code)
|
|
@@ -449,13 +478,21 @@ Doctest summary
|
|
|
449
478
|
for group in groups.values():
|
|
450
479
|
group.add_code(code)
|
|
451
480
|
if self.config.doctest_global_setup:
|
|
452
|
-
code = TestCode(
|
|
453
|
-
|
|
481
|
+
code = TestCode(
|
|
482
|
+
self.config.doctest_global_setup,
|
|
483
|
+
'testsetup',
|
|
484
|
+
filename='<global_setup>',
|
|
485
|
+
lineno=0,
|
|
486
|
+
)
|
|
454
487
|
for group in groups.values():
|
|
455
488
|
group.add_code(code, prepend=True)
|
|
456
489
|
if self.config.doctest_global_cleanup:
|
|
457
|
-
code = TestCode(
|
|
458
|
-
|
|
490
|
+
code = TestCode(
|
|
491
|
+
self.config.doctest_global_cleanup,
|
|
492
|
+
'testcleanup',
|
|
493
|
+
filename='<global_cleanup>',
|
|
494
|
+
lineno=0,
|
|
495
|
+
)
|
|
459
496
|
for group in groups.values():
|
|
460
497
|
group.add_code(code)
|
|
461
498
|
if not groups:
|
|
@@ -463,9 +500,7 @@ Doctest summary
|
|
|
463
500
|
|
|
464
501
|
show_successes = self.config.doctest_show_successes
|
|
465
502
|
if show_successes:
|
|
466
|
-
self._out('\n'
|
|
467
|
-
f'Document: {docname}\n'
|
|
468
|
-
f'----------{"-" * len(docname)}\n')
|
|
503
|
+
self._out(f'\nDocument: {docname}\n----------{"-" * len(docname)}\n')
|
|
469
504
|
for group in groups.values():
|
|
470
505
|
self.test_group(group)
|
|
471
506
|
# Separately count results from setup code
|
|
@@ -473,23 +508,27 @@ Doctest summary
|
|
|
473
508
|
self.setup_failures += res_f
|
|
474
509
|
self.setup_tries += res_t
|
|
475
510
|
if self.test_runner.tries:
|
|
476
|
-
res_f, res_t = self.test_runner.summarize(
|
|
477
|
-
self._out, verbose=show_successes)
|
|
511
|
+
res_f, res_t = self.test_runner.summarize(self._out, verbose=show_successes)
|
|
478
512
|
self.total_failures += res_f
|
|
479
513
|
self.total_tries += res_t
|
|
480
514
|
if self.cleanup_runner.tries:
|
|
481
515
|
res_f, res_t = self.cleanup_runner.summarize(
|
|
482
|
-
self._out, verbose=show_successes
|
|
516
|
+
self._out, verbose=show_successes
|
|
517
|
+
)
|
|
483
518
|
self.cleanup_failures += res_f
|
|
484
519
|
self.cleanup_tries += res_t
|
|
485
520
|
|
|
486
|
-
def compile(
|
|
521
|
+
def compile(
|
|
522
|
+
self, code: str, name: str, type: str, flags: Any, dont_inherit: bool
|
|
523
|
+
) -> Any:
|
|
487
524
|
return compile(code, name, self.type, flags, dont_inherit)
|
|
488
525
|
|
|
489
526
|
def test_group(self, group: TestGroup) -> None:
|
|
490
|
-
ns: dict = {}
|
|
527
|
+
ns: dict[str, Any] = {}
|
|
491
528
|
|
|
492
|
-
def run_setup_cleanup(
|
|
529
|
+
def run_setup_cleanup(
|
|
530
|
+
runner: Any, testcodes: list[TestCode], what: Any
|
|
531
|
+
) -> bool:
|
|
493
532
|
examples = []
|
|
494
533
|
for testcode in testcodes:
|
|
495
534
|
example = doctest.Example(testcode.code, '', lineno=testcode.lineno)
|
|
@@ -497,9 +536,14 @@ Doctest summary
|
|
|
497
536
|
if not examples:
|
|
498
537
|
return True
|
|
499
538
|
# simulate a doctest with the code
|
|
500
|
-
sim_doctest = doctest.DocTest(
|
|
501
|
-
|
|
502
|
-
|
|
539
|
+
sim_doctest = doctest.DocTest(
|
|
540
|
+
examples,
|
|
541
|
+
{},
|
|
542
|
+
f'{group.name} ({what} code)',
|
|
543
|
+
testcodes[0].filename,
|
|
544
|
+
0,
|
|
545
|
+
None,
|
|
546
|
+
)
|
|
503
547
|
sim_doctest.globs = ns
|
|
504
548
|
old_f = runner.failures
|
|
505
549
|
self.type = 'exec' # the snippet may contain multiple statements
|
|
@@ -516,11 +560,15 @@ Doctest summary
|
|
|
516
560
|
if len(code) == 1:
|
|
517
561
|
# ordinary doctests (code/output interleaved)
|
|
518
562
|
try:
|
|
519
|
-
test = parser.get_doctest(
|
|
520
|
-
|
|
563
|
+
test = parser.get_doctest(
|
|
564
|
+
code[0].code, {}, group.name, code[0].filename, code[0].lineno
|
|
565
|
+
)
|
|
521
566
|
except Exception:
|
|
522
|
-
logger.warning(
|
|
523
|
-
|
|
567
|
+
logger.warning(
|
|
568
|
+
__('ignoring invalid doctest code: %r'),
|
|
569
|
+
code[0].code,
|
|
570
|
+
location=(code[0].filename, code[0].lineno),
|
|
571
|
+
)
|
|
524
572
|
continue
|
|
525
573
|
if not test.examples:
|
|
526
574
|
continue
|
|
@@ -542,10 +590,21 @@ Doctest summary
|
|
|
542
590
|
exc_msg = m.group('msg')
|
|
543
591
|
else:
|
|
544
592
|
exc_msg = None
|
|
545
|
-
example = doctest.Example(
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
593
|
+
example = doctest.Example(
|
|
594
|
+
code[0].code,
|
|
595
|
+
output,
|
|
596
|
+
exc_msg=exc_msg,
|
|
597
|
+
lineno=code[0].lineno,
|
|
598
|
+
options=options,
|
|
599
|
+
)
|
|
600
|
+
test = doctest.DocTest(
|
|
601
|
+
[example],
|
|
602
|
+
{},
|
|
603
|
+
group.name,
|
|
604
|
+
code[0].filename,
|
|
605
|
+
code[0].lineno,
|
|
606
|
+
None,
|
|
607
|
+
)
|
|
549
608
|
self.type = 'exec' # multiple statements again
|
|
550
609
|
# DocTest.__init__ copies the globs namespace, which we don't want
|
|
551
610
|
test.globs = ns
|
|
@@ -564,13 +623,22 @@ def setup(app: Sphinx) -> ExtensionMetadata:
|
|
|
564
623
|
app.add_directive('testoutput', TestoutputDirective)
|
|
565
624
|
app.add_builder(DocTestBuilder)
|
|
566
625
|
# this config value adds to sys.path
|
|
567
|
-
app.add_config_value('doctest_show_successes', True, '', bool)
|
|
568
|
-
app.add_config_value('doctest_path',
|
|
569
|
-
app.add_config_value(
|
|
570
|
-
|
|
571
|
-
|
|
626
|
+
app.add_config_value('doctest_show_successes', True, '', types=frozenset({bool}))
|
|
627
|
+
app.add_config_value('doctest_path', (), '', types=frozenset({list, tuple}))
|
|
628
|
+
app.add_config_value(
|
|
629
|
+
'doctest_test_doctest_blocks', 'default', '', types=frozenset({str})
|
|
630
|
+
)
|
|
631
|
+
app.add_config_value('doctest_global_setup', '', '', types=frozenset({str}))
|
|
632
|
+
app.add_config_value('doctest_global_cleanup', '', '', types=frozenset({str}))
|
|
572
633
|
app.add_config_value(
|
|
573
634
|
'doctest_default_flags',
|
|
574
|
-
doctest.DONT_ACCEPT_TRUE_FOR_1
|
|
575
|
-
|
|
576
|
-
|
|
635
|
+
doctest.DONT_ACCEPT_TRUE_FOR_1
|
|
636
|
+
| doctest.ELLIPSIS
|
|
637
|
+
| doctest.IGNORE_EXCEPTION_DETAIL,
|
|
638
|
+
'',
|
|
639
|
+
types=frozenset({int}),
|
|
640
|
+
)
|
|
641
|
+
return {
|
|
642
|
+
'version': sphinx.__display_version__,
|
|
643
|
+
'parallel_read_safe': True,
|
|
644
|
+
}
|
sphinx/ext/duration.py
CHANGED
|
@@ -23,6 +23,7 @@ if TYPE_CHECKING:
|
|
|
23
23
|
class _DurationDomainData(TypedDict):
|
|
24
24
|
reading_durations: dict[str, float]
|
|
25
25
|
|
|
26
|
+
|
|
26
27
|
logger = logging.getLogger(__name__)
|
|
27
28
|
|
|
28
29
|
|
|
@@ -44,7 +45,9 @@ class DurationDomain(Domain):
|
|
|
44
45
|
def clear_doc(self, docname: str) -> None:
|
|
45
46
|
self.reading_durations.pop(docname, None)
|
|
46
47
|
|
|
47
|
-
def merge_domaindata(
|
|
48
|
+
def merge_domaindata( # type: ignore[override]
|
|
49
|
+
self, docnames: Set[str], otherdata: _DurationDomainData
|
|
50
|
+
) -> None:
|
|
48
51
|
other_reading_durations = otherdata.get('reading_durations', {})
|
|
49
52
|
docnames_set = frozenset(docnames)
|
|
50
53
|
for docname, duration in other_reading_durations.items():
|
|
@@ -63,13 +66,12 @@ def on_builder_inited(app: Sphinx) -> None:
|
|
|
63
66
|
|
|
64
67
|
def on_source_read(app: Sphinx, docname: str, content: list[str]) -> None:
|
|
65
68
|
"""Start to measure reading duration."""
|
|
66
|
-
app.env.
|
|
69
|
+
app.env.current_document.reading_started_at = time.monotonic()
|
|
67
70
|
|
|
68
71
|
|
|
69
72
|
def on_doctree_read(app: Sphinx, doctree: nodes.document) -> None:
|
|
70
73
|
"""Record a reading duration."""
|
|
71
|
-
|
|
72
|
-
duration = time.monotonic() - started_at
|
|
74
|
+
duration = time.monotonic() - app.env.current_document.reading_started_at
|
|
73
75
|
domain = app.env.domains['duration']
|
|
74
76
|
domain.note_reading_duration(duration)
|
|
75
77
|
|
|
@@ -79,10 +81,14 @@ def on_build_finished(app: Sphinx, error: Exception) -> None:
|
|
|
79
81
|
domain = app.env.domains['duration']
|
|
80
82
|
if not domain.reading_durations:
|
|
81
83
|
return
|
|
82
|
-
durations = sorted(
|
|
84
|
+
durations = sorted(
|
|
85
|
+
domain.reading_durations.items(), key=itemgetter(1), reverse=True
|
|
86
|
+
)
|
|
83
87
|
|
|
84
88
|
logger.info('')
|
|
85
|
-
logger.info(
|
|
89
|
+
logger.info(
|
|
90
|
+
__('====================== slowest reading durations =======================')
|
|
91
|
+
)
|
|
86
92
|
for docname, d in islice(durations, 5):
|
|
87
93
|
logger.info(f'{d:.3f} {docname}') # NoQA: G004
|
|
88
94
|
|
sphinx/ext/extlinks.py
CHANGED
|
@@ -20,7 +20,7 @@ Both, the url string and the caption string must escape ``%`` as ``%%``.
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
22
|
import re
|
|
23
|
-
from typing import TYPE_CHECKING
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
24
|
|
|
25
25
|
from docutils import nodes, utils
|
|
26
26
|
|
|
@@ -32,6 +32,7 @@ from sphinx.util.nodes import split_explicit_title
|
|
|
32
32
|
|
|
33
33
|
if TYPE_CHECKING:
|
|
34
34
|
from collections.abc import Sequence
|
|
35
|
+
from typing import Any
|
|
35
36
|
|
|
36
37
|
from docutils.nodes import Node, system_message
|
|
37
38
|
from docutils.parsers.rst.states import Inliner
|
|
@@ -43,8 +44,7 @@ logger = logging.getLogger(__name__)
|
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
class ExternalLinksChecker(SphinxPostTransform):
|
|
46
|
-
"""
|
|
47
|
-
For each external link, check if it can be replaced by an extlink.
|
|
47
|
+
"""For each external link, check if it can be replaced by an extlink.
|
|
48
48
|
|
|
49
49
|
We treat each ``reference`` node without ``internal`` attribute as an external link.
|
|
50
50
|
"""
|
|
@@ -59,8 +59,7 @@ class ExternalLinksChecker(SphinxPostTransform):
|
|
|
59
59
|
self.check_uri(refnode)
|
|
60
60
|
|
|
61
61
|
def check_uri(self, refnode: nodes.reference) -> None:
|
|
62
|
-
"""
|
|
63
|
-
If the URI in ``refnode`` has a replacement in ``extlinks``,
|
|
62
|
+
"""If the URI in ``refnode`` has a replacement in ``extlinks``,
|
|
64
63
|
emit a warning with a replacement suggestion.
|
|
65
64
|
"""
|
|
66
65
|
if 'internal' in refnode or 'refuri' not in refnode:
|
|
@@ -74,31 +73,38 @@ class ExternalLinksChecker(SphinxPostTransform):
|
|
|
74
73
|
|
|
75
74
|
match = uri_pattern.match(uri)
|
|
76
75
|
if (
|
|
77
|
-
match
|
|
78
|
-
match.groupdict().get('value')
|
|
79
|
-
'/' not in match.groupdict()['value']
|
|
76
|
+
match
|
|
77
|
+
and match.groupdict().get('value')
|
|
78
|
+
and '/' not in match.groupdict()['value']
|
|
80
79
|
):
|
|
81
80
|
# build a replacement suggestion
|
|
82
|
-
msg = __(
|
|
83
|
-
|
|
81
|
+
msg = __(
|
|
82
|
+
'hardcoded link %r could be replaced by an extlink '
|
|
83
|
+
'(try using %r instead)'
|
|
84
|
+
)
|
|
84
85
|
value = match.groupdict().get('value')
|
|
85
86
|
if uri != title:
|
|
86
|
-
replacement = f
|
|
87
|
+
replacement = f':{alias}:`{rst.escape(title)} <{value}>`'
|
|
87
88
|
else:
|
|
88
|
-
replacement = f
|
|
89
|
+
replacement = f':{alias}:`{value}`'
|
|
89
90
|
logger.warning(msg, uri, replacement, location=refnode)
|
|
90
91
|
|
|
91
92
|
|
|
92
|
-
def make_link_role(name: str, base_url: str, caption: str) -> RoleFunction:
|
|
93
|
+
def make_link_role(name: str, base_url: str, caption: str | None) -> RoleFunction:
|
|
93
94
|
# Check whether we have base_url and caption strings have an '%s' for
|
|
94
95
|
# expansion. If not, fall back to the old behaviour and use the string as
|
|
95
96
|
# a prefix.
|
|
96
97
|
# Remark: It is an implementation detail that we use Python's %-formatting.
|
|
97
98
|
# So far we only expose ``%s`` and require quoting of ``%`` using ``%%``.
|
|
98
|
-
def role(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
def role(
|
|
100
|
+
typ: str,
|
|
101
|
+
rawtext: str,
|
|
102
|
+
text: str,
|
|
103
|
+
lineno: int,
|
|
104
|
+
inliner: Inliner,
|
|
105
|
+
options: dict[str, Any] | None = None,
|
|
106
|
+
content: Sequence[str] = (),
|
|
107
|
+
) -> tuple[list[Node], list[system_message]]:
|
|
102
108
|
text = utils.unescape(text)
|
|
103
109
|
has_explicit_title, title, part = split_explicit_title(text)
|
|
104
110
|
full_url = base_url % part
|
|
@@ -108,8 +114,9 @@ def make_link_role(name: str, base_url: str, caption: str) -> RoleFunction:
|
|
|
108
114
|
else:
|
|
109
115
|
title = caption % part
|
|
110
116
|
pnode = nodes.reference(title, title, internal=False, refuri=full_url)
|
|
111
|
-
pnode[
|
|
117
|
+
pnode['classes'].append(f'extlink-{name}')
|
|
112
118
|
return [pnode], []
|
|
119
|
+
|
|
113
120
|
return role
|
|
114
121
|
|
|
115
122
|
|
|
@@ -119,9 +126,14 @@ def setup_link_roles(app: Sphinx) -> None:
|
|
|
119
126
|
|
|
120
127
|
|
|
121
128
|
def setup(app: Sphinx) -> ExtensionMetadata:
|
|
122
|
-
app.add_config_value('extlinks', {}, 'env')
|
|
123
|
-
app.add_config_value(
|
|
129
|
+
app.add_config_value('extlinks', {}, 'env', types=frozenset({dict}))
|
|
130
|
+
app.add_config_value(
|
|
131
|
+
'extlinks_detect_hardcoded_links', False, 'env', types=frozenset({bool})
|
|
132
|
+
)
|
|
124
133
|
|
|
125
134
|
app.connect('builder-inited', setup_link_roles)
|
|
126
135
|
app.add_post_transform(ExternalLinksChecker)
|
|
127
|
-
return {
|
|
136
|
+
return {
|
|
137
|
+
'version': sphinx.__display_version__,
|
|
138
|
+
'parallel_read_safe': True,
|
|
139
|
+
}
|