Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions cuda_core/cuda/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions cuda_core/cuda/core/graph/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion cuda_core/cuda/core/system/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand All @@ -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)
8 changes: 8 additions & 0 deletions cuda_core/docs/source/api_nvml.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,11 @@ Types

Device
NvlinkInfo

Constants
---------

.. autosummary::
:toctree: generated/

CUDA_BINDINGS_NVML_IS_COMPATIBLE
35 changes: 34 additions & 1 deletion cuda_core/docs/source/api_private.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions cuda_core/pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pytest-randomly = "*"
pytest-repeat = "*"
pytest-rerunfailures = "*"
cloudpickle = "*"
docutils = "*"
psutil = "*"
pyglet = "*"

Expand Down
1 change: 1 addition & 0 deletions cuda_core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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'",
]
Expand Down
223 changes: 223 additions & 0 deletions cuda_core/tests/test_api_docs_consistency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# 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``.
"""

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)}"
)
Loading