Idaho
Idaho

Reputation: 241

have sphinx report broken links

When building html documentation, how do you force sphinx to report, or create an error, on links that don't exist?

Specifically, I have properties and methods within my Python project that have been removed or renamed, and it is hard to find all the dead links with the sphinx generated html output.

I feel like I'm staring at the answer here: http://sphinx-doc.org/glossary.html, as descriped in the opening paragraph.

I'm obviously not understanding something.

Upvotes: 17

Views: 4204

Answers (3)

dEVI
dEVI

Reputation: 1

I wanted to write a test that there are no broken links, but faced several difficulties:

  • running sphinx build in a subprocess doesn't work if you don't want to make actual http requests in tests -> use python API with pytest.mark.vcr
  • getting access to the warnings when using the python api was surprisingly difficult, I couldn't do it using capfd or similar -> use the warning keyword to Sphinx constructor
  • the logged warnings depend on the object type (the :class: prefix). By default, only :any: seems to throw warnings, but I prefer default_role = "py:obj" -> use nitpicky mode. Luckily, its easy to override the conf.py if you don't want to set nitpicky there.
  • I use mypy and the type-hints don't seem to resolve well for sphinx, leading to a ton of warnings -> set autodoc_typehints = "none"
  • running app.build generates a ton of stdout and logging (possibly by extensions) -> set status=StringIO() and use caplog

See below:

import logging
import shutil
import subprocess
from io import StringIO
from pathlib import Path

import pytest
from sphinx.application import Sphinx

#: Log messages from sphinx that should fail the test
MESSAGES_TO_AVOID = {
    "ERROR:",
    "more than one target found",
    "start-string without end-string",
    "undefined label",
    "reference target not found",
}
#: Whitelisted (intersphinx) objects (network access is blocked)
WHITELISTED_REFERENCE_TARGETS = {
    "matplotlib",
    "numpy",
    "pandas",
    "python",
}
#: Path to docs directory
DOCS_DIR = Path(__file__).parent.parent / "docs"
#: Path to build directory
OUTPUT_DIR = DOCS_DIR / "_test_build"


def bad_msg(line: str) -> bool:
    if "<unknown>" in line or any(
        f"reference target not found: {obj}" in line
        for obj in WHITELISTED_REFERENCE_TARGETS
    ):
        return False
    return any(message in line for message in MESSAGES_TO_AVOID)


@pytest.fixture
def clean(capfd):
    shutil.rmtree(OUTPUT_DIR, ignore_errors=True)
    subprocess.run(["make", "-C", str(DOCS_DIR), "clean-api"])
    capfd.readouterr()  # hide stdout output


@pytest.fixture
def warning_io():
    return StringIO()


@pytest.fixture
def app(clean, warning_io):
    app = Sphinx(
        srcdir=str(DOCS_DIR),
        confdir=str(DOCS_DIR),
        outdir=str(OUTPUT_DIR),
        doctreedir=str(OUTPUT_DIR / "doctrees"),
        buildername="html",
        status=StringIO(),  # hide stdout output
        warning=warning_io,
    )
    app.config.nitpicky = True
    # type hints and inherited classes depend on external resources and aren't
    # necessarily fixable, so set them off so that we don't complain about them
    app.config.autodoc_typehints = "none"
    # its interesting that the following option must be `pop`ped, setting it to
    # `False` is not enough. Also ensure the generated apidocs don't have
    # show-inheritance, which is on by default
    app.config.autodoc_default_options.pop("show-inheritance")
    app.config.autodoc_inherit_docstrings = False
    return app


@pytest.mark.block_network
def test_docs(app, warning_io, caplog, capfd):
    subprocess.run(["make", "-C", str(DOCS_DIR), "apidocs"])
    with caplog.at_level(logging.CRITICAL):
        app.build()
    capfd.readouterr()  # hide stdout output
    logs = warning_io.getvalue()
    bad_messages = [line for line in logs.split("\n") if bad_msg(line)]
    if bad_messages:
        bad_logs = "\n".join(bad_messages)
        result = f"{len(bad_messages)} failure cases: {bad_logs}"
        raise AssertionError(result)

Upvotes: 0

mzjn
mzjn

Reputation: 50947

Set the nitpicky configuration variable to True (you can also use the -n option when running sphinx-build).

In nitpicky mode, a cross-reference to a function (such as :func:`myfunc`), class, or other object that cannot be found will generate a warning message.

Upvotes: 18

alecxe
alecxe

Reputation: 473863

I think CheckExternalLinksBuilder is what you're looking for.

It's basically used by calling 'sphinx-build' with -b linkcheck option. Please see sphinx-build for more info. Also, take a look at the list of sphinx-extensions here and here.

Upvotes: 9

Related Questions