Skip to content

Commit 3c4b4e3

Browse files
authored
Warn on all redirects if linkcheck_allowed_redirects is an empty dictionary (#13452)
1 parent c40ef2b commit 3c4b4e3

File tree

4 files changed

+60
-11
lines changed

4 files changed

+60
-11
lines changed

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ Features added
1616
* #13332: Add :confval:`doctest_fail_fast` option to exit after the first failed
1717
test.
1818
Patch by Till Hoffmann.
19+
* #13439: linkcheck: Permit warning on every redirect with
20+
``linkcheck_allowed_redirects = {}``.
21+
Patch by Adam Turner.
1922

2023
Bugs fixed
2124
----------

doc/usage/configuration.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3668,6 +3668,11 @@ and which failures and redirects it ignores.
36683668
36693669
.. versionadded:: 4.1
36703670

3671+
.. versionchanged:: 8.3
3672+
Setting :confval:`!linkcheck_allowed_redirects` to the empty directory
3673+
may now be used to warn on all redirects encountered
3674+
by the *linkcheck* builder.
3675+
36713676
.. confval:: linkcheck_anchors
36723677
:type: :code-py:`bool`
36733678
:default: :code-py:`True`

sphinx/builders/linkcheck.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
from sphinx._cli.util.colour import darkgray, darkgreen, purple, red, turquoise
2727
from sphinx.builders.dummy import DummyBuilder
28+
from sphinx.errors import ConfigError
2829
from sphinx.locale import __
2930
from sphinx.transforms.post_transforms import SphinxPostTransform
3031
from sphinx.util import logging, requests
@@ -178,7 +179,7 @@ def process_result(self, result: CheckResult) -> None:
178179
text = 'with unknown code'
179180
linkstat['text'] = text
180181
redirection = f'{text} to {result.message}'
181-
if self.config.linkcheck_allowed_redirects:
182+
if self.config.linkcheck_allowed_redirects is not None:
182183
msg = f'redirect {res_uri} - {redirection}'
183184
logger.warning(msg, location=(result.docname, result.lineno))
184185
else:
@@ -386,7 +387,7 @@ def __init__(
386387
)
387388
self.check_anchors: bool = config.linkcheck_anchors
388389
self.allowed_redirects: dict[re.Pattern[str], re.Pattern[str]]
389-
self.allowed_redirects = config.linkcheck_allowed_redirects
390+
self.allowed_redirects = config.linkcheck_allowed_redirects or {}
390391
self.retries: int = config.linkcheck_retries
391392
self.rate_limit_timeout = config.linkcheck_rate_limit_timeout
392393
self._allow_unauthorized = config.linkcheck_allow_unauthorized
@@ -748,20 +749,26 @@ def rewrite_github_anchor(app: Sphinx, uri: str) -> str | None:
748749

749750

750751
def compile_linkcheck_allowed_redirects(app: Sphinx, config: Config) -> None:
751-
"""Compile patterns in linkcheck_allowed_redirects to the regexp objects."""
752-
linkcheck_allowed_redirects = app.config.linkcheck_allowed_redirects
753-
for url, pattern in list(linkcheck_allowed_redirects.items()):
752+
"""Compile patterns to the regexp objects."""
753+
if config.linkcheck_allowed_redirects is _sentinel_lar:
754+
config.linkcheck_allowed_redirects = None
755+
return
756+
if not isinstance(config.linkcheck_allowed_redirects, dict):
757+
raise ConfigError
758+
allowed_redirects = {}
759+
for url, pattern in config.linkcheck_allowed_redirects.items():
754760
try:
755-
linkcheck_allowed_redirects[re.compile(url)] = re.compile(pattern)
761+
allowed_redirects[re.compile(url)] = re.compile(pattern)
756762
except re.error as exc:
757763
logger.warning(
758764
__('Failed to compile regex in linkcheck_allowed_redirects: %r %s'),
759765
exc.pattern,
760766
exc.msg,
761767
)
762-
finally:
763-
# Remove the original regexp-string
764-
linkcheck_allowed_redirects.pop(url)
768+
config.linkcheck_allowed_redirects = allowed_redirects
769+
770+
771+
_sentinel_lar = object()
765772

766773

767774
def setup(app: Sphinx) -> ExtensionMetadata:
@@ -772,7 +779,9 @@ def setup(app: Sphinx) -> ExtensionMetadata:
772779
app.add_config_value(
773780
'linkcheck_exclude_documents', [], '', types=frozenset({list, tuple})
774781
)
775-
app.add_config_value('linkcheck_allowed_redirects', {}, '', types=frozenset({dict}))
782+
app.add_config_value(
783+
'linkcheck_allowed_redirects', _sentinel_lar, '', types=frozenset({dict})
784+
)
776785
app.add_config_value('linkcheck_auth', [], '', types=frozenset({list, tuple}))
777786
app.add_config_value('linkcheck_request_headers', {}, '', types=frozenset({dict}))
778787
app.add_config_value('linkcheck_retries', 1, '', types=frozenset({int}))
@@ -799,7 +808,8 @@ def setup(app: Sphinx) -> ExtensionMetadata:
799808

800809
app.add_event('linkcheck-process-uri')
801810

802-
app.connect('config-inited', compile_linkcheck_allowed_redirects, priority=800)
811+
# priority 900 to happen after ``check_confval_types()``
812+
app.connect('config-inited', compile_linkcheck_allowed_redirects, priority=900)
803813

804814
# FIXME: Disable URL rewrite handler for github.com temporarily.
805815
# See: https://github.com/sphinx-doc/sphinx/issues/9435

tests/test_builders/test_build_linkcheck.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import wsgiref.handlers
1111
from base64 import b64encode
1212
from http.server import BaseHTTPRequestHandler
13+
from io import StringIO
1314
from queue import Queue
1415
from typing import TYPE_CHECKING
1516
from unittest import mock
@@ -27,6 +28,7 @@
2728
RateLimit,
2829
compile_linkcheck_allowed_redirects,
2930
)
31+
from sphinx.errors import ConfigError
3032
from sphinx.testing.util import SphinxTestApp
3133
from sphinx.util import requests
3234
from sphinx.util._pathlib import _StrPath
@@ -37,6 +39,7 @@
3739

