From ff17187a00935a3c45ca3c3d112f27b561a2ab90 Mon Sep 17 00:00:00 2001 From: lola Date: Thu, 13 Nov 2025 16:38:19 -0800 Subject: [PATCH 1/5] Check intersphinx inventories before building docs --- docs/conf.py | 10 +----- docs/intersphinx_config.py | 22 ++++++++++++ tests/test_docs.py | 69 +++++++++++++++++++++++++++++++------- 3 files changed, 80 insertions(+), 21 deletions(-) create mode 100644 docs/intersphinx_config.py 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..45cf2626e --- /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 inventory URLs derived from the mapping.""" + for base_url, _ in intersphinx_mapping.values(): + yield base_url.rstrip("/") + "/objects.inv" diff --git a/tests/test_docs.py b/tests/test_docs.py index c2a0f33a3..420c92c55 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -1,28 +1,73 @@ import os -import socket import subprocess +import sys +from urllib.error import HTTPError, URLError +import urllib.request import pytest pytest.importorskip("sphinx") -@pytest.mark.slow -def test_build_docs(): - """Test that the documentation builds, and run doctests. +def _get_intersphinx_urls(): + """Return inventory URLs from docs/intersphinx_config.py.""" + root = os.path.dirname(os.path.dirname(__file__)) + docs_dir = os.path.join(root, "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. - """ + sys.path.insert(0, docs_dir) try: - socket.getaddrinfo("docs.python.org", 80) - except OSError: - pytest.skip("cannot connect to python.org for Intersphinx") + from intersphinx_config import iter_intersphinx_urls + + return list(iter_intersphinx_urls()) + finally: + sys.path.pop(0) + + +def _check_intersphinx_connectivity(timeout=3.0): + """Check Intersphinx inventories before building docs. + + If any inventory URL returns 404, fail as a configuration error. + If any inventory URL has other HTTP or network errors, skip to avoid flaky CI. + """ + urls = _get_intersphinx_urls() + if not urls: + return + + config_errors = [] + network_errors = [] + + for url in urls: + try: + urllib.request.urlopen(url, timeout=timeout).close() + except HTTPError as exc: + if exc.code == 404: + config_errors.append(f"{url} (HTTP 404)") + else: + network_errors.append(f"{url} (HTTP {exc.code})") + except URLError as exc: + network_errors.append(f"{url} ({exc.reason!r})") + except Exception as exc: + network_errors.append(f"{url} ({exc!r})") + + if config_errors: + pytest.fail("intersphinx configuration error(s):\n" + "\n".join(config_errors)) + + if network_errors: + pytest.skip( + "intersphinx network issue(s), skipping docs build:\n" + + "\n".join(network_errors) + ) + + +@pytest.mark.slow +def test_build_docs(): + """Test that the documentation builds and doctests pass.""" + _check_intersphinx_connectivity() - oldDirectory = os.getcwd() + old_directory = os.getcwd() try: os.chdir("docs") subprocess.run(["make", "clean", "html", "SPHINXOPTS=-W"], check=True) subprocess.run(["make", "doctest", "SPHINXOPTS=-W"], check=True) finally: - os.chdir(oldDirectory) + os.chdir(old_directory) From d6c0f4dc3d50bc8c291c6e3058ed8b9c674f4e00 Mon Sep 17 00:00:00 2001 From: lola Date: Thu, 13 Nov 2025 17:17:01 -0800 Subject: [PATCH 2/5] Ignore 403 intersphinx responses; skip only on network failures --- tests/test_docs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_docs.py b/tests/test_docs.py index 420c92c55..6dc515e21 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -23,11 +23,11 @@ def _get_intersphinx_urls(): sys.path.pop(0) -def _check_intersphinx_connectivity(timeout=3.0): +def _check_intersphinx_connectivity(timeout=5.0): """Check Intersphinx inventories before building docs. - If any inventory URL returns 404, fail as a configuration error. - If any inventory URL has other HTTP or network errors, skip to avoid flaky CI. + 404 errors are treated as configuration issues; network failures cause this + test to be skipped. """ urls = _get_intersphinx_urls() if not urls: @@ -43,7 +43,7 @@ def _check_intersphinx_connectivity(timeout=3.0): if exc.code == 404: config_errors.append(f"{url} (HTTP 404)") else: - network_errors.append(f"{url} (HTTP {exc.code})") + continue except URLError as exc: network_errors.append(f"{url} ({exc.reason!r})") except Exception as exc: From fb41ac036f5d75ac267c6864bb2b1097db8c6a4e Mon Sep 17 00:00:00 2001 From: lola Date: Mon, 24 Nov 2025 14:31:41 -0800 Subject: [PATCH 3/5] Simplify Intersphinx connectivity check for docs build --- docs/intersphinx_config.py | 4 ++-- tests/test_docs.py | 47 +++++++++++++++++++------------------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/docs/intersphinx_config.py b/docs/intersphinx_config.py index 45cf2626e..f1dbcc41a 100644 --- a/docs/intersphinx_config.py +++ b/docs/intersphinx_config.py @@ -17,6 +17,6 @@ def iter_intersphinx_urls(): - """Yield the inventory URLs derived from the mapping.""" + """Yield the base URLs from the mapping.""" for base_url, _ in intersphinx_mapping.values(): - yield base_url.rstrip("/") + "/objects.inv" + yield base_url diff --git a/tests/test_docs.py b/tests/test_docs.py index 6dc515e21..a982b6c13 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -1,8 +1,8 @@ import os import subprocess import sys -from urllib.error import HTTPError, URLError -import urllib.request +from urllib.error import URLError +from urllib.request import Request, urlopen import pytest @@ -10,7 +10,7 @@ def _get_intersphinx_urls(): - """Return inventory URLs from docs/intersphinx_config.py.""" + """Return base URLs from docs/intersphinx_config.py.""" root = os.path.dirname(os.path.dirname(__file__)) docs_dir = os.path.join(root, "docs") @@ -24,38 +24,39 @@ def _get_intersphinx_urls(): def _check_intersphinx_connectivity(timeout=5.0): - """Check Intersphinx inventories before building docs. + """Check that Intersphinx sites are reachable before building docs. - 404 errors are treated as configuration issues; network failures cause this - test to be skipped. + Any URL that raises URLError (network or HTTP error) is treated as down; + if this happens, we skip the docs build test to avoid flaky CI. """ urls = _get_intersphinx_urls() if not urls: return - config_errors = [] - network_errors = [] + problems = [] for url in urls: + # Some docs hosts return 403 for the default Python-urllib user agent. + req = Request(url, headers={"User-Agent": "Mozilla/5.0"}) try: - urllib.request.urlopen(url, timeout=timeout).close() - except HTTPError as exc: - if exc.code == 404: - config_errors.append(f"{url} (HTTP 404)") + # Just try to open and then immediately close the response. + with urlopen(req, timeout=timeout): + pass + except (URLError, TimeoutError) as e: + # Prefer .reason if present (typical for URLError), + # otherwise use HTTP status code, otherwise repr(e). + if hasattr(e, "reason"): + msg = e.reason + elif hasattr(e, "code"): + msg = f"HTTP {e.code}" else: - continue - except URLError as exc: - network_errors.append(f"{url} ({exc.reason!r})") - except Exception as exc: - network_errors.append(f"{url} ({exc!r})") + msg = repr(e) + problems.append(f"{url} ({msg})") - if config_errors: - pytest.fail("intersphinx configuration error(s):\n" + "\n".join(config_errors)) - - if network_errors: + if problems: pytest.skip( - "intersphinx network issue(s), skipping docs build:\n" - + "\n".join(network_errors) + "Some Intersphinx sites are not reachable; skipping docs build:\n" + + "\n".join(problems) ) From fee1792f245495d90af53895006561b9c2f0f1b3 Mon Sep 17 00:00:00 2001 From: lola Date: Tue, 25 Nov 2025 13:37:12 -0800 Subject: [PATCH 4/5] simplify docs connectivity check --- tests/test_docs.py | 47 ++++++++++------------------------------------ 1 file changed, 10 insertions(+), 37 deletions(-) diff --git a/tests/test_docs.py b/tests/test_docs.py index a982b6c13..4d5bc41ce 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -1,57 +1,30 @@ import os import subprocess -import sys from urllib.error import URLError from urllib.request import Request, urlopen import pytest -pytest.importorskip("sphinx") - - -def _get_intersphinx_urls(): - """Return base URLs from docs/intersphinx_config.py.""" - root = os.path.dirname(os.path.dirname(__file__)) - docs_dir = os.path.join(root, "docs") - - sys.path.insert(0, docs_dir) - try: - from intersphinx_config import iter_intersphinx_urls +from docs.intersphinx_config import iter_intersphinx_urls - return list(iter_intersphinx_urls()) - finally: - sys.path.pop(0) - - -def _check_intersphinx_connectivity(timeout=5.0): - """Check that Intersphinx sites are reachable before building docs. +pytest.importorskip("sphinx") - Any URL that raises URLError (network or HTTP error) is treated as down; - if this happens, we skip the docs build test to avoid flaky CI. - """ - urls = _get_intersphinx_urls() - if not urls: - return +def _check_intersphinx_connectivity(timeout=10.0): + """Check that Intersphinx sites are reachable before building docs.""" problems = [] - for url in urls: - # Some docs hosts return 403 for the default Python-urllib user agent. + 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: - # Just try to open and then immediately close the response. with urlopen(req, timeout=timeout): pass except (URLError, TimeoutError) as e: - # Prefer .reason if present (typical for URLError), - # otherwise use HTTP status code, otherwise repr(e). - if hasattr(e, "reason"): - msg = e.reason - elif hasattr(e, "code"): - msg = f"HTTP {e.code}" - else: - msg = repr(e) - problems.append(f"{url} ({msg})") + # 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( From 356686a1dcc98a4a7ffd2efaf230358e28e2bdbe Mon Sep 17 00:00:00 2001 From: lola Date: Wed, 17 Dec 2025 16:39:49 -0800 Subject: [PATCH 5/5] Address PR review comments --- tests/test_docs.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_docs.py b/tests/test_docs.py index 4d5bc41ce..4b4598517 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -35,13 +35,17 @@ def _check_intersphinx_connectivity(timeout=10.0): @pytest.mark.slow def test_build_docs(): - """Test that the documentation builds and doctests pass.""" + """Test that the documentation builds, and run doctests. + + We do this in a subprocess since the Sphinx configuration file activates the veneer + and has other side-effects that aren't reset afterward. + """ _check_intersphinx_connectivity() - old_directory = os.getcwd() + oldDirectory = os.getcwd() try: os.chdir("docs") subprocess.run(["make", "clean", "html", "SPHINXOPTS=-W"], check=True) subprocess.run(["make", "doctest", "SPHINXOPTS=-W"], check=True) finally: - os.chdir(old_directory) + os.chdir(oldDirectory)