Skip to content

Commit bad8f0f

Browse files
committed
Check docs example links locally
1 parent 8c67216 commit bad8f0f

7 files changed

Lines changed: 167 additions & 12 deletions

File tree

cuda_bindings/docs/build_docs.sh

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,18 @@ fi
3838
SPHINXOPTS="${HTML_SPHINXOPTS}" make html
3939

4040
if [[ "${DOCS_LINKCHECK:-0}" == "1" ]]; then
41-
if [[ -z "${LINKCHECK_SPHINXOPTS:-}" ]]; then
42-
LINKCHECK_SPHINXOPTS="-W --keep-going -j 4 -d build/.linkcheck-doctrees"
41+
if [[ -n "${CUDA_PYTHON_DOCS_GITHUB_REF:-}" ]]; then
42+
DOCS_EXAMPLES_REF="${CUDA_PYTHON_DOCS_GITHUB_REF}"
43+
elif [[ "${BUILD_PREVIEW:-0}" == "1" || "${BUILD_LATEST:-0}" == "1" ]]; then
44+
DOCS_EXAMPLES_REF="main"
45+
else
46+
DOCS_EXAMPLES_REF="v${SPHINX_CUDA_BINDINGS_VER}"
4347
fi
44-
SPHINXOPTS="${LINKCHECK_SPHINXOPTS}" make linkcheck BUILDDIR="build/linkcheck/${SPHINX_CUDA_BINDINGS_VER}"
48+
python ../../cuda_python/docs/check_example_links.py \
49+
--source-dir source \
50+
--examples-root cuda_bindings/examples \
51+
--expected-ref "${DOCS_EXAMPLES_REF}" \
52+
--placeholder cuda_bindings_github_ref
4553
fi
4654

4755
# for debugging/developing (conf.py), please comment out the above line and

cuda_bindings/docs/source/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ def _github_examples_ref():
133133

134134
def rewrite_source(app, docname, source):
135135
text = source[0]
136+
text = text.replace("|cuda_bindings_github_ref|", GITHUB_EXAMPLES_REF)
136137

137138
if docname.startswith("release/"):
138139
text = text.replace(".. module:: cuda.bindings\n\n", "", 1)

cuda_core/docs/build_docs.sh

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,18 @@ SPHINXOPTS="${HTML_SPHINXOPTS}"
3838
make html
3939

4040
if [[ "${DOCS_LINKCHECK:-0}" == "1" ]]; then
41-
if [[ -z "${LINKCHECK_SPHINXOPTS:-}" ]]; then
42-
LINKCHECK_SPHINXOPTS="-W --keep-going -j 4 -d build/.linkcheck-doctrees"
41+
if [[ -n "${CUDA_PYTHON_DOCS_GITHUB_REF:-}" ]]; then
42+
DOCS_EXAMPLES_REF="${CUDA_PYTHON_DOCS_GITHUB_REF}"
43+
elif [[ "${BUILD_PREVIEW:-0}" == "1" || "${BUILD_LATEST:-0}" == "1" ]]; then
44+
DOCS_EXAMPLES_REF="main"
45+
else
46+
DOCS_EXAMPLES_REF="cuda-core-v${SPHINX_CUDA_CORE_VER}"
4347
fi
44-
SPHINXOPTS="${LINKCHECK_SPHINXOPTS}" make linkcheck BUILDDIR="build/linkcheck/${SPHINX_CUDA_CORE_VER}"
48+
python ../../cuda_python/docs/check_example_links.py \
49+
--source-dir source \
50+
--examples-root cuda_core/examples \
51+
--expected-ref "${DOCS_EXAMPLES_REF}" \
52+
--placeholder cuda_core_github_ref
4553
fi
4654

4755
# to support version dropdown menu

cuda_core/docs/source/conf.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,11 @@ def skip_member(app, what, name, obj, skip, options):
200200
return None
201201

202202

