Skip to content

linkcheck: Fix 'linkcheck_allowed_redirects' sentinel #13483

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c9d6d6f
Tests: uncover a quirk in our ``linkcheck`` tests
jayaddison Apr 4, 2025
5afe46d
linkcheck: prevent `linkcheck_allowed_redirects` value of `None`
jayaddison Apr 14, 2025
a8df874
linkcheck: adjust test coverage
jayaddison Apr 14, 2025
3fa174b
Update CHANGES.rst
jayaddison Apr 14, 2025
a8a2fad
Merge branch 'master' into issue-13462/fix-linkcheck-theme-regression…
jayaddison May 6, 2025
bf12dfd
linkcheck: use boolean literal `True` instead of sentinel as wildcard…
jayaddison May 6, 2025
6356f75
linkcheck: invert `linkcheck_allowed_redirects` boolean value to more…
jayaddison May 6, 2025
90cd874
linkcheck: rectify test comment
jayaddison May 6, 2025
ccf74ee
Merge branch 'master' into issue-13462/fix-linkcheck-theme-regression…
jayaddison May 20, 2025
14a80d3
Merge branch 'master' into issue-13462/fix-linkcheck-theme-regression…
jayaddison Jun 2, 2025
57e4d08
linkcheck: undo a couple of `allowed_redirects` config logic statemen…
jayaddison Jun 2, 2025
6915362
linkcheck: tests: add coverage for status/info-level redirection log …
jayaddison Jun 2, 2025
85cafc5
Merge branch 'master' into issue-13462/fix-linkcheck-theme-regression…
jayaddison Jun 11, 2025
1233e23
Merge branch 'master' into issue-13462/fix-linkcheck-theme-regression…
AA-Turner Aug 18, 2025
2ae6790
Use ... as the sentinel
AA-Turner Aug 18, 2025
49de888
Use None
AA-Turner Aug 18, 2025
f0088b2
Use None
AA-Turner Aug 18, 2025
4d32bee
Use ...
AA-Turner Aug 18, 2025
fd0e769
Use _SENTINEL_LAR
AA-Turner Aug 18, 2025
a186819
__reduce__
AA-Turner Aug 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Features added
Patch by Till Hoffmann.
* #13439: linkcheck: Permit warning on every redirect with
``linkcheck_allowed_redirects = {}``.
Patch by Adam Turner.
Patch by Adam Turner and James Addison.
* #13497: Support C domain objects in the table of contents.
* #13500: LaTeX: add support for ``fontawesome6`` package.
Patch by Jean-François B.
Expand Down
23 changes: 15 additions & 8 deletions sphinx/builders/linkcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ class _Status(StrEnum):
DEFAULT_DELAY = 60.0


@object.__new__
class _SENTINEL_LAR:
def __repr__(self) -> str:
return '_SENTINEL_LAR'

def __reduce__(self) -> str:
return self.__class__.__name__


class CheckExternalLinksBuilder(DummyBuilder):
"""Checks for broken external links."""

