From 1fc21b2d22c35c4b3c14c8a4f05ce00427bcb915 Mon Sep 17 00:00:00 2001 From: Aryan Date: Sun, 12 Jul 2026 11:16:55 -0400 Subject: [PATCH 1/3] 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/3] 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)}" + ) From 75e719d42ea842e4c8ccc2bd3e172b471371dc09 Mon Sep 17 00:00:00 2001 From: Aryan Date: Tue, 14 Jul 2026 12:12:21 -0400 Subject: [PATCH 3/3] test(core): parse API docs with docutils --- cuda_core/pixi.toml | 1 + cuda_core/tests/test_api_docs_consistency.py | 263 ++++++++++++------- 2 files changed, 169 insertions(+), 95 deletions(-) diff --git a/cuda_core/pixi.toml b/cuda_core/pixi.toml index 8772ed4e88..30767983ba 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/tests/test_api_docs_consistency.py b/cuda_core/tests/test_api_docs_consistency.py index c4e0a0f7d5..c512e68ca0 100644 --- a/cuda_core/tests/test_api_docs_consistency.py +++ b/cuda_core/tests/test_api_docs_consistency.py @@ -6,25 +6,21 @@ 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. +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 @@ -41,77 +37,147 @@ ) -def _iter_directive_blocks(text): - """Yield (module, directive, argument, body_lines) for each RST directive. +class _ModuleNode(nodes.Element): + pass - ``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 + +class _AutosummaryNode(nodes.Element): + pass -def _flat_names(names): - """Filter out dotted (submodule-namespace) entries.""" - return {name for name in names if "." not in name} +class _DataNode(nodes.Element): + pass -def _documented_subpackage_names(rst_path, sub): - """Collect the public names documented for subpackage ``sub`` in ``rst_path``. +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, + } - 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 + 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, } - return flat | dotted + + def run(self): + node = _DataNode() + node["name"] = self.arguments[0].strip() + return [node] + + +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 _public_doc_paths(docs_dir): + return sorted(path for path in docs_dir.glob("*.rst") if path.name != "api_private.rst") + + +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(paths): + documented = collections.defaultdict(set) + for rst_path in paths: + for module, entry in _iter_documented_entries(rst_path): + _add_documented_name(documented, module, entry) + return documented + + +def _private_documented_names(docs_dir): + names = collections.defaultdict(set) + private_path = docs_dir / "api_private.rst" + if not private_path.is_file(): + return names + for module, entry in _iter_documented_entries(private_path): + if module == "cuda.core": + if "." in entry: + sub, name = entry.split(".", 1) + if sub in PUBLIC_SUBPACKAGES: + names[f"cuda.core.{sub}"].add(name.rsplit(".", 1)[-1]) + else: + names[module].add(entry.rsplit(".", 1)[-1]) + else: + names[module].add(entry) + elif module in PUBLIC_NAMESPACES: + names[module].add(entry.rsplit(".", 1)[-1]) + return names + + +PUBLIC_NAMESPACES = ("cuda.core", *(f"cuda.core.{sub}" for sub in PUBLIC_SUBPACKAGES)) @pytest.fixture(scope="module") @@ -123,11 +189,21 @@ def exported(): @pytest.fixture(scope="module") def docs_dir(): - if not (DOCS_SOURCE_DIR / "api.rst").is_file(): + 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 public_documented(docs_dir): + return _documented_names(_public_doc_paths(docs_dir)) + + +@pytest.fixture(scope="module") +def private_documented(docs_dir): + return _private_documented_names(docs_dir) + + def test_public_subpackages_discovered(): # Guards against a broken __path__ walk silently turning every # parametrized subpackage check into a no-op. @@ -141,22 +217,20 @@ def test_all_exports_resolve(): 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")) +def test_public_symbols_are_documented(exported, public_documented, private_documented): + documented = public_documented["cuda.core"] # 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)}" - ) + # them by their trailing name. + private = private_documented["cuda.core"] + undocumented = exported - documented - private + assert not undocumented, f"public by cuda.core.__all__ but missing from docs/source/*.rst: {sorted(undocumented)}" -def test_documented_symbols_are_exported(exported, docs_dir): - documented = _flat_names(_documented_names(docs_dir / "api.rst")) +def test_documented_symbols_are_exported(exported, public_documented): + documented = public_documented["cuda.core"] unexported = documented - exported assert not unexported, ( - f"documented as public in api.rst but not exported by cuda.core.__all__: {sorted(unexported)}" + f"documented as public in docs/source/*.rst but not exported by cuda.core.__all__: {sorted(unexported)}" ) @@ -169,18 +243,17 @@ def test_subpackage_defines_all(sub): @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)") +def test_subpackage_exports_match_docs(sub, public_documented, private_documented): + documented = public_documented[f"cuda.core.{sub}"] 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 + private = private_documented[f"cuda.core.{sub}"] + undocumented = exported - documented - private assert not undocumented, ( - f"public by cuda.core.{sub}.__all__ but missing from public docs (api.rst): {sorted(undocumented)}" + f"public by cuda.core.{sub}.__all__ but missing from docs/source/*.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)}" + f"documented as public in docs/source/*.rst under {sub} but not exported by cuda.core.{sub}.__all__: " + f"{sorted(unexported)}" )