Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
85d9e66
Copy enum explanation dicts and health-check tests to cuda_bindings (…
rwgk Mar 23, 2026
f35670a
Remove enum explanation health tests from cuda_core (#1712)
rwgk Mar 23, 2026
d6b3e78
cuda_core: prefer enum explanations from cuda.bindings, keep local fa…
rwgk Mar 23, 2026
b54ec83
Add _CTK_MAJOR_MINOR_PATCH version guards and DRY up explanation test…
rwgk Mar 23, 2026
3f6c30f
Clean up test code: parametrize bindings health tests, drop no-op f-s…
rwgk Mar 23, 2026
b1645c0
Update pathfinder descriptor catalogs for cusparseLt release 0.9.0
rwgk Mar 23, 2026
fb12195
Merge branch 'main' into move_enum_explanations
rwgk Mar 23, 2026
47e9c2d
Merge branch 'main' into move_enum_explanations
rwgk Mar 24, 2026
89a2052
Merge branch 'main' into move_enum_explanations
rwgk Mar 25, 2026
6fc77b7
Delete cuda_core fallback explanation copies, import from cuda.bindin…
rwgk Mar 26, 2026
112fb41
Merge branch 'main' into move_enum_explanations
rwgk Mar 26, 2026
914ffd6
Revert to original dict names and drop _CTK_MAJOR_MINOR_PATCH (#1712)
rwgk Mar 26, 2026
07d522d
Merge branch 'main' into move_enum_explanations
rwgk Mar 31, 2026
76efdf6
cuda_bindings: compare explanation dicts to CUresult/cudaError_t __doc__
rwgk Mar 31, 2026
86e9488
cuda_bindings: add clean_enum_member_docstring helper in enum explana…
rwgk Mar 31, 2026
f4094c5
cuda_bindings: compare explanation dicts to cleaned enum __doc__ in h…
rwgk Mar 31, 2026
4adec22
cuda_bindings: improve dict/__doc__ parity test (Doxygen :: strip, de…
rwgk Mar 31, 2026
1ce9bc8
cuda_bindings: collapse wrapped hyphen spaces in clean_enum_member_do…
rwgk Mar 31, 2026
cbebba0
cuda_bindings: fix Interactions __doc__ workaround and CUDART_DRIVER …
rwgk Mar 31, 2026
ba2f65a
cuda_bindings: skip cudaErrorLaunchTimeout in dict vs cleaned __doc__…
rwgk Mar 31, 2026
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
2 changes: 2 additions & 0 deletions cuda_bindings/cuda/bindings/_utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE
258 changes: 258 additions & 0 deletions cuda_bindings/tests/test_enum_explanations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE

import importlib
import importlib.metadata
import re

import pytest

from cuda.bindings import driver, runtime

_EXPLANATION_MODULES = [
("driver_cu_result_explanations", "DRIVER_CU_RESULT_EXPLANATIONS", driver.CUresult),
("runtime_cuda_error_explanations", "RUNTIME_CUDA_ERROR_EXPLANATIONS", runtime.cudaError_t),
]

# Explanation dicts are maintained for the same toolkit as cuda-bindings; enum members
# carry docstrings from code generation (reportedly aligned since cuda-bindings 13.2.0).
_MIN_BINDING_VERSION_FOR_DOCSTRING_COMPARE = (13, 2)


def _get_binding_version():
try:
major_minor = importlib.metadata.version("cuda-bindings").split(".")[:2]
except importlib.metadata.PackageNotFoundError:
major_minor = importlib.metadata.version("cuda-python").split(".")[:2]
return tuple(int(v) for v in major_minor)


def _explanation_text_from_dict_value(value):
"""Flatten a dict entry to a single str (entries are str or tuple of str fragments)."""
if isinstance(value, tuple):
return "".join(value)
return value


def _strip_doxygen_double_colon_prefixes(s: str) -> str:
"""Remove Doxygen-style ``::`` before CUDA identifiers in header-comment text.

Matches ``::`` only when it *starts* a reference (not C++ scope between two names):
use a negative lookbehind so ``Foo::Bar`` keeps the inner ``::``.

Applied repeatedly so ``::a ::b`` becomes ``a b``.
"""
prev = None
while prev != s:
prev = s
s = re.sub(r"(?<![A-Za-z0-9_])::+([A-Za-z_][A-Za-z0-9_]*)", r"\1", s)
return s


def _explanation_dict_text_for_cleaned_doc_compare(value) -> str:
"""Normalize hand-maintained dict text to compare with ``clean_enum_member_docstring`` output.

Dicts use Doxygen ``::Symbol`` for APIs, types, and constants; cleaned enum ``__doc__``
uses plain names after Sphinx role stripping. Strip those ``::`` prefixes on the fly,
then collapse whitespace like ``clean_enum_member_docstring``.
"""
s = _explanation_text_from_dict_value(value)
s = _strip_doxygen_double_colon_prefixes(s)
s = re.sub(r"\s+", " ", s).strip()
s = _fix_hyphenation_wordwrap_spacing(s)
# Manpage token only in dict text; cleaned __doc__ cites the section title alone.
s = s.replace("CUDART_DRIVER ", "")
return s


def _fix_hyphenation_wordwrap_spacing(s: str) -> str:
"""Remove spaces around hyphens introduced by line wrapping in generated ``__doc__`` text.

Sphinx/reflow often splits hyphenated words as ``non- linear`` or ``word -word``.
The explanation dicts are usually single-line and do not contain these splits; the
mismatch shows up on the cleaned enum side, so this runs inside
``clean_enum_member_docstring`` (and the same transform is applied to dict text for
comparison parity).

Patterns (all lowercase ASCII letters as in the CUDA blurbs): ``[a-z]- [a-z]`` and
``[a-z] -[a-z]``. Applied repeatedly until stable.
"""
prev = None
while prev != s:
prev = s
s = re.sub(r"([a-z])- ([a-z])", r"\1-\2", s)
s = re.sub(r"([a-z]) -([a-z])", r"\1-\2", s)
return s


def clean_enum_member_docstring(doc: str | None) -> str | None:
"""Turn a FastEnum member ``__doc__`` into plain text for display or fallback logic.

Always: collapse all whitespace (including newlines) to single spaces and strip ends.

Best-effort: remove common Sphinx/reST inline markup seen in generated CUDA docs,
e.g. ``:py:obj:`~.cudaGetLastError()` `` -> ``cudaGetLastError()`` (relative ``~.`` is
dropped). Does not aim for perfect reST parsing—only patterns that appear on these
enums in practice.

After whitespace collapse, removes spurious spaces around hyphens from line wrapping
(``[a-z]- [a-z]`` and ``[a-z] -[a-z]``) so ``non- linear`` matches dict ``non-linear``.

Returns ``None`` if ``doc`` is ``None``; otherwise returns a non-empty or empty str.
"""
if doc is None:
return None
s = doc
# Work around codegen bug (cudaErrorIncompatibleDriverContext):
# malformed :py:obj before `with`. Please remove after fix.
s = s.replace("\n:py:obj:`~.Interactions`", ' "Interactions ')
# Sphinx roles with a single backtick-delimited target (most common on these enums).
# Strip the role and keep the inner text; drop leading ~. used for same-module refs.
s = re.sub(
r":(?:py:)?(?:obj|func|meth|class|mod|data|const|exc):`([^`]+)`",
lambda m: re.sub(r"^~?\.", "", m.group(1)),
s,
)
# Inline emphasis / strong (rare in error blurbs)
s = re.sub(r"\*\*([^*]+)\*\*", r"\1", s)
s = re.sub(r"\*([^*]+)\*", r"\1", s)
# Collapse whitespace (newlines -> spaces) and trim
s = re.sub(r"\s+", " ", s).strip()
s = _fix_hyphenation_wordwrap_spacing(s)
return s


@pytest.mark.parametrize(
("raw", "expected"),
[
pytest.param("a\nb c", "a b c", id="collapse_whitespace"),
pytest.param(" x \n ", "x", id="strip_padding"),
pytest.param(
"see\n:py:obj:`~.cuInit()` or :py:obj:`cuCtxDestroy()`",
"see cuInit() or cuCtxDestroy()",
id="sphinx_py_obj_roles",
),
pytest.param(
"x :py:func:`~.cudaMalloc()` y",
"x cudaMalloc() y",
id="sphinx_py_func_role",
),
pytest.param("**Note:** text", "Note: text", id="strip_bold"),
pytest.param("[Deprecated]\n", "[Deprecated]", id="deprecated_line"),
pytest.param("non- linear", "non-linear", id="hyphen_space_after"),
pytest.param("word -word", "word-word", id="hyphen_space_before"),
pytest.param(
'Please see\n:py:obj:`~.Interactions`with the CUDA Driver API" for more information.',
'Please see "Interactions with the CUDA Driver API" for more information.',
id="codegen_broken_interactions_role",
),
],
)
def test_clean_enum_member_docstring_examples(raw, expected):
assert clean_enum_member_docstring(raw) == expected


def test_clean_enum_member_docstring_none_input():
assert clean_enum_member_docstring(None) is None


@pytest.mark.parametrize(
("raw", "expected"),
[
pytest.param("see ::CUDA_SUCCESS", "see CUDA_SUCCESS", id="type_ref"),
pytest.param("Foo::Bar unchanged", "Foo::Bar unchanged", id="cpp_scope_preserved"),
pytest.param("::cuInit() and ::CUstream", "cuInit() and CUstream", id="multiple_prefixes"),
],
)
def test_strip_doxygen_double_colon_prefixes(raw, expected):
assert _strip_doxygen_double_colon_prefixes(raw) == expected


def _enum_docstring_parity_cases():
for module_name, dict_name, enum_type in _EXPLANATION_MODULES:
for error in enum_type:
yield pytest.param(
module_name,
dict_name,
error,
id=f"{enum_type.__name__}.{error.name}",
)


@pytest.mark.xfail(
reason=(
"Some members still differ after clean_enum_member_docstring and dict-side "
"::/whitespace alignment (wording drift, etc.). [Deprecated] stubs are skipped. "
"Remove xfail when dicts and generated docstrings share one source."
),
strict=False,
)
@pytest.mark.parametrize(
"module_name,dict_name,error",
list(_enum_docstring_parity_cases()),
)
def test_explanations_dict_matches_cleaned_enum_docstrings(module_name, dict_name, error):
"""Hand-maintained explanation dict entries should match cleaned enum ``__doc__`` text.

cuda-bindings 13.2+ attaches per-member documentation on driver ``CUresult`` and
runtime ``cudaError_t``. This compares ``clean_enum_member_docstring(member.__doc__)``
to dict text normalized with ``_explanation_dict_text_for_cleaned_doc_compare`` (same
whitespace rules; strip Doxygen ``::`` before ``name(`` to align with Sphinx output).

Members whose ``__doc__`` is the ``[Deprecated]`` stub alone, or ends with
``[Deprecated]`` after stripping whitespace, are skipped (dicts may keep longer
text; we do not compare those).

``cudaErrorLaunchTimeout`` is skipped: cleaned ``__doc__`` is considered authoritative;
the hand dict still carries a Doxygen-style ``cudaDeviceAttr::…`` fragment that we do
not normalize to match.

Marked xfail while any non-skipped member still mismatches; many cases already match
(reported as xpassed when this mark is present).
"""
if _get_binding_version() < _MIN_BINDING_VERSION_FOR_DOCSTRING_COMPARE:
pytest.skip(
"Enum __doc__ vs explanation dict compare is only run for "
f"cuda-bindings >= {_MIN_BINDING_VERSION_FOR_DOCSTRING_COMPARE[0]}.{_MIN_BINDING_VERSION_FOR_DOCSTRING_COMPARE[1]}"
)

if error is runtime.cudaError_t.cudaErrorLaunchTimeout:
pytest.skip("Known good __doc__, bad explanations dict value")

mod = importlib.import_module(f"cuda.bindings._utils.{module_name}")
expl_dict = getattr(mod, dict_name)

code = int(error)
assert code in expl_dict

raw_doc = error.__doc__
if raw_doc is not None and raw_doc.strip().endswith("[Deprecated]"):
pytest.skip(f"SKIPPED: {error.name} is deprecated (__doc__ is or ends with [Deprecated])")

if raw_doc is None:
pytest.skip(f"SKIPPED: {error.name} has no __doc__")

expected = _explanation_dict_text_for_cleaned_doc_compare(expl_dict[code])
actual = clean_enum_member_docstring(raw_doc)
if expected != actual:
pytest.fail(
f"normalized dict != cleaned __doc__ for {error!r}:\n"
f" dict (normalized for compare): {expected!r}\n"
f" cleaned __doc__: {actual!r}"
)


@pytest.mark.parametrize("module_name,dict_name,enum_type", _EXPLANATION_MODULES)
def test_explanations_health(module_name, dict_name, enum_type):
mod = importlib.import_module(f"cuda.bindings._utils.{module_name}")
expl_dict = getattr(mod, dict_name)

known_codes = set()
for error in enum_type:
code = int(error)
assert code in expl_dict
known_codes.add(code)

if _get_binding_version() >= (13, 0):
extra_expl = sorted(set(expl_dict.keys()) - known_codes)
assert not extra_expl
11 changes: 9 additions & 2 deletions cuda_core/cuda/core/_utils/cuda_utils.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,15 @@ from cpython.buffer cimport PyObject_GetBuffer, PyBuffer_Release, Py_buffer, PyB

from cuda.bindings cimport cynvrtc, cynvvm, cynvjitlink

from cuda.core._utils.driver_cu_result_explanations import DRIVER_CU_RESULT_EXPLANATIONS
from cuda.core._utils.runtime_cuda_error_explanations import RUNTIME_CUDA_ERROR_EXPLANATIONS
try:
from cuda.bindings._utils.driver_cu_result_explanations import DRIVER_CU_RESULT_EXPLANATIONS
except ModuleNotFoundError:
DRIVER_CU_RESULT_EXPLANATIONS = {}

try:
from cuda.bindings._utils.runtime_cuda_error_explanations import RUNTIME_CUDA_ERROR_EXPLANATIONS
except ModuleNotFoundError:
RUNTIME_CUDA_ERROR_EXPLANATIONS = {}


class CUDAError(Exception):
Expand Down
36 changes: 0 additions & 36 deletions cuda_core/tests/test_cuda_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,42 +11,6 @@
from cuda.core._utils.clear_error_support import assert_type_str_or_bytes_like, raise_code_path_meant_to_be_unreachable


def test_driver_cu_result_explanations_health():
expl_dict = cuda_utils.DRIVER_CU_RESULT_EXPLANATIONS

# Ensure all CUresult enums are in expl_dict
known_codes = set()
for error in driver.CUresult:
code = int(error)
assert code in expl_dict
known_codes.add(code)

from cuda.core._utils.version import binding_version

if binding_version() >= (13, 0, 0):
# Ensure expl_dict has no codes not known as a CUresult enum
extra_expl = sorted(set(expl_dict.keys()) - known_codes)
assert not extra_expl


def test_runtime_cuda_error_explanations_health():
expl_dict = cuda_utils.RUNTIME_CUDA_ERROR_EXPLANATIONS

# Ensure all cudaError_t enums are in expl_dict
known_codes = set()
for error in runtime.cudaError_t:
code = int(error)
assert code in expl_dict
known_codes.add(code)

from cuda.core._utils.version import binding_version

if binding_version() >= (13, 0, 0):
# Ensure expl_dict has no codes not known as a cudaError_t enum
extra_expl = sorted(set(expl_dict.keys()) - known_codes)
assert not extra_expl


def test_check_driver_error():
num_unexpected = 0
for error in driver.CUresult:
Expand Down