3840
if TYPE_CHECKING:
3941
from collections.abc import Callable, Iterable
42+
from pathlib import Path
4043
from typing import Any
4144

4245
from urllib3 import HTTPConnectionPool
@@ -752,6 +755,34 @@ def test_follows_redirects_on_GET(app, capsys):
752755
assert app.warning.getvalue() == ''
753756

754757

758+
def test_linkcheck_allowed_redirects_config(
759+
make_app: Callable[..., SphinxTestApp], tmp_path: Path
760+
) -> None:
761+
tmp_path.joinpath('conf.py').touch()
762+
tmp_path.joinpath('index.rst').touch()
763+
764+
# ``linkcheck_allowed_redirects = None`` is rejected
765+
warning_stream = StringIO()
766+
with pytest.raises(ConfigError):
767+
make_app(
768+
'linkcheck',
769+
srcdir=tmp_path,
770+
confoverrides={'linkcheck_allowed_redirects': None},
771+
warning=warning_stream,
772+
)
773+
assert strip_escape_sequences(warning_stream.getvalue()).splitlines() == [
774+
"WARNING: The config value `linkcheck_allowed_redirects' has type `NoneType'; expected `dict'."
775+
]
776+
777+
# ``linkcheck_allowed_redirects = {}`` is permitted
778+
app = make_app(
779+
'linkcheck',
780+
srcdir=tmp_path,
781+
confoverrides={'linkcheck_allowed_redirects': {}},
782+
)
783+
assert strip_escape_sequences(app.warning.getvalue()) == ''
784+
785+
755786
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-warn-redirects')
756787
def test_linkcheck_allowed_redirects(app: SphinxTestApp) -> None:
757788
with serve_application(app, make_redirect_handler(support_head=False)) as address:

0 commit comments

Comments
 (0)