Expand Down Expand Up @@ -179,7 +188,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 is not None:
if self.config.linkcheck_allowed_redirects is not _SENTINEL_LAR:
msg = f'redirect {res_uri} - {redirection}'
logger.warning(msg, location=(result.docname, result.lineno))
else:
Expand Down Expand Up @@ -387,7 +396,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 or {}
self.allowed_redirects = config.linkcheck_allowed_redirects
self.retries: int = config.linkcheck_retries
self.rate_limit_timeout = config.linkcheck_rate_limit_timeout
self._allow_unauthorized = config.linkcheck_allow_unauthorized
Expand Down Expand Up @@ -722,6 +731,8 @@ def handle_starttag(self, tag: Any, attrs: Any) -> None:
def _allowed_redirect(
url: str, new_url: str, allowed_redirects: dict[re.Pattern[str], re.Pattern[str]]
) -> bool:
if allowed_redirects is _SENTINEL_LAR:
return False
return any(
from_url.match(url) and to_url.match(new_url)
for from_url, to_url in allowed_redirects.items()
Expand Down Expand Up @@ -750,8 +761,7 @@ def rewrite_github_anchor(app: Sphinx, uri: str) -> str | None:

def compile_linkcheck_allowed_redirects(app: Sphinx, config: Config) -> None:
"""Compile patterns to the regexp objects."""
if config.linkcheck_allowed_redirects is _sentinel_lar:
config.linkcheck_allowed_redirects = None
if config.linkcheck_allowed_redirects is _SENTINEL_LAR:
return
if not isinstance(config.linkcheck_allowed_redirects, dict):
msg = __(
Expand All @@ -772,9 +782,6 @@ def compile_linkcheck_allowed_redirects(app: Sphinx, config: Config) -> None:
config.linkcheck_allowed_redirects = allowed_redirects


_sentinel_lar = object()


def setup(app: Sphinx) -> ExtensionMetadata:
app.add_builder(CheckExternalLinksBuilder)
app.add_post_transform(HyperlinkCollector)
Expand All @@ -784,7 +791,7 @@ def setup(app: Sphinx) -> ExtensionMetadata:
'linkcheck_exclude_documents', [], '', types=frozenset({list, tuple})
)
app.add_config_value(
'linkcheck_allowed_redirects', _sentinel_lar, '', types=frozenset({dict})
'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}))
Expand Down
35 changes: 34 additions & 1 deletion tests/test_builders/test_build_linkcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,7 @@ def check_headers(self):
assert content['status'] == 'working'


def make_redirect_handler(*, support_head: bool) -> type[BaseHTTPRequestHandler]:
def make_redirect_handler(*, support_head: bool = True) -> type[BaseHTTPRequestHandler]:
class RedirectOnceHandler(BaseHTTPRequestHandler):
protocol_version = 'HTTP/1.1'

Expand Down Expand Up @@ -715,6 +715,7 @@ def log_date_time_string(self):
)
def test_follows_redirects_on_HEAD(app, capsys):
with serve_application(app, make_redirect_handler(support_head=True)) as address:
compile_linkcheck_allowed_redirects(app, app.config)
app.build()
_stdout, stderr = capsys.readouterr()
content = (app.outdir / 'output.txt').read_text(encoding='utf8')
Expand All @@ -728,6 +729,9 @@ def test_follows_redirects_on_HEAD(app, capsys):
127.0.0.1 - - [] "HEAD /?redirected=1 HTTP/1.1" 204 -
""",
)
assert (
f'redirect http://{address}/ - with Found to http://{address}/?redirected=1\n'
) in strip_escape_sequences(app.status.getvalue())
assert app.warning.getvalue() == ''


Expand All @@ -738,6 +742,7 @@ def test_follows_redirects_on_HEAD(app, capsys):
)
def test_follows_redirects_on_GET(app, capsys):
with serve_application(app, make_redirect_handler(support_head=False)) as address:
compile_linkcheck_allowed_redirects(app, app.config)
app.build()
_stdout, stderr = capsys.readouterr()
content = (app.outdir / 'output.txt').read_text(encoding='utf8')
Expand All @@ -752,9 +757,37 @@ def test_follows_redirects_on_GET(app, capsys):
127.0.0.1 - - [] "GET /?redirected=1 HTTP/1.1" 204 -
""",
)
assert (
f'redirect http://{address}/ - with Found to http://{address}/?redirected=1\n'
) in strip_escape_sequences(app.status.getvalue())
assert app.warning.getvalue() == ''


@pytest.mark.sphinx(
'linkcheck',
testroot='linkcheck-localserver',
freshenv=True,
confoverrides={'linkcheck_allowed_redirects': {}}, # warn about any redirects
)
def test_warns_disallowed_redirects(app, capsys):
with serve_application(app, make_redirect_handler()) as address:
compile_linkcheck_allowed_redirects(app, app.config)
app.build()
_stdout, stderr = capsys.readouterr()
content = (app.outdir / 'output.txt').read_text(encoding='utf8')
assert content == (
'index.rst:1: [redirected with Found] '
f'http://{address}/ to http://{address}/?redirected=1\n'
)
assert stderr == textwrap.dedent(
"""\
127.0.0.1 - - [] "HEAD / HTTP/1.1" 302 -
127.0.0.1 - - [] "HEAD /?redirected=1 HTTP/1.1" 204 -
""",
)
assert len(app.warning.getvalue().splitlines()) == 1


def test_linkcheck_allowed_redirects_config(
make_app: Callable[..., SphinxTestApp], tmp_path: Path
) -> None:
Expand Down
Loading