Sphinx 8.1.3__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 +829 -480
- 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.3.dist-info → sphinx-8.2.0.dist-info}/LICENSE.rst +1 -1
- {sphinx-8.1.3.dist-info → sphinx-8.2.0.dist-info}/METADATA +25 -15
- sphinx-8.2.0.dist-info/RECORD +606 -0
- {sphinx-8.1.3.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.3.dist-info/RECORD +0 -598
- {sphinx-8.1.3.dist-info → sphinx-8.2.0.dist-info}/entry_points.txt +0 -0
sphinx/builders/linkcheck.py
CHANGED
|
@@ -7,29 +7,34 @@ import json
|
|
|
7
7
|
import re
|
|
8
8
|
import socket
|
|
9
9
|
import time
|
|
10
|
+
from enum import StrEnum
|
|
10
11
|
from html.parser import HTMLParser
|
|
11
|
-
from os import path
|
|
12
12
|
from queue import PriorityQueue, Queue
|
|
13
13
|
from threading import Thread
|
|
14
14
|
from typing import TYPE_CHECKING, NamedTuple, cast
|
|
15
15
|
from urllib.parse import quote, unquote, urlparse, urlsplit, urlunparse
|
|
16
16
|
|
|
17
17
|
from docutils import nodes
|
|
18
|
-
from requests.exceptions import
|
|
18
|
+
from requests.exceptions import (
|
|
19
|
+
ConnectionError, # NoQA: A004
|
|
20
|
+
HTTPError,
|
|
21
|
+
SSLError,
|
|
22
|
+
TooManyRedirects,
|
|
23
|
+
)
|
|
19
24
|
from requests.exceptions import Timeout as RequestTimeout
|
|
20
25
|
|
|
26
|
+
from sphinx._cli.util.colour import darkgray, darkgreen, purple, red, turquoise
|
|
21
27
|
from sphinx.builders.dummy import DummyBuilder
|
|
22
28
|
from sphinx.locale import __
|
|
23
29
|
from sphinx.transforms.post_transforms import SphinxPostTransform
|
|
24
30
|
from sphinx.util import logging, requests
|
|
25
31
|
from sphinx.util._uri import encode_uri
|
|
26
|
-
from sphinx.util.console import darkgray, darkgreen, purple, red, turquoise
|
|
27
32
|
from sphinx.util.http_date import rfc1123_to_epoch
|
|
28
33
|
from sphinx.util.nodes import get_node_line
|
|
29
34
|
|
|
30
35
|
if TYPE_CHECKING:
|
|
31
36
|
from collections.abc import Callable, Iterator
|
|
32
|
-
from typing import Any
|
|
37
|
+
from typing import Any, Literal, TypeAlias
|
|
33
38
|
|
|
34
39
|
from requests import Response
|
|
35
40
|
|
|
@@ -38,6 +43,20 @@ if TYPE_CHECKING:
|
|
|
38
43
|
from sphinx.util._pathlib import _StrPath
|
|
39
44
|
from sphinx.util.typing import ExtensionMetadata
|
|
40
45
|
|
|
46
|
+
_URIProperties: TypeAlias = tuple['_Status', str, int]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class _Status(StrEnum):
|
|
50
|
+
BROKEN = 'broken'
|
|
51
|
+
IGNORED = 'ignored'
|
|
52
|
+
RATE_LIMITED = 'rate-limited'
|
|
53
|
+
REDIRECTED = 'redirected'
|
|
54
|
+
TIMEOUT = 'timeout'
|
|
55
|
+
UNCHECKED = 'unchecked'
|
|
56
|
+
UNKNOWN = 'unknown'
|
|
57
|
+
WORKING = 'working'
|
|
58
|
+
|
|
59
|
+
|
|
41
60
|
logger = logging.getLogger(__name__)
|
|
42
61
|
|
|
43
62
|
# matches to foo:// and // (a protocol relative URL)
|
|
@@ -52,9 +71,7 @@ DEFAULT_DELAY = 60.0
|
|
|
52
71
|
|
|
53
72
|
|
|
54
73
|
class CheckExternalLinksBuilder(DummyBuilder):
|
|
55
|
-
"""
|
|
56
|
-
Checks for broken external links.
|
|
57
|
-
"""
|
|
74
|
+
"""Checks for broken external links."""
|
|
58
75
|
|
|
59
76
|
name = 'linkcheck'
|
|
60
77
|
epilog = __('Look for any errors in the above output or in %(outdir)s/output.txt')
|
|
@@ -70,8 +87,8 @@ class CheckExternalLinksBuilder(DummyBuilder):
|
|
|
70
87
|
checker = HyperlinkAvailabilityChecker(self.config)
|
|
71
88
|
logger.info('')
|
|
72
89
|
|
|
73
|
-
output_text =
|
|
74
|
-
output_json =
|
|
90
|
+
output_text = self.outdir / 'output.txt'
|
|
91
|
+
output_json = self.outdir / 'output.json'
|
|
75
92
|
with (
|
|
76
93
|
open(output_text, 'w', encoding='utf-8') as self.txt_outfile,
|
|
77
94
|
open(output_json, 'w', encoding='utf-8') as self.json_outfile,
|
|
@@ -84,112 +101,107 @@ class CheckExternalLinksBuilder(DummyBuilder):
|
|
|
84
101
|
|
|
85
102
|
def process_result(self, result: CheckResult) -> None:
|
|
86
103
|
filename = self.env.doc2path(result.docname, False)
|
|
104
|
+
res_uri = result.uri
|
|
87
105
|
|
|
88
|
-
linkstat: dict[str, str | int] = {
|
|
106
|
+
linkstat: dict[str, str | int | _Status] = {
|
|
89
107
|
'filename': str(filename),
|
|
90
108
|
'lineno': result.lineno,
|
|
91
109
|
'status': result.status,
|
|
92
110
|
'code': result.code,
|
|
93
|
-
'uri':
|
|
111
|
+
'uri': res_uri,
|
|
94
112
|
'info': result.message,
|
|
95
113
|
}
|
|
96
114
|
self.write_linkstat(linkstat)
|
|
97
115
|
|
|
98
|
-
if result.status
|
|
99
|
-
|
|
100
|
-
if result.status == 'working' and result.message == 'old':
|
|
101
|
-
return
|
|
102
|
-
if result.lineno:
|
|
116
|
+
if result.lineno and result.status != _Status.UNCHECKED:
|
|
117
|
+
# unchecked links are not logged
|
|
103
118
|
logger.info('(%16s: line %4d) ', result.docname, result.lineno, nonl=True)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
filename,
|
|
130
|
-
result.lineno,
|
|
131
|
-
result.uri + ': ' + result.message,
|
|
132
|
-
)
|
|
133
|
-
self.timed_out_hyperlinks += 1
|
|
134
|
-
elif result.status == 'broken':
|
|
135
|
-
if self.app.quiet:
|
|
136
|
-
logger.warning(
|
|
137
|
-
__('broken link: %s (%s)'),
|
|
138
|
-
result.uri,
|
|
139
|
-
result.message,
|
|
140
|
-
location=(result.docname, result.lineno),
|
|
141
|
-
)
|
|
142
|
-
else:
|
|
143
|
-
logger.info(
|
|
144
|
-
red('broken ') + result.uri + red(' - ' + result.message)
|
|
119
|
+
|
|
120
|
+
match result.status:
|
|
121
|
+
case _Status.RATE_LIMITED | _Status.UNCHECKED:
|
|
122
|
+
pass
|
|
123
|
+
case _Status.IGNORED:
|
|
124
|
+
if result.message:
|
|
125
|
+
msg = f'{res_uri}: {result.message}'
|
|
126
|
+
else:
|
|
127
|
+
msg = res_uri
|
|
128
|
+
logger.info(darkgray('-ignored- ') + msg) # NoQA: G003
|
|
129
|
+
case _Status.WORKING:
|
|
130
|
+
logger.info(darkgreen('ok ') + f'{res_uri}{result.message}') # NoQA: G003
|
|
131
|
+
case _Status.TIMEOUT:
|
|
132
|
+
if self.app.quiet:
|
|
133
|
+
msg = 'timeout ' + f'{res_uri}{result.message}'
|
|
134
|
+
logger.warning(msg, location=(result.docname, result.lineno))
|
|
135
|
+
else:
|
|
136
|
+
msg = red('timeout ') + res_uri + red(f' - {result.message}')
|
|
137
|
+
logger.info(msg)
|
|
138
|
+
self.write_entry(
|
|
139
|
+
_Status.TIMEOUT,
|
|
140
|
+
result.docname,
|
|
141
|
+
filename,
|
|
142
|
+
result.lineno,
|
|
143
|
+
f'{res_uri}: {result.message}',
|
|
145
144
|
)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
text, color = ('with unknown code', purple)
|
|
165
|
-
linkstat['text'] = text
|
|
166
|
-
if self.config.linkcheck_allowed_redirects:
|
|
167
|
-
logger.warning(
|
|
168
|
-
'redirect ' + result.uri + ' - ' + text + ' to ' + result.message,
|
|
169
|
-
location=(result.docname, result.lineno),
|
|
145
|
+
self.timed_out_hyperlinks += 1
|
|
146
|
+
case _Status.BROKEN:
|
|
147
|
+
if self.app.quiet:
|
|
148
|
+
logger.warning(
|
|
149
|
+
__('broken link: %s (%s)'),
|
|
150
|
+
res_uri,
|
|
151
|
+
result.message,
|
|
152
|
+
location=(result.docname, result.lineno),
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
msg = red('broken ') + res_uri + red(f' - {result.message}')
|
|
156
|
+
logger.info(msg)
|
|
157
|
+
self.write_entry(
|
|
158
|
+
_Status.BROKEN,
|
|
159
|
+
result.docname,
|
|
160
|
+
filename,
|
|
161
|
+
result.lineno,
|
|
162
|
+
f'{res_uri}: {result.message}',
|
|
170
163
|
)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
164
|
+
self.broken_hyperlinks += 1
|
|
165
|
+
case _Status.REDIRECTED:
|
|
166
|
+
match result.code:
|
|
167
|
+
case 301:
|
|
168
|
+
text = 'permanently'
|
|
169
|
+
case 302:
|
|
170
|
+
text = 'with Found'
|
|
171
|
+
case 303:
|
|
172
|
+
text = 'with See Other'
|
|
173
|
+
case 307:
|
|
174
|
+
text = 'temporarily'
|
|
175
|
+
case 308:
|
|
176
|
+
text = 'permanently'
|
|
177
|
+
case _:
|
|
178
|
+
text = 'with unknown code'
|
|
179
|
+
linkstat['text'] = text
|
|
180
|
+
redirection = f'{text} to {result.message}'
|
|
181
|
+
if self.config.linkcheck_allowed_redirects:
|
|
182
|
+
msg = f'redirect {res_uri} - {redirection}'
|
|
183
|
+
logger.warning(msg, location=(result.docname, result.lineno))
|
|
184
|
+
else:
|
|
185
|
+
colour = turquoise if result.code == 307 else purple
|
|
186
|
+
msg = colour('redirect ') + res_uri + colour(f' - {redirection}')
|
|
187
|
+
logger.info(msg)
|
|
188
|
+
self.write_entry(
|
|
189
|
+
f'redirected {text}',
|
|
190
|
+
result.docname,
|
|
191
|
+
filename,
|
|
192
|
+
result.lineno,
|
|
193
|
+
f'{res_uri} to {result.message}',
|
|
176
194
|
)
|
|
177
|
-
|
|
178
|
-
'
|
|
179
|
-
|
|
180
|
-
filename,
|
|
181
|
-
result.lineno,
|
|
182
|
-
result.uri + ' to ' + result.message,
|
|
183
|
-
)
|
|
184
|
-
else:
|
|
185
|
-
raise ValueError('Unknown status %s.' % result.status)
|
|
195
|
+
case _Status.UNKNOWN:
|
|
196
|
+
msg = 'Unknown status.'
|
|
197
|
+
raise ValueError(msg)
|
|
186
198
|
|
|
187
|
-
def write_linkstat(self, data: dict[str, str | int]) -> None:
|
|
199
|
+
def write_linkstat(self, data: dict[str, str | int | _Status]) -> None:
|
|
188
200
|
self.json_outfile.write(json.dumps(data))
|
|
189
201
|
self.json_outfile.write('\n')
|
|
190
202
|
|
|
191
203
|
def write_entry(
|
|
192
|
-
self, what: str, docname: str, filename: _StrPath, line: int, uri: str
|
|
204
|
+
self, what: _Status | str, docname: str, filename: _StrPath, line: int, uri: str
|
|
193
205
|
) -> None:
|
|
194
206
|
self.txt_outfile.write(f'{filename}:{line}: [{what}] {uri}\n')
|
|
195
207
|
|
|
@@ -246,11 +258,11 @@ class HyperlinkCollector(SphinxPostTransform):
|
|
|
246
258
|
:param uri: URI to add
|
|
247
259
|
:param node: A node class where the URI was found
|
|
248
260
|
"""
|
|
249
|
-
builder = cast(CheckExternalLinksBuilder, self.app.builder)
|
|
261
|
+
builder = cast('CheckExternalLinksBuilder', self.app.builder)
|
|
250
262
|
hyperlinks = builder.hyperlinks
|
|
251
263
|
docname = self.env.docname
|
|
252
264
|
|
|
253
|
-
if newuri := self.app.emit_firstresult('linkcheck-process-uri', uri):
|
|
265
|
+
if newuri := self.app.events.emit_firstresult('linkcheck-process-uri', uri):
|
|
254
266
|
uri = newuri
|
|
255
267
|
|
|
256
268
|
try:
|
|
@@ -291,7 +303,12 @@ class HyperlinkAvailabilityChecker:
|
|
|
291
303
|
for hyperlink in hyperlinks.values():
|
|
292
304
|
if self.is_ignored_uri(hyperlink.uri):
|
|
293
305
|
yield CheckResult(
|
|
294
|
-
hyperlink.uri,
|
|
306
|
+
uri=hyperlink.uri,
|
|
307
|
+
docname=hyperlink.docname,
|
|
308
|
+
lineno=hyperlink.lineno,
|
|
309
|
+
status=_Status.IGNORED,
|
|
310
|
+
message='',
|
|
311
|
+
code=0,
|
|
295
312
|
)
|
|
296
313
|
else:
|
|
297
314
|
self.wqueue.put(CheckRequest(CHECK_IMMEDIATELY, hyperlink), False)
|
|
@@ -330,7 +347,7 @@ class CheckResult(NamedTuple):
|
|
|
330
347
|
uri: str
|
|
331
348
|
docname: str
|
|
332
349
|
lineno: int
|
|
333
|
-
status:
|
|
350
|
+
status: _Status
|
|
334
351
|
message: str
|
|
335
352
|
code: int
|
|
336
353
|
|
|
@@ -373,16 +390,19 @@ class HyperlinkAvailabilityCheckWorker(Thread):
|
|
|
373
390
|
self.retries: int = config.linkcheck_retries
|
|
374
391
|
self.rate_limit_timeout = config.linkcheck_rate_limit_timeout
|
|
375
392
|
self._allow_unauthorized = config.linkcheck_allow_unauthorized
|
|
393
|
+
self._timeout_status: Literal[_Status.BROKEN, _Status.TIMEOUT]
|
|
376
394
|
if config.linkcheck_report_timeouts_as_broken:
|
|
377
|
-
self._timeout_status =
|
|
395
|
+
self._timeout_status = _Status.BROKEN
|
|
378
396
|
else:
|
|
379
|
-
self._timeout_status =
|
|
397
|
+
self._timeout_status = _Status.TIMEOUT
|
|
380
398
|
|
|
381
399
|
self.user_agent = config.user_agent
|
|
382
400
|
self.tls_verify = config.tls_verify
|
|
383
401
|
self.tls_cacerts = config.tls_cacerts
|
|
384
402
|
|
|
385
|
-
self._session = requests._Session(
|
|
403
|
+
self._session = requests._Session(
|
|
404
|
+
_ignored_redirects=tuple(map(re.compile, config.linkcheck_ignore))
|
|
405
|
+
)
|
|
386
406
|
|
|
387
407
|
super().__init__(daemon=True)
|
|
388
408
|
|
|
@@ -413,17 +433,15 @@ class HyperlinkAvailabilityCheckWorker(Thread):
|
|
|
413
433
|
self.wqueue.task_done()
|
|
414
434
|
continue
|
|
415
435
|
status, info, code = self._check(docname, uri, hyperlink)
|
|
416
|
-
if status ==
|
|
436
|
+
if status == _Status.RATE_LIMITED:
|
|
417
437
|
logger.info(
|
|
418
|
-
darkgray('-rate limited- ') + uri + darkgray(' | sleeping...')
|
|
438
|
+
darkgray('-rate limited- ') + uri + darkgray(' | sleeping...') # NoQA: G003
|
|
419
439
|
)
|
|
420
440
|
else:
|
|
421
441
|
self.rqueue.put(CheckResult(uri, docname, lineno, status, info, code))
|
|
422
442
|
self.wqueue.task_done()
|
|
423
443
|
|
|
424
|
-
def _check(
|
|
425
|
-
self, docname: str, uri: str, hyperlink: Hyperlink
|
|
426
|
-
) -> tuple[str, str, int]:
|
|
444
|
+
def _check(self, docname: str, uri: str, hyperlink: Hyperlink) -> _URIProperties:
|
|
427
445
|
# check for various conditions without bothering the network
|
|
428
446
|
|
|
429
447
|
for doc_matcher in self.documents_exclude:
|
|
@@ -432,25 +450,25 @@ class HyperlinkAvailabilityCheckWorker(Thread):
|
|
|
432
450
|
f'{docname} matched {doc_matcher.pattern} from '
|
|
433
451
|
'linkcheck_exclude_documents'
|
|
434
452
|
)
|
|
435
|
-
return
|
|
453
|
+
return _Status.IGNORED, info, 0
|
|
436
454
|
|
|
437
455
|
if len(uri) == 0 or uri.startswith(('#', 'mailto:', 'tel:')):
|
|
438
|
-
return
|
|
456
|
+
return _Status.UNCHECKED, '', 0
|
|
439
457
|
if not uri.startswith(('http:', 'https:')):
|
|
440
458
|
if uri_re.match(uri):
|
|
441
459
|
# Non-supported URI schemes (ex. ftp)
|
|
442
|
-
return
|
|
460
|
+
return _Status.UNCHECKED, '', 0
|
|
443
461
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
return 'broken', '', 0
|
|
462
|
+
if (hyperlink.docpath.parent / uri).exists():
|
|
463
|
+
return _Status.WORKING, '', 0
|
|
464
|
+
return _Status.BROKEN, '', 0
|
|
448
465
|
|
|
449
466
|
# need to actually check the URI
|
|
450
|
-
status
|
|
467
|
+
status: _Status
|
|
468
|
+
status, info, code = _Status.UNKNOWN, '', 0
|
|
451
469
|
for _ in range(self.retries):
|
|
452
470
|
status, info, code = self._check_uri(uri, hyperlink)
|
|
453
|
-
if status !=
|
|
471
|
+
if status != _Status.BROKEN:
|
|
454
472
|
break
|
|
455
473
|
|
|
456
474
|
return status, info, code
|
|
@@ -464,7 +482,7 @@ class HyperlinkAvailabilityCheckWorker(Thread):
|
|
|
464
482
|
yield self._session.head, {'allow_redirects': True}
|
|
465
483
|
yield self._session.get, {'stream': True}
|
|
466
484
|
|
|
467
|
-
def _check_uri(self, uri: str, hyperlink: Hyperlink) ->
|
|
485
|
+
def _check_uri(self, uri: str, hyperlink: Hyperlink) -> _URIProperties:
|
|
468
486
|
req_url, delimiter, anchor = uri.partition('#')
|
|
469
487
|
if delimiter and anchor:
|
|
470
488
|
for rex in self.anchors_ignore:
|
|
@@ -519,10 +537,14 @@ class HyperlinkAvailabilityCheckWorker(Thread):
|
|
|
519
537
|
try:
|
|
520
538
|
found = contains_anchor(response, anchor)
|
|
521
539
|
except UnicodeDecodeError:
|
|
522
|
-
return
|
|
540
|
+
return (
|
|
541
|
+
_Status.IGNORED,
|
|
542
|
+
'unable to decode response content',
|
|
543
|
+
0,
|
|
544
|
+
)
|
|
523
545
|
if not found:
|
|
524
546
|
return (
|
|
525
|
-
|
|
547
|
+
_Status.BROKEN,
|
|
526
548
|
__("Anchor '%s' not found") % quote(anchor),
|
|
527
549
|
0,
|
|
528
550
|
)
|
|
@@ -531,7 +553,7 @@ class HyperlinkAvailabilityCheckWorker(Thread):
|
|
|
531
553
|
status_code = response.status_code
|
|
532
554
|
redirect_status_code = (
|
|
533
555
|
response.history[-1].status_code if response.history else None
|
|
534
|
-
)
|
|
556
|
+
)
|
|
535
557
|
retry_after = response.headers.get('Retry-After', '')
|
|
536
558
|
response_url = f'{response.url}'
|
|
537
559
|
response.raise_for_status()
|
|
@@ -543,7 +565,7 @@ class HyperlinkAvailabilityCheckWorker(Thread):
|
|
|
543
565
|
|
|
544
566
|
except SSLError as err:
|
|
545
567
|
# SSL failure; report that the link is broken.
|
|
546
|
-
return
|
|
568
|
+
return _Status.BROKEN, str(err), 0
|
|
547
569
|
|
|
548
570
|
except (ConnectionError, TooManyRedirects) as err:
|
|
549
571
|
# Servers drop the connection on HEAD requests, causing
|
|
@@ -551,24 +573,34 @@ class HyperlinkAvailabilityCheckWorker(Thread):
|
|
|
551
573
|
error_message = str(err)
|
|
552
574
|
continue
|
|
553
575
|
|
|
576
|
+
except requests._IgnoredRedirection as err:
|
|
577
|
+
# A redirection to an ignored URI was attempted; report it appropriately
|
|
578
|
+
return (
|
|
579
|
+
_Status.IGNORED,
|
|
580
|
+
f'ignored redirect: {err.destination}',
|
|
581
|
+
err.status_code,
|
|
582
|
+
)
|
|
583
|
+
|
|
554
584
|
except HTTPError as err:
|
|
555
585
|
error_message = str(err)
|
|
556
586
|
|
|
557
587
|
# Unauthorized: the client did not provide required credentials
|
|
558
588
|
if status_code == 401:
|
|
559
|
-
|
|
560
|
-
|
|
589
|
+
if self._allow_unauthorized:
|
|
590
|
+
return _Status.WORKING, 'unauthorized', 0
|
|
591
|
+
else:
|
|
592
|
+
return _Status.BROKEN, 'unauthorized', 0
|
|
561
593
|
|
|
562
594
|
# Rate limiting; back-off if allowed, or report failure otherwise
|
|
563
595
|
if status_code == 429:
|
|
564
596
|
if next_check := self.limit_rate(response_url, retry_after):
|
|
565
597
|
self.wqueue.put(CheckRequest(next_check, hyperlink), False)
|
|
566
|
-
return
|
|
567
|
-
return
|
|
598
|
+
return _Status.RATE_LIMITED, '', 0
|
|
599
|
+
return _Status.BROKEN, error_message, 0
|
|
568
600
|
|
|
569
601
|
# Don't claim success/failure during server-side outages
|
|
570
602
|
if status_code == 503:
|
|
571
|
-
return
|
|
603
|
+
return _Status.IGNORED, 'service unavailable', 0
|
|
572
604
|
|
|
573
605
|
# For most HTTP failures, continue attempting alternate retrieval methods
|
|
574
606
|
continue
|
|
@@ -576,12 +608,12 @@ class HyperlinkAvailabilityCheckWorker(Thread):
|
|
|
576
608
|
except Exception as err:
|
|
577
609
|
# Unhandled exception (intermittent or permanent); report that
|
|
578
610
|
# the link is broken.
|
|
579
|
-
return
|
|
611
|
+
return _Status.BROKEN, str(err), 0
|
|
580
612
|
|
|
581
613
|
else:
|
|
582
614
|
# All available retrieval methods have been exhausted; report
|
|
583
615
|
# that the link is broken.
|
|
584
|
-
return
|
|
616
|
+
return _Status.BROKEN, error_message, 0
|
|
585
617
|
|
|
586
618
|
# Success; clear rate limits for the origin
|
|
587
619
|
netloc = urlsplit(req_url).netloc
|
|
@@ -591,11 +623,11 @@ class HyperlinkAvailabilityCheckWorker(Thread):
|
|
|
591
623
|
(response_url.rstrip('/') == req_url.rstrip('/'))
|
|
592
624
|
or _allowed_redirect(req_url, response_url, self.allowed_redirects)
|
|
593
625
|
): # fmt: skip
|
|
594
|
-
return
|
|
626
|
+
return _Status.WORKING, '', 0
|
|
595
627
|
elif redirect_status_code is not None:
|
|
596
|
-
return
|
|
628
|
+
return _Status.REDIRECTED, response_url, redirect_status_code
|
|
597
629
|
else:
|
|
598
|
-
return
|
|
630
|
+
return _Status.REDIRECTED, response_url, 0
|
|
599
631
|
|
|
600
632
|
def limit_rate(self, response_url: str, retry_after: str | None) -> float | None:
|
|
601
633
|
delay = DEFAULT_DELAY
|
|
@@ -681,7 +713,7 @@ class AnchorCheckParser(HTMLParser):
|
|
|
681
713
|
|
|
682
714
|
def handle_starttag(self, tag: Any, attrs: Any) -> None:
|
|
683
715
|
for key, value in attrs:
|
|
684
|
-
if key in
|
|
716
|
+
if key in {'id', 'name'} and value == self.search_anchor:
|
|
685
717
|
self.found = True
|
|
686
718
|
break
|
|
687
719
|
|
|
@@ -736,29 +768,41 @@ def setup(app: Sphinx) -> ExtensionMetadata:
|
|
|
736
768
|
app.add_builder(CheckExternalLinksBuilder)
|
|
737
769
|
app.add_post_transform(HyperlinkCollector)
|
|
738
770
|
|
|
739
|
-
app.add_config_value('linkcheck_ignore', [], '')
|
|
740
|
-
app.add_config_value(
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
app.add_config_value('
|
|
744
|
-
app.add_config_value('
|
|
745
|
-
app.add_config_value('
|
|
746
|
-
app.add_config_value('
|
|
747
|
-
app.add_config_value('
|
|
771
|
+
app.add_config_value('linkcheck_ignore', [], '', types=frozenset({list, tuple}))
|
|
772
|
+
app.add_config_value(
|
|
773
|
+
'linkcheck_exclude_documents', [], '', types=frozenset({list, tuple})
|
|
774
|
+
)
|
|
775
|
+
app.add_config_value('linkcheck_allowed_redirects', {}, '', types=frozenset({dict}))
|
|
776
|
+
app.add_config_value('linkcheck_auth', [], '', types=frozenset({list, tuple}))
|
|
777
|
+
app.add_config_value('linkcheck_request_headers', {}, '', types=frozenset({dict}))
|
|
778
|
+
app.add_config_value('linkcheck_retries', 1, '', types=frozenset({int}))
|
|
779
|
+
app.add_config_value('linkcheck_timeout', 30, '', types=frozenset({float, int}))
|
|
780
|
+
app.add_config_value('linkcheck_workers', 5, '', types=frozenset({int}))
|
|
781
|
+
app.add_config_value('linkcheck_anchors', True, '', types=frozenset({bool}))
|
|
748
782
|
# Anchors starting with ! are ignored since they are
|
|
749
783
|
# commonly used for dynamic pages
|
|
750
|
-
app.add_config_value(
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
app.add_config_value(
|
|
754
|
-
|
|
784
|
+
app.add_config_value(
|
|
785
|
+
'linkcheck_anchors_ignore', ['^!'], '', types=frozenset({list, tuple})
|
|
786
|
+
)
|
|
787
|
+
app.add_config_value(
|
|
788
|
+
'linkcheck_anchors_ignore_for_url', (), '', types=frozenset({list, tuple})
|
|
789
|
+
)
|
|
790
|
+
app.add_config_value(
|
|
791
|
+
'linkcheck_rate_limit_timeout', 300.0, '', types=frozenset({float, int})
|
|
792
|
+
)
|
|
793
|
+
app.add_config_value(
|
|
794
|
+
'linkcheck_allow_unauthorized', False, '', types=frozenset({bool})
|
|
795
|
+
)
|
|
796
|
+
app.add_config_value(
|
|
797
|
+
'linkcheck_report_timeouts_as_broken', False, '', types=frozenset({bool})
|
|
798
|
+
)
|
|
755
799
|
|
|
756
800
|
app.add_event('linkcheck-process-uri')
|
|
757
801
|
|
|
758
802
|
app.connect('config-inited', compile_linkcheck_allowed_redirects, priority=800)
|
|
759
803
|
|
|
760
804
|
# FIXME: Disable URL rewrite handler for github.com temporarily.
|
|
761
|
-
#
|
|
805
|
+
# See: https://github.com/sphinx-doc/sphinx/issues/9435
|
|
762
806
|
# app.connect('linkcheck-process-uri', rewrite_github_anchor)
|
|
763
807
|
|
|
764
808
|
return {
|
sphinx/builders/manpage.py
CHANGED
|
@@ -3,17 +3,16 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import warnings
|
|
6
|
-
from
|
|
7
|
-
from typing import TYPE_CHECKING, Any
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
8
7
|
|
|
9
8
|
from docutils.frontend import OptionParser
|
|
10
9
|
from docutils.io import FileOutput
|
|
11
10
|
|
|
12
11
|
from sphinx import addnodes
|
|
12
|
+
from sphinx._cli.util.colour import darkgreen
|
|
13
13
|
from sphinx.builders import Builder
|
|
14
14
|
from sphinx.locale import __
|
|
15
15
|
from sphinx.util import logging
|
|
16
|
-
from sphinx.util.console import darkgreen
|
|
17
16
|
from sphinx.util.display import progress_message
|
|
18
17
|
from sphinx.util.nodes import inline_all_toctrees
|
|
19
18
|
from sphinx.util.osutil import ensuredir, make_filename_from_project
|
|
@@ -21,6 +20,7 @@ from sphinx.writers.manpage import ManualPageTranslator, ManualPageWriter
|
|
|
21
20
|
|
|
22
21
|
if TYPE_CHECKING:
|
|
23
22
|
from collections.abc import Set
|
|
23
|
+
from typing import Any
|
|
24
24
|
|
|
25
25
|
from sphinx.application import Sphinx
|
|
26
26
|
from sphinx.config import Config
|
|
@@ -30,9 +30,7 @@ logger = logging.getLogger(__name__)
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class ManualPageBuilder(Builder):
|
|
33
|
-
"""
|
|
34
|
-
Builds groff output in manual page format.
|
|
35
|
-
"""
|
|
33
|
+
"""Builds groff output in manual page format."""
|
|
36
34
|
|
|
37
35
|
name = 'man'
|
|
38
36
|
format = 'man'
|
|
@@ -44,10 +42,7 @@ class ManualPageBuilder(Builder):
|
|
|
44
42
|
def init(self) -> None:
|
|
45
43
|
if not self.config.man_pages:
|
|
46
44
|
logger.warning(
|
|
47
|
-
__(
|
|
48
|
-
'no "man_pages" config value found; no manual pages '
|
|
49
|
-
'will be written'
|
|
50
|
-
)
|
|
45
|
+
__('no "man_pages" config value found; no manual pages will be written')
|
|
51
46
|
)
|
|
52
47
|
|
|
53
48
|
def get_outdated_docs(self) -> str | list[str]:
|
|
@@ -73,7 +68,7 @@ class ManualPageBuilder(Builder):
|
|
|
73
68
|
docname, name, description, authors, section = info
|
|
74
69
|
if docname not in self.env.all_docs:
|
|
75
70
|
logger.warning(
|
|
76
|
-
__('"man_pages" config value references unknown
|
|
71
|
+
__('"man_pages" config value references unknown document %s'),
|
|
77
72
|
docname,
|
|
78
73
|
)
|
|
79
74
|
continue
|
|
@@ -90,14 +85,15 @@ class ManualPageBuilder(Builder):
|
|
|
90
85
|
|
|
91
86
|
if self.config.man_make_section_directory:
|
|
92
87
|
dirname = 'man%s' % section
|
|
93
|
-
ensuredir(
|
|
88
|
+
ensuredir(self.outdir / dirname)
|
|
94
89
|
targetname = f'{dirname}/{name}.{section}'
|
|
95
90
|
else:
|
|
96
91
|
targetname = f'{name}.{section}'
|
|
97
92
|
|
|
98
|
-
logger.info(
|
|
93
|
+
logger.info('%s { ', darkgreen(targetname))
|
|
99
94
|
destination = FileOutput(
|
|
100
|
-
destination_path=
|
|
95
|
+
destination_path=self.outdir / targetname,
|
|
96
|
+
encoding='utf-8',
|
|
101
97
|
)
|
|
102
98
|
|
|
103
99
|
tree = self.env.get_doctree(docname)
|
|
@@ -135,9 +131,13 @@ def default_man_pages(config: Config) -> list[tuple[str, str, str, list[str], in
|
|
|
135
131
|
def setup(app: Sphinx) -> ExtensionMetadata:
|
|
136
132
|
app.add_builder(ManualPageBuilder)
|
|
137
133
|
|
|
138
|
-
app.add_config_value(
|
|
139
|
-
|
|
140
|
-
|
|
134
|
+
app.add_config_value(
|
|
135
|
+
'man_pages', default_man_pages, '', types=frozenset({list, tuple})
|
|
136
|
+
)
|
|
137
|
+
app.add_config_value('man_show_urls', False, '', types=frozenset({bool}))
|
|
138
|
+
app.add_config_value(
|
|
139
|
+
'man_make_section_directory', False, '', types=frozenset({bool})
|
|
140
|
+
)
|
|
141
141
|
|
|
142
142
|
return {
|
|
143
143
|
'version': 'builtin',
|