diff --git a/CHANGES.rst b/CHANGES.rst index 82396f2a33e..fede8b5177b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -16,6 +16,9 @@ Features added * #13332: Add :confval:`doctest_fail_fast` option to exit after the first failed test. Patch by Till Hoffmann. +* #13439: linkcheck: Permit warning on every redirect with + ``linkcheck_allowed_redirects = {}``. + Patch by Adam Turner. Bugs fixed ---------- diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index 75e08d7654b..20912d1dc19 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -3668,6 +3668,11 @@ and which failures and redirects it ignores. .. versionadded:: 4.1 + .. versionchanged:: 8.3 + Setting :confval:`!linkcheck_allowed_redirects` to the empty directory + may now be used to warn on all redirects encountered + by the *linkcheck* builder. + .. confval:: linkcheck_anchors :type: :code-py:`bool` :default: :code-py:`True` diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index 93ab2e78b00..e1a80a47c0f 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -25,6 +25,7 @@ from sphinx._cli.util.colour import darkgray, darkgreen, purple, red, turquoise from sphinx.builders.dummy import DummyBuilder +from sphinx.errors import ConfigError from sphinx.locale import __ from sphinx.transforms.post_transforms import SphinxPostTransform from sphinx.util import logging, requests @@ -178,7 +179,7 @@ def process_result(self, result: CheckResult) -> None: text = 'with unknown code' linkstat['text'] = text redirection = f'{text} to {result.message}' - if self.config.linkcheck_allowed_redirects: + if self.config.linkcheck_allowed_redirects is not None: msg = f'redirect {res_uri} - {redirection}' logger.warning(msg, location=(result.docname, result.lineno)) else: @@ -386,7 +387,7 @@ def __init__( ) self.check_anchors: bool = config.linkcheck_anchors self.allowed_redirects: dict[re.Pattern[str], re.Pattern[str]] - self.allowed_redirects = config.linkcheck_allowed_redirects + self.allowed_redirects = config.linkcheck_allowed_redirects or {} self.retries: int = config.linkcheck_retries self.rate_limit_timeout = config.linkcheck_rate_limit_timeout self._allow_unauthorized = config.linkcheck_allow_unauthorized @@ -748,20 +749,26 @@ def rewrite_github_anchor(app: Sphinx, uri: str) -> str | None: def compile_linkcheck_allowed_redirects(app: Sphinx, config: Config) -> None: - """Compile patterns in linkcheck_allowed_redirects to the regexp objects.""" - linkcheck_allowed_redirects = app.config.linkcheck_allowed_redirects - for url, pattern in list(linkcheck_allowed_redirects.items()): + """Compile patterns to the regexp objects.""" + if config.linkcheck_allowed_redirects is _sentinel_lar: + config.linkcheck_allowed_redirects = None + return + if not isinstance(config.linkcheck_allowed_redirects, dict): + raise ConfigError + allowed_redirects = {} + for url, pattern in config.linkcheck_allowed_redirects.items(): try: - linkcheck_allowed_redirects[re.compile(url)] = re.compile(pattern) + allowed_redirects[re.compile(url)] = re.compile(pattern) except re.error as exc: logger.warning( __('Failed to compile regex in linkcheck_allowed_redirects: %r %s'), exc.pattern, exc.msg, ) - finally: - # Remove the original regexp-string - linkcheck_allowed_redirects.pop(url) + config.linkcheck_allowed_redirects = allowed_redirects + + +_sentinel_lar = object() def setup(app: Sphinx) -> ExtensionMetadata: @@ -772,7 +779,9 @@ def setup(app: Sphinx) -> ExtensionMetadata: app.add_config_value( 'linkcheck_exclude_documents', [], '', types=frozenset({list, tuple}) ) - app.add_config_value('linkcheck_allowed_redirects', {}, '', types=frozenset({dict})) + app.add_config_value( + 'linkcheck_allowed_redirects', _sentinel_lar, '', types=frozenset({dict}) + ) app.add_config_value('linkcheck_auth', [], '', types=frozenset({list, tuple})) app.add_config_value('linkcheck_request_headers', {}, '', types=frozenset({dict})) app.add_config_value('linkcheck_retries', 1, '', types=frozenset({int})) @@ -799,7 +808,8 @@ def setup(app: Sphinx) -> ExtensionMetadata: app.add_event('linkcheck-process-uri') - app.connect('config-inited', compile_linkcheck_allowed_redirects, priority=800) + # priority 900 to happen after ``check_confval_types()`` + app.connect('config-inited', compile_linkcheck_allowed_redirects, priority=900) # FIXME: Disable URL rewrite handler for github.com temporarily. # See: https://github.com/sphinx-doc/sphinx/issues/9435 diff --git a/tests/test_builders/test_build_linkcheck.py b/tests/test_builders/test_build_linkcheck.py index 82baa62f3ef..bdd8dea54c1 100644 --- a/tests/test_builders/test_build_linkcheck.py +++ b/tests/test_builders/test_build_linkcheck.py @@ -10,6 +10,7 @@ import wsgiref.handlers from base64 import b64encode from http.server import BaseHTTPRequestHandler +from io import StringIO from queue import Queue from typing import TYPE_CHECKING from unittest import mock @@ -27,6 +28,7 @@ RateLimit, compile_linkcheck_allowed_redirects, ) +from sphinx.errors import ConfigError from sphinx.testing.util import SphinxTestApp from sphinx.util import requests from sphinx.util._pathlib import _StrPath @@ -37,6 +39,7 @@ if TYPE_CHECKING: from collections.abc import Callable, Iterable + from pathlib import Path from typing import Any from urllib3 import HTTPConnectionPool @@ -752,6 +755,34 @@ def test_follows_redirects_on_GET(app, capsys): assert app.warning.getvalue() == '' +def test_linkcheck_allowed_redirects_config( + make_app: Callable[..., SphinxTestApp], tmp_path: Path +) -> None: + tmp_path.joinpath('conf.py').touch() + tmp_path.joinpath('index.rst').touch() + + # ``linkcheck_allowed_redirects = None`` is rejected + warning_stream = StringIO() + with pytest.raises(ConfigError): + make_app( + 'linkcheck', + srcdir=tmp_path, + confoverrides={'linkcheck_allowed_redirects': None}, + warning=warning_stream, + ) + assert strip_escape_sequences(warning_stream.getvalue()).splitlines() == [ + "WARNING: The config value `linkcheck_allowed_redirects' has type `NoneType'; expected `dict'." + ] + + # ``linkcheck_allowed_redirects = {}`` is permitted + app = make_app( + 'linkcheck', + srcdir=tmp_path, + confoverrides={'linkcheck_allowed_redirects': {}}, + ) + assert strip_escape_sequences(app.warning.getvalue()) == '' + + @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-warn-redirects') def test_linkcheck_allowed_redirects(app: SphinxTestApp) -> None: with serve_application(app, make_redirect_handler(support_head=False)) as address: