-
Notifications
You must be signed in to change notification settings - Fork 321
test(core): add cuda.core.__all__ vs public docs consistency check #2347
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
1fc21b2
37cd68f
75e719d
f38051c
7b1be3e
1438c20
39f9843
c09dd0c
41003b8
5a4a178
b69022b
123ab66
c6429f0
30bf6fc
93e6d48
565479b
f768d40
7e7c512
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
| """ | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Document what we're intentionally not checking for here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I expanded the module docstring to spell out the intentional scope limits: the check is one-way (public export -> documented) and does not enforce the reverse, since documenting a private or internal symbol on any page is allowed. It is also a name-presence check only, so it does not verify signatures, docstrings, or rendered output, treats |
||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This sort of string parsing can become very brittle to slightly different uses. I think it would be worth experimenting with using the For example, my agent found these bugs with this. Using the upstream ==== _iter_directive_blocks swallows the body of .. module:: / .. currentmodule:: directives silently. When those directives are encountered the function does continue and skips the body scan. If the RST has :synopsis: or other options under .. module:: cuda.core, the outer loop's next iteration sees those indented lines as ordinary text (they don't start with .. ) and skips them — which is the right behaviour here. However, if someone puts a body directive immediately below .. module:: (no blank line separator), the first non-blank indented line will be silently dropped from the outer loop. Not a current issue but fragile. |
||
|
|
||
|
|
||
| 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)}" | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should be included (as well as
cuda.core.system).Any
__init__.pyinside of a public (non_-prefixed) submodule is public API. Currently,graph,system,textureandutils, though more may be added in the future so the test should adapt automatically.Today, only
systemhas its own separate API.rstpage (which is fine). I would argue the other submodules should have their own.rstpage as well. (And we could do that refactor here).