From 1fc21b2d22c35c4b3c14c8a4f05ce00427bcb915 Mon Sep 17 00:00:00 2001 From: Aryan Date: Sun, 12 Jul 2026 11:16:55 -0400 Subject: [PATCH 1/2] test(core): add cuda.core.__all__ vs public docs consistency check Closes #2326. Parses docs/source/api.rst (autosummary entries and data directives while cuda.core is the active module) and compares the flat public names against cuda.core.__all__ in both directions. Dotted entries such as graph.Graph or checkpoint.Process are submodule namespaces and are excluded. Symbols documented in api_private.rst are accepted as documented so returned-helper docs do not fail the check. The tests skip when cuda.core.__all__ is not defined, so this lands independently of #2300 and activates once #2300 merges. Also adds the __all__-names-resolve guard suggested in the #2300 review. Signed-off-by: Aryan --- cuda_core/tests/test_api_docs_consistency.py | 118 +++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 cuda_core/tests/test_api_docs_consistency.py diff --git a/cuda_core/tests/test_api_docs_consistency.py b/cuda_core/tests/test_api_docs_consistency.py new file mode 100644 index 0000000000..bb807f4804 --- /dev/null +++ b/cuda_core/tests/test_api_docs_consistency.py @@ -0,0 +1,118 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Consistency check between ``cuda.core.__all__`` and the public API docs. + +Compares the public symbols exported by ``cuda.core.__all__`` against the +public entries in ``docs/source/api.rst``. Symbols documented in +``docs/source/api_private.rst`` (returned helpers users cannot instantiate) +are accepted as documented. Dotted entries such as ``graph.Graph`` or +``checkpoint.Process`` describe submodule namespaces, not the flat +``cuda.core`` namespace, and are excluded from the comparison. +""" + +import pathlib + +import pytest + +import cuda.core + +DOCS_SOURCE_DIR = pathlib.Path(__file__).resolve().parent.parent / "docs" / "source" + + +def _iter_directive_blocks(text): + """Yield (module, directive, argument, body_lines) for each RST directive. + + ``module`` is the module active at the directive, tracked from + ``.. module::`` and ``.. currentmodule::`` directives. ``body_lines`` is + the indented block that follows the directive. + """ + module = None + lines = text.splitlines() + i = 0 + while i < len(lines): + line = lines[i] + i += 1 + if not line.startswith(".. "): + continue + head, _, argument = line[3:].partition("::") + directive = head.strip() + argument = argument.strip() + if directive in ("module", "currentmodule"): + module = argument + continue + body = [] + while i < len(lines): + body_line = lines[i] + if body_line.strip() and not body_line.startswith(" "): + break + body.append(body_line) + i += 1 + yield module, directive, argument, body + + +def _documented_names(rst_path, module="cuda.core"): + """Collect names documented for ``module`` in an RST file. + + Returns the entries of ``autosummary`` blocks plus ``.. data::`` + arguments that appear while ``module`` is the active module. + """ + names = set() + for active_module, directive, argument, body in _iter_directive_blocks(rst_path.read_text()): + if active_module != module: + continue + if directive == "data": + names.add(argument) + elif directive == "autosummary": + for line in body: + entry = line.strip() + if not entry or entry.startswith(":"): + continue + names.add(entry) + return names + + +def _flat_names(names): + """Filter out dotted (submodule-namespace) entries.""" + return {name for name in names if "." not in name} + + +@pytest.fixture(scope="module") +def exported(): + if not hasattr(cuda.core, "__all__"): + pytest.skip("cuda.core does not define __all__") + return set(cuda.core.__all__) + + +@pytest.fixture(scope="module") +def docs_dir(): + if not (DOCS_SOURCE_DIR / "api.rst").is_file(): + pytest.skip("docs sources not available (not running from a source checkout)") + return DOCS_SOURCE_DIR + + +def test_all_exports_resolve(): + if not hasattr(cuda.core, "__all__"): + pytest.skip("cuda.core does not define __all__") + missing = [name for name in cuda.core.__all__ if not hasattr(cuda.core, name)] + assert missing == [], f"cuda.core.__all__ lists names that do not resolve: {missing}" + + +def test_public_symbols_are_documented(exported, docs_dir): + documented = _flat_names(_documented_names(docs_dir / "api.rst")) + # Returned helpers are deliberately documented in api_private.rst; accept + # them by their trailing name (e.g. _device_resources.DeviceResources). + private_documented = {name.rsplit(".", 1)[-1] for name in _documented_names(docs_dir / "api_private.rst")} + undocumented = exported - documented - private_documented + assert not undocumented, ( + f"public by cuda.core.__all__ but missing from public docs (api.rst): {sorted(undocumented)}" + ) + + +def test_documented_symbols_are_exported(exported, docs_dir): + documented = _flat_names(_documented_names(docs_dir / "api.rst")) + unexported = documented - exported + assert not unexported, ( + f"documented as public in api.rst but not exported by cuda.core.__all__: {sorted(unexported)}" + ) From 37cd68ff845dc61f745b3773a5b9fdbf430c5e13 Mon Sep 17 00:00:00 2001 From: Aryan Date: Mon, 13 Jul 2026 15:17:05 -0400 Subject: [PATCH 2/2] test(core): land cuda.core.__all__ and extend docs check to public subpackages Addresses review feedback that the consistency check was too narrow: - Define cuda.core.__all__ (flat public namespace) so the check runs instead of skipping, and add an aggregated __all__ to cuda.core.graph derived from its star-imported submodules. - Auto-discover public subpackages from cuda.core.__path__ (graph, system, texture, utils, and any added later; the internal cuNN wheel shims are excluded) and assert each defines a fully resolvable __all__. - Cross-check each documented subpackage's __all__ against api.rst, handling both the dotted (graph.Graph) and flat (currentmodule) doc conventions. system is documented in api_nvml.rst, so its doc cross-check is skipped. --- cuda_core/cuda/core/__init__.py | 45 +++++++++++ cuda_core/cuda/core/graph/__init__.py | 10 +++ cuda_core/tests/test_api_docs_consistency.py | 84 ++++++++++++++++++-- 3 files changed, 131 insertions(+), 8 deletions(-) diff --git a/cuda_core/cuda/core/__init__.py b/cuda_core/cuda/core/__init__.py index dc6fefdffe..402b4ec7b3 100644 --- a/cuda_core/cuda/core/__init__.py +++ b/cuda_core/cuda/core/__init__.py @@ -109,6 +109,51 @@ class _PatchedProperty(metaclass=_PatchedPropMeta): ) from cuda.core._tensor_map import TensorMapDescriptor, TensorMapDescriptorOptions +# Flat public API of the ``cuda.core`` namespace. Submodule namespaces +# (``checkpoint``, ``graph``, ``system``, ``texture``, ``utils``) carry their +# own ``__all__`` and are intentionally not re-listed here. +__all__ = [ + "LEGACY_DEFAULT_STREAM", + "PER_THREAD_DEFAULT_STREAM", + "Buffer", + "Context", + "ContextOptions", + "Device", + "DeviceMemoryResource", + "DeviceMemoryResourceOptions", + "DeviceResources", + "Event", + "EventOptions", + "GraphMemoryResource", + "GraphicsResource", + "Host", + "Kernel", + "LaunchConfig", + "LegacyPinnedMemoryResource", + "Linker", + "LinkerOptions", + "ManagedBuffer", + "ManagedMemoryResource", + "ManagedMemoryResourceOptions", + "MemoryResource", + "ObjectCode", + "PinnedMemoryResource", + "PinnedMemoryResourceOptions", + "Program", + "ProgramOptions", + "SMResource", + "SMResourceOptions", + "Stream", + "StreamOptions", + "TensorMapDescriptor", + "TensorMapDescriptorOptions", + "VirtualMemoryResource", + "VirtualMemoryResourceOptions", + "WorkqueueResource", + "WorkqueueResourceOptions", + "launch", +] + # isort: split # Texture/surface types live under the cuda.core.texture namespace (not the # flat cuda.core namespace); import the subpackage so it is available as diff --git a/cuda_core/cuda/core/graph/__init__.py b/cuda_core/cuda/core/graph/__init__.py index e109111436..36108c6c0f 100644 --- a/cuda_core/cuda/core/graph/__init__.py +++ b/cuda_core/cuda/core/graph/__init__.py @@ -2,7 +2,17 @@ # # SPDX-License-Identifier: Apache-2.0 +from . import _graph_builder, _graph_definition, _graph_node, _subclasses from ._graph_builder import * from ._graph_definition import * from ._graph_node import * from ._subclasses import * + +# Aggregate the star-imported submodule exports so ``cuda.core.graph`` carries +# an explicit ``__all__`` derived from its parts (no manual list to drift). +__all__ = [ + *_graph_builder.__all__, + *_graph_definition.__all__, + *_graph_node.__all__, + *_subclasses.__all__, +] diff --git a/cuda_core/tests/test_api_docs_consistency.py b/cuda_core/tests/test_api_docs_consistency.py index bb807f4804..c4e0a0f7d5 100644 --- a/cuda_core/tests/test_api_docs_consistency.py +++ b/cuda_core/tests/test_api_docs_consistency.py @@ -2,17 +2,27 @@ # # SPDX-License-Identifier: Apache-2.0 -"""Consistency check between ``cuda.core.__all__`` and the public API docs. - -Compares the public symbols exported by ``cuda.core.__all__`` against the -public entries in ``docs/source/api.rst``. Symbols documented in -``docs/source/api_private.rst`` (returned helpers users cannot instantiate) -are accepted as documented. Dotted entries such as ``graph.Graph`` or -``checkpoint.Process`` describe submodule namespaces, not the flat -``cuda.core`` namespace, and are excluded from the comparison. +"""Consistency checks between the public ``__all__`` surface and the API docs. + +Covers the flat ``cuda.core`` namespace and every public subpackage +(``graph``, ``system``, ``texture``, ``utils``, and any added later) discovered +automatically from ``cuda.core.__path__``. For each, the exported ``__all__`` +is compared against the public entries in ``docs/source/api.rst``. Symbols +documented in ``docs/source/api_private.rst`` (returned helpers users cannot +instantiate) are accepted as documented. + +Docs reference submodule symbols two ways: dotted entries such as +``graph.Graph`` under the ``cuda.core`` module, and flat entries under a +``.. currentmodule:: cuda.core.`` block. Both forms are collected. A +subpackage documented outside ``api.rst`` (for example ``system``, whose +reference lives in ``api_nvml.rst``) is still checked for a well-formed +``__all__``, but its doc cross-check is skipped here. """ +import importlib import pathlib +import pkgutil +import re import pytest @@ -20,6 +30,16 @@ DOCS_SOURCE_DIR = pathlib.Path(__file__).resolve().parent.parent / "docs" / "source" +# ``cuda.core`` ships a versioned wheel shim as ``cu12`` / ``cu13`` subpackages; +# those are an internal packaging mechanism, not public API. +_VERSIONED_SUBPACKAGE = re.compile(r"^cu\d+$") + +PUBLIC_SUBPACKAGES = sorted( + name + for _, name, ispkg in pkgutil.iter_modules(cuda.core.__path__) + if ispkg and not name.startswith("_") and not _VERSIONED_SUBPACKAGE.match(name) +) + def _iter_directive_blocks(text): """Yield (module, directive, argument, body_lines) for each RST directive. @@ -78,6 +98,22 @@ def _flat_names(names): return {name for name in names if "." not in name} +def _documented_subpackage_names(rst_path, sub): + """Collect the public names documented for subpackage ``sub`` in ``rst_path``. + + Combines the two doc conventions: flat entries under + ``.. currentmodule:: cuda.core.`` and dotted ``.Name`` entries + written under the ``cuda.core`` module. + """ + flat = _documented_names(rst_path, module=f"cuda.core.{sub}") + dotted = { + name.split(".", 1)[1] + for name in _documented_names(rst_path, module="cuda.core") + if name.startswith(f"{sub}.") and name.count(".") == 1 + } + return flat | dotted + + @pytest.fixture(scope="module") def exported(): if not hasattr(cuda.core, "__all__"): @@ -92,6 +128,12 @@ def docs_dir(): return DOCS_SOURCE_DIR +def test_public_subpackages_discovered(): + # Guards against a broken __path__ walk silently turning every + # parametrized subpackage check into a no-op. + assert PUBLIC_SUBPACKAGES, "no public cuda.core subpackages were discovered" + + def test_all_exports_resolve(): if not hasattr(cuda.core, "__all__"): pytest.skip("cuda.core does not define __all__") @@ -116,3 +158,29 @@ def test_documented_symbols_are_exported(exported, docs_dir): assert not unexported, ( f"documented as public in api.rst but not exported by cuda.core.__all__: {sorted(unexported)}" ) + + +@pytest.mark.parametrize("sub", PUBLIC_SUBPACKAGES) +def test_subpackage_defines_all(sub): + module = importlib.import_module(f"cuda.core.{sub}") + assert hasattr(module, "__all__"), f"cuda.core.{sub} does not define __all__" + missing = [name for name in module.__all__ if not hasattr(module, name)] + assert missing == [], f"cuda.core.{sub}.__all__ lists names that do not resolve: {missing}" + + +@pytest.mark.parametrize("sub", PUBLIC_SUBPACKAGES) +def test_subpackage_exports_match_docs(sub, docs_dir): + documented = _documented_subpackage_names(docs_dir / "api.rst", sub) + if not documented: + pytest.skip(f"cuda.core.{sub} is not documented in api.rst (its reference lives elsewhere)") + module = importlib.import_module(f"cuda.core.{sub}") + exported = set(module.__all__) + private_documented = {name.rsplit(".", 1)[-1] for name in _documented_names(docs_dir / "api_private.rst")} + undocumented = exported - documented - private_documented + assert not undocumented, ( + f"public by cuda.core.{sub}.__all__ but missing from public docs (api.rst): {sorted(undocumented)}" + ) + unexported = documented - exported + assert not unexported, ( + f"documented as public in api.rst under {sub} but not exported by cuda.core.{sub}.__all__: {sorted(unexported)}" + )