diff --git a/cuda_core/cuda/core/__init__.py b/cuda_core/cuda/core/__init__.py index dc6fefdffea..87941a46217 100644 --- a/cuda_core/cuda/core/__init__.py +++ b/cuda_core/cuda/core/__init__.py @@ -69,45 +69,51 @@ class _PatchedProperty(metaclass=_PatchedPropMeta): from cuda.core import checkpoint, system, utils -from cuda.core._context import Context, ContextOptions -from cuda.core._device import Device -from cuda.core._device_resources import ( - DeviceResources, - SMResource, - SMResourceOptions, - WorkqueueResource, - WorkqueueResourceOptions, +from cuda.core._context import * +from cuda.core._context import __all__ as _context_all +from cuda.core._device import * +from cuda.core._device import __all__ as _device_all +from cuda.core._device_resources import * +from cuda.core._device_resources import __all__ as _device_resources_all +from cuda.core._event import * +from cuda.core._event import __all__ as _event_all +from cuda.core._graphics import * +from cuda.core._graphics import __all__ as _graphics_all +from cuda.core._host import * +from cuda.core._host import __all__ as _host_all +from cuda.core._launch_config import * +from cuda.core._launch_config import __all__ as _launch_config_all +from cuda.core._launcher import * +from cuda.core._launcher import __all__ as _launcher_all +from cuda.core._linker import * +from cuda.core._linker import __all__ as _linker_all +from cuda.core._memory import * +from cuda.core._memory import __all__ as _memory_all +from cuda.core._module import * +from cuda.core._module import __all__ as _module_all +from cuda.core._program import * +from cuda.core._program import __all__ as _program_all +from cuda.core._stream import * +from cuda.core._stream import __all__ as _stream_all +from cuda.core._tensor_map import * +from cuda.core._tensor_map import __all__ as _tensor_map_all + +__all__ = ( + _context_all + + _device_all + + _device_resources_all + + _event_all + + _graphics_all + + _host_all + + _launch_config_all + + _launcher_all + + _linker_all + + _memory_all + + _module_all + + _program_all + + _stream_all + + _tensor_map_all ) -from cuda.core._event import Event, EventOptions -from cuda.core._graphics import GraphicsResource -from cuda.core._host import Host -from cuda.core._launch_config import LaunchConfig -from cuda.core._launcher import launch -from cuda.core._linker import Linker, LinkerOptions -from cuda.core._memory import ( - Buffer, - DeviceMemoryResource, - DeviceMemoryResourceOptions, - GraphMemoryResource, - LegacyPinnedMemoryResource, - ManagedBuffer, - ManagedMemoryResource, - ManagedMemoryResourceOptions, - MemoryResource, - PinnedMemoryResource, - PinnedMemoryResourceOptions, - VirtualMemoryResource, - VirtualMemoryResourceOptions, -) -from cuda.core._module import Kernel, ObjectCode -from cuda.core._program import Program, ProgramOptions -from cuda.core._stream import ( - LEGACY_DEFAULT_STREAM, - PER_THREAD_DEFAULT_STREAM, - Stream, - StreamOptions, -) -from cuda.core._tensor_map import TensorMapDescriptor, TensorMapDescriptorOptions # isort: split # Texture/surface types live under the cuda.core.texture namespace (not the diff --git a/cuda_core/cuda/core/_device.pyi b/cuda_core/cuda/core/_device.pyi index 14893fbd3c0..e83aef8a8d0 100644 --- a/cuda_core/cuda/core/_device.pyi +++ b/cuda_core/cuda/core/_device.pyi @@ -1023,4 +1023,5 @@ class Device: .. versionadded:: 1.1.0 """ _tls = threading.local() -_lock = threading.Lock() \ No newline at end of file +_lock = threading.Lock() +__all__ = ['Device'] \ No newline at end of file diff --git a/cuda_core/cuda/core/_device.pyx b/cuda_core/cuda/core/_device.pyx index ea5bb62cc1b..b026c32c029 100644 --- a/cuda_core/cuda/core/_device.pyx +++ b/cuda_core/cuda/core/_device.pyx @@ -61,6 +61,8 @@ _tls = threading.local() _lock = threading.Lock() cdef bint _is_cuInit = False +__all__ = ['Device'] + cdef class DeviceProperties: """ diff --git a/cuda_core/cuda/core/_event.pyi b/cuda_core/cuda/core/_event.pyi index 1ea91308bc1..9391735b6ab 100644 --- a/cuda_core/cuda/core/_event.pyi +++ b/cuda_core/cuda/core/_event.pyi @@ -180,6 +180,7 @@ class IPCEventDescriptor: def __reduce__(self) -> tuple[object, ...]: ... +__all__ = ['Event', 'EventOptions'] def _reduce_event(event: Event) -> tuple[object, ...]: ... \ No newline at end of file diff --git a/cuda_core/cuda/core/_event.pyx b/cuda_core/cuda/core/_event.pyx index e5cb81ac41e..314347f6cce 100644 --- a/cuda_core/cuda/core/_event.pyx +++ b/cuda_core/cuda/core/_event.pyx @@ -43,6 +43,8 @@ if TYPE_CHECKING: import cuda.bindings.driver # no-cython-lint from cuda.core._device import Device +__all__ = ['Event', 'EventOptions'] + @dataclass cdef class EventOptions: diff --git a/cuda_core/cuda/core/_host.py b/cuda_core/cuda/core/_host.py index e74743d493a..30464409871 100644 --- a/cuda_core/cuda/core/_host.py +++ b/cuda_core/cuda/core/_host.py @@ -6,6 +6,8 @@ import threading from typing import ClassVar +__all__ = ["Host"] + class Host: """Host (CPU) location for managed-memory operations. diff --git a/cuda_core/cuda/core/_launch_config.pyi b/cuda_core/cuda/core/_launch_config.pyi index eac16c1878f..bb47f1901a8 100644 --- a/cuda_core/cuda/core/_launch_config.pyi +++ b/cuda_core/cuda/core/_launch_config.pyi @@ -66,6 +66,7 @@ class LaunchConfig: def __hash__(self) -> int: ... _LAUNCH_CONFIG_ATTRS = ('grid', 'cluster', 'block', 'shmem_size', 'is_cooperative') +__all__ = ['LaunchConfig'] def _to_native_launch_config(config: LaunchConfig) -> object: """Convert LaunchConfig to native driver CUlaunchConfig. diff --git a/cuda_core/cuda/core/_launch_config.pyx b/cuda_core/cuda/core/_launch_config.pyx index a92ecf1f9e3..44dbe2f1cbf 100644 --- a/cuda_core/cuda/core/_launch_config.pyx +++ b/cuda_core/cuda/core/_launch_config.pyx @@ -15,6 +15,8 @@ from cuda.core._utils.cuda_utils import ( _LAUNCH_CONFIG_ATTRS = ('grid', 'cluster', 'block', 'shmem_size', 'is_cooperative') +__all__ = ['LaunchConfig'] + cdef class LaunchConfig: """Customizable launch options. diff --git a/cuda_core/cuda/core/_launcher.pyi b/cuda_core/cuda/core/_launcher.pyi index a292c3eec95..27ed7e86da7 100644 --- a/cuda_core/cuda/core/_launcher.pyi +++ b/cuda_core/cuda/core/_launcher.pyi @@ -8,6 +8,7 @@ from cuda.core._stream import Stream from cuda.core.graph import GraphBuilder from cuda.core.typing import IsStreamType +__all__ = ['launch'] def launch(stream: Stream | GraphBuilder | IsStreamType, config: LaunchConfig, kernel: Kernel, *kernel_args) -> None: """Launches a :obj:`~_module.Kernel` diff --git a/cuda_core/cuda/core/_launcher.pyx b/cuda_core/cuda/core/_launcher.pyx index d5ddaff4d56..036189790e0 100644 --- a/cuda_core/cuda/core/_launcher.pyx +++ b/cuda_core/cuda/core/_launcher.pyx @@ -24,6 +24,8 @@ if TYPE_CHECKING: from cuda.core.graph import GraphBuilder from cuda.core.typing import IsStreamType +__all__ = ['launch'] + def launch( stream: Stream | GraphBuilder | IsStreamType, diff --git a/cuda_core/cuda/core/_memory/__init__.py b/cuda_core/cuda/core/_memory/__init__.py index bf40a643f8c..bdb22c9f6f3 100644 --- a/cuda_core/cuda/core/_memory/__init__.py +++ b/cuda_core/cuda/core/_memory/__init__.py @@ -1,13 +1,34 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 from ._buffer import * +from ._buffer import __all__ as _buffer_all from ._device_memory_resource import * +from ._device_memory_resource import __all__ as _device_memory_resource_all from ._graph_memory_resource import * +from ._graph_memory_resource import __all__ as _graph_memory_resource_all from ._ipc import * +from ._ipc import __all__ as _ipc_all from ._legacy import * -from ._managed_buffer import ManagedBuffer +from ._legacy import __all__ as _legacy_all +from ._managed_buffer import * +from ._managed_buffer import __all__ as _managed_buffer_all from ._managed_memory_resource import * +from ._managed_memory_resource import __all__ as _managed_memory_resource_all from ._pinned_memory_resource import * +from ._pinned_memory_resource import __all__ as _pinned_memory_resource_all from ._virtual_memory_resource import * +from ._virtual_memory_resource import __all__ as _virtual_memory_resource_all + +__all__ = ( + _buffer_all + + _device_memory_resource_all + + _graph_memory_resource_all + + _ipc_all + + _legacy_all + + _managed_buffer_all + + _managed_memory_resource_all + + _pinned_memory_resource_all + + _virtual_memory_resource_all +) diff --git a/cuda_core/cuda/core/_memory/_managed_buffer.py b/cuda_core/cuda/core/_memory/_managed_buffer.py index 9a8333e7094..83a6c618864 100644 --- a/cuda_core/cuda/core/_memory/_managed_buffer.py +++ b/cuda_core/cuda/core/_memory/_managed_buffer.py @@ -25,6 +25,8 @@ from cuda.core._stream import Stream from cuda.core.graph import GraphBuilder +__all__ = ["ManagedBuffer"] + _INT_SIZE = 4 diff --git a/cuda_core/cuda/core/_stream.pyi b/cuda_core/cuda/core/_stream.pyi index f4d78982a1d..efdcca29256 100644 --- a/cuda_core/cuda/core/_stream.pyi +++ b/cuda_core/cuda/core/_stream.pyi @@ -212,6 +212,7 @@ class Stream: Newly created graph builder object. """ +__all__ = ['LEGACY_DEFAULT_STREAM', 'PER_THREAD_DEFAULT_STREAM', 'Stream', 'StreamOptions'] LEGACY_DEFAULT_STREAM: Stream = Stream._legacy_default() PER_THREAD_DEFAULT_STREAM: Stream = Stream._per_thread_default() diff --git a/cuda_core/cuda/core/_stream.pyx b/cuda_core/cuda/core/_stream.pyx index 5212ec5c7de..87911226860 100644 --- a/cuda_core/cuda/core/_stream.pyx +++ b/cuda_core/cuda/core/_stream.pyx @@ -47,6 +47,9 @@ if TYPE_CHECKING: from cuda.core._device import Device from cuda.core.graph import GraphBuilder +__all__ = ['LEGACY_DEFAULT_STREAM', 'PER_THREAD_DEFAULT_STREAM', 'Stream', 'StreamOptions'] + + @dataclass cdef class StreamOptions: """Customizable :obj:`~_stream.Stream` options. diff --git a/cuda_core/cuda/core/_tensor_map.pyi b/cuda_core/cuda/core/_tensor_map.pyi index 986ab41549f..c6a18ad2399 100644 --- a/cuda_core/cuda/core/_tensor_map.pyi +++ b/cuda_core/cuda/core/_tensor_map.pyi @@ -284,6 +284,7 @@ class TensorMapDescriptor: def __repr__(self) -> str: ... +__all__ = ['TensorMapDescriptor', 'TensorMapDescriptorOptions'] _TMA_DT_UINT8 = int(cydriver.CU_TENSOR_MAP_DATA_TYPE_UINT8) _TMA_DT_UINT16 = int(cydriver.CU_TENSOR_MAP_DATA_TYPE_UINT16) _TMA_DT_UINT32 = int(cydriver.CU_TENSOR_MAP_DATA_TYPE_UINT32) diff --git a/cuda_core/cuda/core/_tensor_map.pyx b/cuda_core/cuda/core/_tensor_map.pyx index 7e059fe7b98..47882affcd4 100644 --- a/cuda_core/cuda/core/_tensor_map.pyx +++ b/cuda_core/cuda/core/_tensor_map.pyx @@ -47,6 +47,8 @@ try: except ImportError: ml_bfloat16 = None +__all__ = ['TensorMapDescriptor', 'TensorMapDescriptorOptions'] + class TensorMapDataType(enum.IntEnum): """Data types for tensor map descriptors. diff --git a/cuda_core/cuda/core/graph/__init__.py b/cuda_core/cuda/core/graph/__init__.py index e1091114368..507888321ea 100644 --- a/cuda_core/cuda/core/graph/__init__.py +++ b/cuda_core/cuda/core/graph/__init__.py @@ -2,7 +2,19 @@ # # 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__, +] + +del _graph_builder, _graph_definition, _graph_node, _subclasses diff --git a/cuda_core/cuda/core/system/__init__.py b/cuda_core/cuda/core/system/__init__.py index 685519f9b80..acb648549bc 100644 --- a/cuda_core/cuda/core/system/__init__.py +++ b/cuda_core/cuda/core/system/__init__.py @@ -12,8 +12,10 @@ __all__ = [ "CUDA_BINDINGS_NVML_IS_COMPATIBLE", + "get_driver_branch", "get_kernel_mode_driver_version", "get_num_devices", + "get_nvml_version", "get_process_name", "get_user_mode_driver_version", ] @@ -40,7 +42,6 @@ from .exceptions import * from .exceptions import __all__ as _exceptions_all - __all__.append("get_nvml_version") __all__.extend(_device_all) __all__.extend(_system_events_all) __all__.extend(_exceptions_all) diff --git a/cuda_core/docs/source/api_nvml.rst b/cuda_core/docs/source/api_nvml.rst index 7780dd6086e..c96b68ab701 100644 --- a/cuda_core/docs/source/api_nvml.rst +++ b/cuda_core/docs/source/api_nvml.rst @@ -45,3 +45,11 @@ Types Device NvlinkInfo + +Constants +--------- + +.. autosummary:: + :toctree: generated/ + + CUDA_BINDINGS_NVML_IS_COMPATIBLE diff --git a/cuda_core/docs/source/api_private.rst b/cuda_core/docs/source/api_private.rst index 907fc2f5bcf..7561512af88 100644 --- a/cuda_core/docs/source/api_private.rst +++ b/cuda_core/docs/source/api_private.rst @@ -43,8 +43,8 @@ CUDA runtime :template: autosummary/cyclass.rst + DeviceResources _device.DeviceProperties - _device_resources.DeviceResources _memory._ipc.IPCAllocationHandle _memory._ipc.IPCBufferDescriptor _memory._managed_buffer.AccessedBySetProxy @@ -125,3 +125,36 @@ NVML system.typing.TemperatureThresholds system.typing.ThermalController system.typing.ThermalTarget + + system.NvmlError + system.UninitializedError + system.InvalidArgumentError + system.NotSupportedError + system.NoPermissionError + system.AlreadyInitializedError + system.NotFoundError + system.InsufficientSizeError + system.InsufficientPowerError + system.DriverNotLoadedError + system.TimeoutError + system.IrqIssueError + system.LibraryNotFoundError + system.FunctionNotFoundError + system.CorruptedInforomError + system.GpuIsLostError + system.ResetRequiredError + system.OperatingSystemError + system.LibRmVersionMismatchError + system.InUseError + system.MemoryError + system.NoDataError + system.VgpuEccNotSupportedError + system.InsufficientResourcesError + system.FreqNotSupportedError + system.ArgumentVersionMismatchError + system.DeprecatedError + system.NotReadyError + system.GpuNotFoundError + system.InvalidStateError + system.ResetTypeNotSupportedError + system.UnknownError diff --git a/cuda_core/pixi.toml b/cuda_core/pixi.toml index 8772ed4e88b..30767983ba1 100644 --- a/cuda_core/pixi.toml +++ b/cuda_core/pixi.toml @@ -21,6 +21,7 @@ pytest-randomly = "*" pytest-repeat = "*" pytest-rerunfailures = "*" cloudpickle = "*" +docutils = "*" psutil = "*" pyglet = "*" diff --git a/cuda_core/pyproject.toml b/cuda_core/pyproject.toml index f3a0c3a70f9..2c6435e4957 100644 --- a/cuda_core/pyproject.toml +++ b/cuda_core/pyproject.toml @@ -69,6 +69,7 @@ test = [ "pytest-timeout==2.4.0", "cloudpickle==3.1.2", "psutil==7.2.2", + "docutils==0.23", # TODO: remove the Python 3.15 guard once 3.15 is officially supported "cffi==2.0.0; python_version < '3.15'", ] 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 00000000000..31f692700af --- /dev/null +++ b/cuda_core/tests/test_api_docs_consistency.py @@ -0,0 +1,239 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""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 public namespace, exported +``__all__`` names must appear somewhere in ``cuda_core/docs/source``. + +The enforced direction is deliberately one-way (public export -> documented). +This is intentionally a *name-presence* check, and it does not verify: + +- the reverse direction (documented -> exported): documenting a private or + internal symbol on any page is allowed, so a documented name is never + required to be public; +- signatures, docstrings, parameter lists, or rendered output: only that each + exported name appears as a documented entry; +- whether an entry is marked ``:no-index:`` or deprecated: such entries still + count as documented; +- class members or attributes nested below the namespace level: only top-level + names of each namespace are matched (entries deeper than + ``.`` are ignored); +- docs outside the top-level ``docs/source/*.rst`` files: nested pages are not + scanned. +""" + +import collections +import importlib +import io +import pathlib +import pkgutil +import re + +import pytest +from docutils import nodes +from docutils.core import publish_doctree +from docutils.parsers.rst import Directive, directives + +import cuda.core + +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) +) + + +class _ModuleNode(nodes.Element): + pass + + +class _AutosummaryNode(nodes.Element): + pass + + +class _DataNode(nodes.Element): + pass + + +class _ModuleDirective(Directive): + required_arguments = 1 + final_argument_whitespace = False + has_content = True + option_spec = { + "deprecated": directives.unchanged, + "no-index": directives.flag, + "platform": directives.unchanged, + "synopsis": directives.unchanged, + } + + def run(self): + node = _ModuleNode() + node["module"] = self.arguments[0].strip() + self.state.nested_parse(self.content, self.content_offset, node) + return [node] + + +class _AutosummaryDirective(Directive): + has_content = True + option_spec = { + "caption": directives.unchanged, + "nosignatures": directives.flag, + "recursive": directives.flag, + "template": directives.unchanged, + "toctree": directives.unchanged, + } + + def run(self): + node = _AutosummaryNode() + node["entries"] = [entry for line in self.content if (entry := line.strip()) and not entry.startswith(":")] + return [node] + + +class _DataDirective(Directive): + required_arguments = 1 + final_argument_whitespace = True + has_content = True + option_spec = { + "annotation": directives.unchanged, + "no-index": directives.flag, + "type": directives.unchanged, + "value": directives.unchanged, + } + + def run(self): + node = _DataNode() + node["name"] = self.arguments[0].strip() + return [node] + + +# These patch the global docutils directive registry for the process lifetime. +# Safe as long as no other test module in the same session uses docutils or +# Sphinx with the real autosummary/module/data directives. If that ever changes, +# move these calls into a session-scoped autouse fixture that saves and restores +# the previous mapping. +directives.register_directive("autosummary", _AutosummaryDirective) +directives.register_directive("currentmodule", _ModuleDirective) +directives.register_directive("data", _DataDirective) +directives.register_directive("module", _ModuleDirective) + + +def _iter_documented_entries(rst_path): + """Yield (module, entry) pairs from Sphinx directives in an RST file.""" + doctree = publish_doctree( + rst_path.read_text(), + source_path=str(rst_path), + settings_overrides={ + "halt_level": 6, + "report_level": 5, + "warning_stream": io.StringIO(), + }, + ) + module = None + for node in doctree.findall(): + if isinstance(node, _ModuleNode): + module = node["module"] + elif isinstance(node, _AutosummaryNode): + for entry in node["entries"]: + yield module, entry + elif isinstance(node, _DataNode): + yield module, node["name"] + + +def _add_documented_name(documented, module, entry): + if not module or not module.startswith("cuda.core"): + return + if module == "cuda.core": + if "." not in entry: + documented[module].add(entry) + return + sub, name = entry.split(".", 1) + if sub in PUBLIC_SUBPACKAGES and "." not in name: + documented[f"cuda.core.{sub}"].add(name) + return + if module.startswith("cuda.core."): + namespace = module + if namespace in PUBLIC_NAMESPACES and "." not in entry: + documented[namespace].add(entry) + + +def _documented_names(docs_dir, *, exclude=frozenset()): + documented = collections.defaultdict(set) + for rst_path in docs_dir.glob("*.rst"): + if rst_path.name in exclude: + continue + for module, entry in _iter_documented_entries(rst_path): + _add_documented_name(documented, module, entry) + return documented + + +PUBLIC_NAMESPACES = ("cuda.core", *(f"cuda.core.{sub}" for sub in PUBLIC_SUBPACKAGES)) + + +@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.is_dir(): + pytest.skip("docs sources not available (not running from a source checkout)") + return DOCS_SOURCE_DIR + + +@pytest.fixture(scope="module") +def documented(docs_dir): + return _documented_names(docs_dir) + + +@pytest.mark.human_authored +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" + + +@pytest.mark.human_authored +def test_main_package_all_exports_resolve(): + assert hasattr(cuda.core, "__all__"), "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}" + + +@pytest.mark.human_authored +def test_main_package_symbols_are_documented(exported, documented): + documented = documented["cuda.core"] + undocumented = exported - documented + assert not undocumented, f"public by cuda.core.__all__ but missing from docs/source/*.rst: {sorted(undocumented)}" + + +@pytest.mark.parametrize("sub", PUBLIC_SUBPACKAGES) +def test_subpackage_symbols_define_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.human_authored +@pytest.mark.parametrize("sub", PUBLIC_SUBPACKAGES) +def test_subpackage_exports_are_documented(sub, documented): + documented = documented[f"cuda.core.{sub}"] + module = importlib.import_module(f"cuda.core.{sub}") + exported = set(module.__all__) + undocumented = exported - documented + assert not undocumented, ( + f"public by cuda.core.{sub}.__all__ but missing from docs/source/*.rst: {sorted(undocumented)}" + )