diff --git a/docs/conf.py b/docs/conf.py index 02628a3ec..b86e53df0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -125,15 +125,7 @@ "show-inheritance": None, } -intersphinx_mapping = { - "python": ("https://docs.python.org/3", None), - "matplotlib": ("https://matplotlib.org/stable/", None), - "numpy": ("https://numpy.org/doc/stable/", None), - "scipy": ("https://docs.scipy.org/doc/scipy/", None), - "sphinx": ("https://www.sphinx-doc.org/en/master/", None), - "pytest": ("https://docs.pytest.org/en/stable/", None), - "trimesh": ("https://trimesh.org/", None), -} +from intersphinx_config import intersphinx_mapping highlight_language = "scenic" pygments_style = "scenic.syntax.pygment.ScenicStyle" diff --git a/docs/intersphinx_config.py b/docs/intersphinx_config.py new file mode 100644 index 000000000..f1dbcc41a --- /dev/null +++ b/docs/intersphinx_config.py @@ -0,0 +1,22 @@ +"""Shared Intersphinx configuration for Scenic's documentation. + +This module defines ``intersphinx_mapping``, which is imported from +``docs/conf.py`` and reused from ``tests/test_docs.py`` to check +connectivity to all configured external documentation sites. +""" + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "matplotlib": ("https://matplotlib.org/stable/", None), + "numpy": ("https://numpy.org/doc/stable/", None), + "scipy": ("https://docs.scipy.org/doc/scipy/", None), + "sphinx": ("https://www.sphinx-doc.org/en/master/", None), + "pytest": ("https://docs.pytest.org/en/stable/", None), + "trimesh": ("https://trimesh.org/", None), +} + + +def iter_intersphinx_urls(): + """Yield the base URLs from the mapping.""" + for base_url, _ in intersphinx_mapping.values(): + yield base_url diff --git a/tests/test_docs.py b/tests/test_docs.py index c2a0f33a3..4b4598517 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -1,12 +1,38 @@ import os -import socket import subprocess +from urllib.error import URLError +from urllib.request import Request, urlopen import pytest +from docs.intersphinx_config import iter_intersphinx_urls + pytest.importorskip("sphinx") +def _check_intersphinx_connectivity(timeout=10.0): + """Check that Intersphinx sites are reachable before building docs.""" + problems = [] + + for url in iter_intersphinx_urls(): + # Some docs hosts don't like the default Python-urllib user-agent. + req = Request(url, headers={"User-Agent": "Mozilla/5.0"}) + try: + with urlopen(req, timeout=timeout): + pass + except (URLError, TimeoutError) as e: + # We've seen slow HTTPS responses raise a bare TimeoutError from the + # SSL layer instead of being wrapped in URLError, so catch both here + # to avoid flaky failures when docs hosts are slow or unresponsive. + problems.append(f"{url} ({e})") + + if problems: + pytest.skip( + "Some Intersphinx sites are not reachable; skipping docs build:\n" + + "\n".join(problems) + ) + + @pytest.mark.slow def test_build_docs(): """Test that the documentation builds, and run doctests. @@ -14,10 +40,7 @@ def test_build_docs(): We do this in a subprocess since the Sphinx configuration file activates the veneer and has other side-effects that aren't reset afterward. """ - try: - socket.getaddrinfo("docs.python.org", 80) - except OSError: - pytest.skip("cannot connect to python.org for Intersphinx") + _check_intersphinx_connectivity() oldDirectory = os.getcwd() try: