|
| 1 | +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| 2 | +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE |
| 3 | + |
| 4 | +"""Internal support for error-enum explanations. |
| 5 | +
|
| 6 | +``cuda_core`` keeps frozen 13.1.1 fallback tables for older ``cuda-bindings`` |
| 7 | +releases. Driver/runtime error enums carry usable ``__doc__`` text starting in |
| 8 | +the 12.x backport line at ``cuda-bindings`` 12.9.6, and in the mainline 13.x |
| 9 | +series at ``cuda-bindings`` 13.2.0. This module decides which source to use |
| 10 | +and normalizes generated docstrings so user-facing ``CUDAError`` messages stay |
| 11 | +presentable. |
| 12 | +
|
| 13 | +The cleanup rules here were derived while validating generated enum docstrings |
| 14 | +in PR #1805. Keep them narrow and remove them when codegen quirks or fallback |
| 15 | +support are no longer needed. |
| 16 | +""" |
| 17 | + |
| 18 | +from __future__ import annotations |
| 19 | + |
| 20 | +import importlib.metadata |
| 21 | +import re |
| 22 | +from collections.abc import Callable |
| 23 | +from typing import Any |
| 24 | + |
| 25 | +_MIN_12X_BINDING_VERSION_FOR_ENUM_DOCSTRINGS = (12, 9, 6) |
| 26 | +_MIN_13X_BINDING_VERSION_FOR_ENUM_DOCSTRINGS = (13, 2, 0) |
| 27 | +_RST_INLINE_ROLE_RE = re.compile(r":(?:[a-z]+:)?[a-z]+:`([^`]+)`") |
| 28 | +_WORDWRAP_HYPHEN_AFTER_RE = re.compile(r"(?<=[0-9A-Za-z_])- (?=[0-9A-Za-z_])") |
| 29 | +_WORDWRAP_HYPHEN_BEFORE_RE = re.compile(r"(?<=[0-9A-Za-z_]) -(?=[0-9A-Za-z_])") |
| 30 | +_ExplanationTable = dict[int, str | tuple[str, ...]] |
| 31 | +_ExplanationTableLoader = Callable[[], _ExplanationTable] |
| 32 | + |
| 33 | + |
| 34 | +# ``version.pyx`` cannot be reused here (circular import via ``cuda_utils``). |
| 35 | +def _binding_version() -> tuple[int, int, int]: |
| 36 | + """Return the installed ``cuda-bindings`` version, or a conservative old value.""" |
| 37 | + try: |
| 38 | + parts = importlib.metadata.version("cuda-bindings").split(".")[:3] |
| 39 | + except importlib.metadata.PackageNotFoundError: |
| 40 | + return (0, 0, 0) # For very old versions of cuda-python |
| 41 | + return tuple(int(v) for v in parts) |
| 42 | + |
| 43 | + |
| 44 | +def _binding_version_has_usable_enum_docstrings(version: tuple[int, int, int]) -> bool: |
| 45 | + """Whether released bindings are known to carry usable error-enum ``__doc__`` text.""" |
| 46 | + return ( |
| 47 | + _MIN_12X_BINDING_VERSION_FOR_ENUM_DOCSTRINGS <= version < (13, 0, 0) |
| 48 | + or version >= _MIN_13X_BINDING_VERSION_FOR_ENUM_DOCSTRINGS |
| 49 | + ) |
| 50 | + |
| 51 | + |
| 52 | +def _fix_hyphenation_wordwrap_spacing(s: str) -> str: |
| 53 | + """Remove spaces around hyphens introduced by line wrapping in generated ``__doc__`` text. |
| 54 | +
|
| 55 | + This targets asymmetric wrap artifacts such as ``non- linear`` or |
| 56 | + ``GPU- Direct`` while leaving intentional ``a - b`` separators alone. |
| 57 | + """ |
| 58 | + prev = None |
| 59 | + while prev != s: |
| 60 | + prev = s |
| 61 | + s = _WORDWRAP_HYPHEN_AFTER_RE.sub("-", s) |
| 62 | + s = _WORDWRAP_HYPHEN_BEFORE_RE.sub("-", s) |
| 63 | + return s |
| 64 | + |
| 65 | + |
| 66 | +def clean_enum_member_docstring(doc: str | None) -> str | None: |
| 67 | + """Turn an enum member ``__doc__`` into plain text. |
| 68 | +
|
| 69 | + The generated enum docstrings are already close to user-facing prose, but |
| 70 | + they may contain Sphinx inline roles, line wrapping, or a small known |
| 71 | + codegen defect. Normalize only those differences so the text is suitable |
| 72 | + for error messages. |
| 73 | + """ |
| 74 | + if doc is None: |
| 75 | + return None |
| 76 | + s = doc |
| 77 | + # Known codegen bug on cudaErrorIncompatibleDriverContext. Remove once fixed |
| 78 | + # in cuda-bindings code generation. |
| 79 | + s = s.replace("\n:py:obj:`~.Interactions`", ' "Interactions ') |
| 80 | + # Drop a leading "~." or "." after removing the surrounding RST inline role. |
| 81 | + s = _RST_INLINE_ROLE_RE.sub(lambda m: re.sub(r"^~?\.", "", m.group(1)), s) |
| 82 | + # Strip simple bold emphasis markers. |
| 83 | + s = re.sub(r"\*\*([^*]+)\*\*", r"\1", s) |
| 84 | + # Strip simple italic emphasis markers. |
| 85 | + s = re.sub(r"\*([^*]+)\*", r"\1", s) |
| 86 | + # Collapse wrapped lines and repeated spaces. |
| 87 | + s = re.sub(r"\s+", " ", s).strip() |
| 88 | + s = _fix_hyphenation_wordwrap_spacing(s) |
| 89 | + return s |
| 90 | + |
| 91 | + |
| 92 | +class DocstringBackedExplanations: |
| 93 | + """Compatibility shim exposing enum-member ``__doc__`` text via ``dict.get``. |
| 94 | +
|
| 95 | + Keeps the existing ``.get(int(error))`` lookup shape used by ``cuda_utils.pyx``. |
| 96 | + """ |
| 97 | + |
| 98 | + __slots__ = ("_enum_type",) |
| 99 | + |
| 100 | + def __init__(self, enum_type: Any) -> None: |
| 101 | + self._enum_type = enum_type |
| 102 | + |
| 103 | + def get(self, code: int, default: str | None = None) -> str | None: |
| 104 | + try: |
| 105 | + member = self._enum_type(code) |
| 106 | + except ValueError: |
| 107 | + return default |
| 108 | + |
| 109 | + raw_doc = member.__doc__ |
| 110 | + if raw_doc is None: |
| 111 | + return default |
| 112 | + |
| 113 | + return clean_enum_member_docstring(raw_doc) |
| 114 | + |
| 115 | + |
| 116 | +def get_best_available_explanations( |
| 117 | + enum_type: Any, |
| 118 | + fallback: _ExplanationTable | _ExplanationTableLoader, |
| 119 | +) -> DocstringBackedExplanations | _ExplanationTable: |
| 120 | + """Pick one explanation source per bindings version. |
| 121 | +
|
| 122 | + Use enum-member ``__doc__`` only for bindings versions known to expose |
| 123 | + usable per-member text (12.9.6+ in the 12.x backport line, 13.2.0+ in the |
| 124 | + 13.x mainline). Otherwise keep using the frozen 13.1.1 fallback tables. |
| 125 | + """ |
| 126 | + if not _binding_version_has_usable_enum_docstrings(_binding_version()): |
| 127 | + if callable(fallback): |
| 128 | + return fallback() |
| 129 | + return fallback |
| 130 | + return DocstringBackedExplanations(enum_type) |
0 commit comments