|
| 1 | +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| 2 | +# |
| 3 | +# SPDX-License-Identifier: Apache-2.0 |
| 4 | + |
| 5 | +"""Consistency check between ``cuda.core.__all__`` and the public API docs. |
| 6 | +
|
| 7 | +Compares the public symbols exported by ``cuda.core.__all__`` against the |
| 8 | +public entries in ``docs/source/api.rst``. Symbols documented in |
| 9 | +``docs/source/api_private.rst`` (returned helpers users cannot instantiate) |
| 10 | +are accepted as documented. Dotted entries such as ``graph.Graph`` or |
| 11 | +``checkpoint.Process`` describe submodule namespaces, not the flat |
| 12 | +``cuda.core`` namespace, and are excluded from the comparison. |
| 13 | +""" |
| 14 | + |
| 15 | +import pathlib |
| 16 | + |
| 17 | +import pytest |
| 18 | + |
| 19 | +import cuda.core |
| 20 | + |
| 21 | +DOCS_SOURCE_DIR = pathlib.Path(__file__).resolve().parent.parent / "docs" / "source" |
| 22 | + |
| 23 | + |
| 24 | +def _iter_directive_blocks(text): |
| 25 | + """Yield (module, directive, argument, body_lines) for each RST directive. |
| 26 | +
|
| 27 | + ``module`` is the module active at the directive, tracked from |
| 28 | + ``.. module::`` and ``.. currentmodule::`` directives. ``body_lines`` is |
| 29 | + the indented block that follows the directive. |
| 30 | + """ |
| 31 | + module = None |
| 32 | + lines = text.splitlines() |
| 33 | + i = 0 |
| 34 | + while i < len(lines): |
| 35 | + line = lines[i] |
| 36 | + i += 1 |
| 37 | + if not line.startswith(".. "): |
| 38 | + continue |
| 39 | + head, _, argument = line[3:].partition("::") |
| 40 | + directive = head.strip() |
| 41 | + argument = argument.strip() |
| 42 | + if directive in ("module", "currentmodule"): |
| 43 | + module = argument |
| 44 | + continue |
| 45 | + body = [] |
| 46 | + while i < len(lines): |
| 47 | + body_line = lines[i] |
| 48 | + if body_line.strip() and not body_line.startswith(" "): |
| 49 | + break |
| 50 | + body.append(body_line) |
| 51 | + i += 1 |
| 52 | + yield module, directive, argument, body |
| 53 | + |
| 54 | + |
| 55 | +def _documented_names(rst_path, module="cuda.core"): |
| 56 | + """Collect names documented for ``module`` in an RST file. |
| 57 | +
|
| 58 | + Returns the entries of ``autosummary`` blocks plus ``.. data::`` |
| 59 | + arguments that appear while ``module`` is the active module. |
| 60 | + """ |
| 61 | + names = set() |
| 62 | + for active_module, directive, argument, body in _iter_directive_blocks(rst_path.read_text()): |
| 63 | + if active_module != module: |
| 64 | + continue |
| 65 | + if directive == "data": |
| 66 | + names.add(argument) |
| 67 | + elif directive == "autosummary": |
| 68 | + for line in body: |
| 69 | + entry = line.strip() |
| 70 | + if not entry or entry.startswith(":"): |
| 71 | + continue |
| 72 | + names.add(entry) |
| 73 | + return names |
| 74 | + |
| 75 | + |
| 76 | +def _flat_names(names): |
| 77 | + """Filter out dotted (submodule-namespace) entries.""" |
| 78 | + return {name for name in names if "." not in name} |
| 79 | + |
| 80 | + |
| 81 | +@pytest.fixture(scope="module") |
| 82 | +def exported(): |
| 83 | + if not hasattr(cuda.core, "__all__"): |
| 84 | + pytest.skip("cuda.core does not define __all__") |
| 85 | + return set(cuda.core.__all__) |
| 86 | + |
| 87 | + |
| 88 | +@pytest.fixture(scope="module") |
| 89 | +def docs_dir(): |
| 90 | + if not (DOCS_SOURCE_DIR / "api.rst").is_file(): |
| 91 | + pytest.skip("docs sources not available (not running from a source checkout)") |
| 92 | + return DOCS_SOURCE_DIR |
| 93 | + |
| 94 | + |
| 95 | +def test_all_exports_resolve(): |
| 96 | + if not hasattr(cuda.core, "__all__"): |
| 97 | + pytest.skip("cuda.core does not define __all__") |
| 98 | + missing = [name for name in cuda.core.__all__ if not hasattr(cuda.core, name)] |
| 99 | + assert missing == [], f"cuda.core.__all__ lists names that do not resolve: {missing}" |
| 100 | + |
| 101 | + |
| 102 | +def test_public_symbols_are_documented(exported, docs_dir): |
| 103 | + documented = _flat_names(_documented_names(docs_dir / "api.rst")) |
| 104 | + # Returned helpers are deliberately documented in api_private.rst; accept |
| 105 | + # them by their trailing name (e.g. _device_resources.DeviceResources). |
| 106 | + private_documented = {name.rsplit(".", 1)[-1] for name in _documented_names(docs_dir / "api_private.rst")} |
| 107 | + undocumented = exported - documented - private_documented |
| 108 | + assert not undocumented, ( |
| 109 | + f"public by cuda.core.__all__ but missing from public docs (api.rst): {sorted(undocumented)}" |
| 110 | + ) |
| 111 | + |
| 112 | + |
| 113 | +def test_documented_symbols_are_exported(exported, docs_dir): |
| 114 | + documented = _flat_names(_documented_names(docs_dir / "api.rst")) |
| 115 | + unexported = documented - exported |
| 116 | + assert not unexported, ( |
| 117 | + f"documented as public in api.rst but not exported by cuda.core.__all__: {sorted(unexported)}" |
| 118 | + ) |
0 commit comments