203+
def rewrite_source(app, docname, source):
204+
source[0] = source[0].replace("|cuda_core_github_ref|", GITHUB_EXAMPLES_REF)
205+
206+
203207
def setup(app):
204208
app.connect("autodoc-process-docstring", autodoc_process_docstring)
205209
app.connect("autodoc-skip-member", skip_member)
210+
app.connect("source-read", rewrite_source)

cuda_core/docs/source/release/0.3.1-notes.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.. SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
1+
.. SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
.. SPDX-License-Identifier: Apache-2.0
33
44
.. currentmodule:: cuda.core.experimental
@@ -31,8 +31,8 @@ None.
3131
New examples
3232
------------
3333

34-
- Add a `CUDA graph <https://github.com/NVIDIA/cuda-python/blob/main/cuda_core/examples/cuda_graphs.py>`_ example.
35-
- Add a `memory resource <https://github.com/NVIDIA/cuda-python/blob/main/cuda_core/examples/memory_ops.py>`_ example.
34+
- Add a `CUDA graph <https://github.com/NVIDIA/cuda-python/blob/|cuda_core_github_ref|/cuda_core/examples/cuda_graphs.py>`_ example.
35+
- Add a `memory resource <https://github.com/NVIDIA/cuda-python/blob/|cuda_core_github_ref|/cuda_core/examples/memory_ops.py>`_ example.
3636

3737

3838
Fixes and enhancements

cuda_pathfinder/docs/build_docs.sh

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,18 @@ fi
3838
SPHINXOPTS="${HTML_SPHINXOPTS}" make html
3939

4040
if [[ "${DOCS_LINKCHECK:-0}" == "1" ]]; then
41-
if [[ -z "${LINKCHECK_SPHINXOPTS:-}" ]]; then
42-
LINKCHECK_SPHINXOPTS="-W --keep-going -j 4 -d build/.linkcheck-doctrees"
41+
if [[ -n "${CUDA_PYTHON_DOCS_GITHUB_REF:-}" ]]; then
42+
DOCS_EXAMPLES_REF="${CUDA_PYTHON_DOCS_GITHUB_REF}"
43+
elif [[ "${BUILD_PREVIEW:-0}" == "1" || "${BUILD_LATEST:-0}" == "1" ]]; then
44+
DOCS_EXAMPLES_REF="main"
45+
else
46+
DOCS_EXAMPLES_REF="cuda-pathfinder-v${SPHINX_CUDA_PATHFINDER_VER}"
4347
fi
44-
SPHINXOPTS="${LINKCHECK_SPHINXOPTS}" make linkcheck BUILDDIR="build/linkcheck/${SPHINX_CUDA_PATHFINDER_VER}"
48+
python ../../cuda_python/docs/check_example_links.py \
49+
--source-dir source \
50+
--examples-root cuda_pathfinder/examples \
51+
--expected-ref "${DOCS_EXAMPLES_REF}" \
52+
--allow-empty
4553
fi
4654

