Skip to content

Commit e8ab5cf

Browse files
linkcheck: Fix linkcheck_allowed_redirects default sentinel (#13483)
Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com>
1 parent 2ce6778 commit e8ab5cf

File tree

3 files changed

+50
-10
lines changed

3 files changed

+50
-10
lines changed

CHANGES.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Features added
4747
Patch by Till Hoffmann.
4848
* #13439: linkcheck: Permit warning on every redirect with
4949
``linkcheck_allowed_redirects = {}``.
50-
Patch by Adam Turner.
50+
Patch by Adam Turner and James Addison.
5151
* #13497: Support C domain objects in the table of contents.
5252
* #13500: LaTeX: add support for ``fontawesome6`` package.
5353
Patch by Jean-François B.

sphinx/builders/linkcheck.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ class _Status(StrEnum):
7171
DEFAULT_DELAY = 60.0
7272

7373

74+
@object.__new__
75+
class _SENTINEL_LAR:
76+
def __repr__(self) -> str:
77+
return '_SENTINEL_LAR'
78+
79+
def __reduce__(self) -> str:
80+
return self.__class__.__name__
81+
82+
7483
class CheckExternalLinksBuilder(DummyBuilder):
7584
"""Checks for broken external links."""
7685

@@ -179,7 +188,7 @@ def process_result(self, result: CheckResult) -> None:
179188
text = 'with unknown code'
180189
linkstat['text'] = text
181190
redirection = f'{text} to {result.message}'
182-
if self.config.linkcheck_allowed_redirects is not None:
191+
if self.config.linkcheck_allowed_redirects is not _SENTINEL_LAR:
183192
msg = f'redirect {res_uri} - {redirection}'
184193
logger.warning(msg, location=(result.docname, result.lineno))
185194
else:
@@ -387,7 +396,7 @@ def __init__(
387396
)
388397
self.check_anchors: bool = config.linkcheck_anchors
389398
self.allowed_redirects: dict[re.Pattern[str], re.Pattern[str]]
390-
self.allowed_redirects = config.linkcheck_allowed_redirects or {}
399+
self.allowed_redirects = config.linkcheck_allowed_redirects
391400
self.retries: int = config.linkcheck_retries
392401
self.rate_limit_timeout = config.linkcheck_rate_limit_timeout
393402
self._allow_unauthorized = config.linkcheck_allow_unauthorized
@@ -722,6 +731,8 @@ def handle_starttag(self, tag: Any, attrs: Any) -> None:
722731
def _allowed_redirect(
723732
url: str, new_url: str, allowed_redirects: dict[re.Pattern[str], re.Pattern[str]]
724733
) -> bool:
734+
if allowed_redirects is _SENTINEL_LAR:
735+
return False
725736
return any(
726737
from_url.match(url) and to_url.match(new_url)
727738
for from_url, to_url in allowed_redirects.items()
@@ -750,8 +761,7 @@ def rewrite_github_anchor(app: Sphinx, uri: str) -> str | None:
750761

751762
def compile_linkcheck_allowed_redirects(app: Sphinx, config: Config) -> None:
752763
"""Compile patterns to the regexp objects."""
753-
if config.linkcheck_allowed_redirects is _sentinel_lar:
754-
config.linkcheck_allowed_redirects = None
764+
if config.linkcheck_allowed_redirects is _SENTINEL_LAR:
755765
return
756766
if not isinstance(config.linkcheck_allowed_redirects, dict):
757767
msg = __(
@@ -772,9 +782,6 @@ def compile_linkcheck_allowed_redirects(app: Sphinx, config: Config) -> None:
772782
config.linkcheck_allowed_redirects = allowed_redirects
773783

774784

775-
_sentinel_lar = object()
776-
777-
778785
def setup(app: Sphinx) -> ExtensionMetadata:
779786
app.add_builder(CheckExternalLinksBuilder)
780787
app.add_post_transform(HyperlinkCollector)
@@ -784,7 +791,7 @@ def setup(app: Sphinx) -> ExtensionMetadata:
784791
'linkcheck_exclude_documents', [], '', types=frozenset({list, tuple})
785792
)
786793
app.add_config_value(
787-
'linkcheck_allowed_redirects', _sentinel_lar, '', types=frozenset({dict})
794+
'linkcheck_allowed_redirects', _SENTINEL_LAR, '', types=frozenset({dict})
788795
)
789796
app.add_config_value('linkcheck_auth', [], '', types=frozenset({list, tuple}))
790797
app.add_config_value('linkcheck_request_headers', {}, '', types=frozenset({dict}))

tests/test_builders/test_build_linkcheck.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -680,7 +680,7 @@ def check_headers(self):
680680
assert content['status'] == 'working'
681681

682682

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

@@ -715,6 +715,7 @@ def log_date_time_string(self):
715715
)
716716
def test_follows_redirects_on_HEAD(app, capsys):
717717
with serve_application(app, make_redirect_handler(support_head=True)) as address:
718+
compile_linkcheck_allowed_redirects(app, app.config)
718719
app.build()
719720
_stdout, stderr = capsys.readouterr()
720721
content = (app.outdir / 'output.txt').read_text(encoding='utf8')
@@ -728,6 +729,9 @@ def test_follows_redirects_on_HEAD(app, capsys):
728729
127.0.0.1 - - [] "HEAD /?redirected=1 HTTP/1.1" 204 -
729730
""",
730731
)
732+
assert (
733+
f'redirect http://{address}/ - with Found to http://{address}/?redirected=1\n'
734+
) in strip_escape_sequences(app.status.getvalue())
731735
assert app.warning.getvalue() == ''
732736

733737

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

757765

766+
@pytest.mark.sphinx(
767+
'linkcheck',
768+
testroot='linkcheck-localserver',
769+
freshenv=True,
770+
confoverrides={'linkcheck_allowed_redirects': {}}, # warn about any redirects
771+
)
772+
def test_warns_disallowed_redirects(app, capsys):
773+
with serve_application(app, make_redirect_handler()) as address:
774+
compile_linkcheck_allowed_redirects(app, app.config)
775+
app.build()
776+
_stdout, stderr = capsys.readouterr()
777+
content = (app.outdir / 'output.txt').read_text(encoding='utf8')
778+
assert content == (
779+
'index.rst:1: [redirected with Found] '
780+
f'http://{address}/ to http://{address}/?redirected=1\n'
781+
)
782+
assert stderr == textwrap.dedent(
783+
"""\
784+
127.0.0.1 - - [] "HEAD / HTTP/1.1" 302 -
785+
127.0.0.1 - - [] "HEAD /?redirected=1 HTTP/1.1" 204 -
786+
""",
787+
)
788+
assert len(app.warning.getvalue().splitlines()) == 1
789+
790+
758791
def test_linkcheck_allowed_redirects_config(
759792
make_app: Callable[..., SphinxTestApp], tmp_path: Path
760793
) -> None:

0 commit comments

Comments
 (0)