4755
# for debugging/developing (conf.py), please comment out the above line and
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#!/usr/bin/env python3
2+
3+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
4+
# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE
5+
6+
from __future__ import annotations
7+
8+
import argparse
9+
import re
10+
import sys
11+
from pathlib import Path
12+
from urllib.parse import unquote
13+
14+
CUDA_PYTHON_URL_RE = re.compile(
15+
r"https://github\.com/NVIDIA/cuda-python/"
16+
r"(?P<kind>blob|tree)/"
17+
r"(?P<ref>[^/\s<>`]+)/"
18+
r"(?P<path>[^\s<>`)]+)"
19+
)
20+
SOURCE_SUFFIXES = {".md", ".rst"}
21+
22+
23+
def _source_files(source_dir: Path):
24+
for path in sorted(source_dir.rglob("*")):
25+
if path.suffix in SOURCE_SUFFIXES and path.is_file():
26+
yield path
27+
28+
29+
def _normalize_url_path(url_path: str) -> str:
30+
path = unquote(url_path)
31+
for separator in ("#", "?"):
32+
path = path.split(separator, 1)[0]
33+
return path.rstrip(".")
34+
35+
36+
def _is_within(path: str, root: str) -> bool:
37+
return path == root or path.startswith(f"{root}/")
38+
39+
40+
def _display_path(path: Path, repo_root: Path) -> str:
41+
try:
42+
return str(path.relative_to(repo_root))
43+
except ValueError:
44+
return str(path)
45+
46+
47+
def check_links(args: argparse.Namespace) -> int:
48+
repo_root = args.repo_root.resolve()
49+
source_dir = args.source_dir.resolve()
50+
examples_root = args.examples_root.strip("/")
51+
placeholder_ref = f"|{args.placeholder}|" if args.placeholder else None
52+
checked = 0
53+
failures: list[str] = []
54+
55+
for source_path in _source_files(source_dir):
56+
text = source_path.read_text(encoding="utf-8")
57+
for match in CUDA_PYTHON_URL_RE.finditer(text):
58+
url_path = _normalize_url_path(match.group("path"))
59+
if not _is_within(url_path, examples_root):
60+
continue
61+
62+
checked += 1
63+
ref = match.group("ref")
64+
kind = match.group("kind")
65+
location = _display_path(source_path, repo_root)
66+
target_path = Path(url_path)
67+
target = repo_root / target_path
68+
69+
if target_path.is_absolute() or ".." in target_path.parts:
70+
failures.append(f"{location}: invalid repository path in {match.group(0)}")
71+
continue
72+
73+
if placeholder_ref and ref == placeholder_ref:
74+
rendered_ref = args.expected_ref
75+
elif ref == args.expected_ref:
76+
rendered_ref = ref
77+
else:
78+
expected = placeholder_ref or args.expected_ref
79+
failures.append(f"{location}: {match.group(0)} uses ref {ref!r}; expected {expected!r}")
80+
rendered_ref = ref
81+
82+
if kind == "blob" and not target.is_file():
83+
failures.append(
84+
f"{location}: {match.group(0)} resolves to missing file "
85+
f"{target.relative_to(repo_root)} at ref {rendered_ref}"
86+
)
87+
elif kind == "tree" and not target.is_dir():
88+
failures.append(
89+
f"{location}: {match.group(0)} resolves to missing directory "
90+
f"{target.relative_to(repo_root)} at ref {rendered_ref}"
91+
)
92+
93+
if checked == 0 and not args.allow_empty:
94+
failures.append(f"No example links under {examples_root!r} found in {source_dir}")
95+
96+
if failures:
97+
sys.stderr.write("Example link check failed:\n")
98+
for failure in failures:
99+
sys.stderr.write(f" - {failure}\n")
100+
return 1
101+
102+
sys.stdout.write(f"Checked {checked} example link(s) under {examples_root} against ref {args.expected_ref}\n")
103+
return 0
104+
105+
106+
def parse_args(argv: list[str]) -> argparse.Namespace:
107+
repo_root = Path(__file__).resolve().parents[2]
108+
109+
parser = argparse.ArgumentParser(description="Validate cuda-python example links without probing GitHub.")
110+
parser.add_argument("--source-dir", type=Path, required=True)
111+
parser.add_argument("--examples-root", required=True)
112+
parser.add_argument("--expected-ref", required=True)
113+
parser.add_argument("--placeholder")
114+
parser.add_argument("--repo-root", type=Path, default=repo_root)
115+
parser.add_argument("--allow-empty", action="store_true")
116+
return parser.parse_args(argv)
117+
118+
119+
def main(argv: list[str] | None = None) -> int:
120+
args = parse_args(sys.argv[1:] if argv is None else argv)
121+
return check_links(args)
122+
123+
124+
if __name__ == "__main__":
125+
raise SystemExit(main())

0 commit comments

Comments
 (0)