66
77Covers the flat ``cuda.core`` namespace and every public subpackage
88(``graph``, ``system``, ``texture``, ``utils``, and any added later) discovered
9- automatically from ``cuda.core.__path__``. For each, the exported ``__all__``
10- is compared against the public entries in ``docs/source/api.rst``. Symbols
11- documented in ``docs/source/api_private.rst`` (returned helpers users cannot
12- instantiate) are accepted as documented.
13-
14- Docs reference submodule symbols two ways: dotted entries such as
15- ``graph.Graph`` under the ``cuda.core`` module, and flat entries under a
16- ``.. currentmodule:: cuda.core.<sub>`` block. Both forms are collected. A
17- subpackage documented outside ``api.rst`` (for example ``system``, whose
18- reference lives in ``api_nvml.rst``) is still checked for a well-formed
19- ``__all__``, but its doc cross-check is skipped here.
9+ automatically from ``cuda.core.__path__``. For each public namespace, exported
10+ ``__all__`` names must appear somewhere in ``cuda_core/docs/source``.
2011"""
2112
13+ import collections
2214import importlib
15+ import io
2316import pathlib
2417import pkgutil
2518import re
2619
2720import pytest
21+ from docutils import nodes
22+ from docutils .core import publish_doctree
23+ from docutils .parsers .rst import Directive , directives
2824
2925import cuda .core
3026
4137)
4238
4339
44- def _iter_directive_blocks ( text ):
45- """Yield (module, directive, argument, body_lines) for each RST directive.
40+ class _ModuleNode ( nodes . Element ):
41+ pass
4642
47- ``module`` is the module active at the directive, tracked from
48- ``.. module::`` and ``.. currentmodule::`` directives. ``body_lines`` is
49- the indented block that follows the directive.
50- """
51- module = None
52- lines = text .splitlines ()
53- i = 0
54- while i < len (lines ):
55- line = lines [i ]
56- i += 1
57- if not line .startswith (".. " ):
58- continue
59- head , _ , argument = line [3 :].partition ("::" )
60- directive = head .strip ()
61- argument = argument .strip ()
62- if directive in ("module" , "currentmodule" ):
63- module = argument
64- continue
65- body = []
66- while i < len (lines ):
67- body_line = lines [i ]
68- if body_line .strip () and not body_line .startswith (" " ):
69- break
70- body .append (body_line )
71- i += 1
72- yield module , directive , argument , body
73-
74-
75- def _documented_names (rst_path , module = "cuda.core" ):
76- """Collect names documented for ``module`` in an RST file.
77-
78- Returns the entries of ``autosummary`` blocks plus ``.. data::``
79- arguments that appear while ``module`` is the active module.
80- """
81- names = set ()
82- for active_module , directive , argument , body in _iter_directive_blocks (rst_path .read_text ()):
83- if active_module != module :
84- continue
85- if directive == "data" :
86- names .add (argument )
87- elif directive == "autosummary" :
88- for line in body :
89- entry = line .strip ()
90- if not entry or entry .startswith (":" ):
91- continue
92- names .add (entry )
93- return names
43+
44+ class _AutosummaryNode (nodes .Element ):
45+ pass
9446
9547
96- def _flat_names (names ):
97- """Filter out dotted (submodule-namespace) entries."""
98- return {name for name in names if "." not in name }
48+ class _DataNode (nodes .Element ):
49+ pass
9950
10051
101- def _documented_subpackage_names (rst_path , sub ):
102- """Collect the public names documented for subpackage ``sub`` in ``rst_path``.
52+ class _ModuleDirective (Directive ):
53+ required_arguments = 1
54+ final_argument_whitespace = False
55+ has_content = True
56+ option_spec = {
57+ "deprecated" : directives .unchanged ,
58+ "no-index" : directives .flag ,
59+ "platform" : directives .unchanged ,
60+ "synopsis" : directives .unchanged ,
61+ }
62+
63+ def run (self ):
64+ node = _ModuleNode ()
65+ node ["module" ] = self .arguments [0 ].strip ()
66+ self .state .nested_parse (self .content , self .content_offset , node )
67+ return [node ]
68+
69+
70+ class _AutosummaryDirective (Directive ):
71+ has_content = True
72+ option_spec = {
73+ "caption" : directives .unchanged ,
74+ "nosignatures" : directives .flag ,
75+ "recursive" : directives .flag ,
76+ "template" : directives .unchanged ,
77+ "toctree" : directives .unchanged ,
78+ }
10379
104- Combines the two doc conventions: flat entries under
105- ``.. currentmodule:: cuda.core.<sub>`` and dotted ``<sub>.Name`` entries
106- written under the ``cuda.core`` module.
107- """
108- flat = _documented_names (rst_path , module = f"cuda.core.{ sub } " )
109- dotted = {
110- name .split ("." , 1 )[1 ]
111- for name in _documented_names (rst_path , module = "cuda.core" )
112- if name .startswith (f"{ sub } ." ) and name .count ("." ) == 1
80+ def run (self ):
81+ node = _AutosummaryNode ()
82+ node ["entries" ] = [entry for line in self .content if (entry := line .strip ()) and not entry .startswith (":" )]
83+ return [node ]
84+
85+
86+ class _DataDirective (Directive ):
87+ required_arguments = 1
88+ final_argument_whitespace = True
89+ has_content = True
90+ option_spec = {
91+ "annotation" : directives .unchanged ,
92+ "no-index" : directives .flag ,
93+ "type" : directives .unchanged ,
94+ "value" : directives .unchanged ,
11395 }
114- return flat | dotted
96+
97+ def run (self ):
98+ node = _DataNode ()
99+ node ["name" ] = self .arguments [0 ].strip ()
100+ return [node ]
101+
102+
103+ directives .register_directive ("autosummary" , _AutosummaryDirective )
104+ directives .register_directive ("currentmodule" , _ModuleDirective )
105+ directives .register_directive ("data" , _DataDirective )
106+ directives .register_directive ("module" , _ModuleDirective )
107+
108+
109+ def _iter_documented_entries (rst_path ):
110+ """Yield (module, entry) pairs from Sphinx directives in an RST file."""
111+ doctree = publish_doctree (
112+ rst_path .read_text (),
113+ source_path = str (rst_path ),
114+ settings_overrides = {
115+ "halt_level" : 6 ,
116+ "report_level" : 5 ,
117+ "warning_stream" : io .StringIO (),
118+ },
119+ )
120+ module = None
121+ for node in doctree .findall ():
122+ if isinstance (node , _ModuleNode ):
123+ module = node ["module" ]
124+ elif isinstance (node , _AutosummaryNode ):
125+ for entry in node ["entries" ]:
126+ yield module , entry
127+ elif isinstance (node , _DataNode ):
128+ yield module , node ["name" ]
129+
130+
131+ def _public_doc_paths (docs_dir ):
132+ return sorted (path for path in docs_dir .glob ("*.rst" ) if path .name != "api_private.rst" )
133+
134+
135+ def _add_documented_name (documented , module , entry ):
136+ if not module or not module .startswith ("cuda.core" ):
137+ return
138+ if module == "cuda.core" :
139+ if "." not in entry :
140+ documented [module ].add (entry )
141+ return
142+ sub , name = entry .split ("." , 1 )
143+ if sub in PUBLIC_SUBPACKAGES and "." not in name :
144+ documented [f"cuda.core.{ sub } " ].add (name )
145+ return
146+ if module .startswith ("cuda.core." ):
147+ namespace = module
148+ if namespace in PUBLIC_NAMESPACES and "." not in entry :
149+ documented [namespace ].add (entry )
150+
151+
152+ def _documented_names (paths ):
153+ documented = collections .defaultdict (set )
154+ for rst_path in paths :
155+ for module , entry in _iter_documented_entries (rst_path ):
156+ _add_documented_name (documented , module , entry )
157+ return documented
158+
159+
160+ def _private_documented_names (docs_dir ):
161+ names = collections .defaultdict (set )
162+ private_path = docs_dir / "api_private.rst"
163+ if not private_path .is_file ():
164+ return names
165+ for module , entry in _iter_documented_entries (private_path ):
166+ if module == "cuda.core" :
167+ if "." in entry :
168+ sub , name = entry .split ("." , 1 )
169+ if sub in PUBLIC_SUBPACKAGES :
170+ names [f"cuda.core.{ sub } " ].add (name .rsplit ("." , 1 )[- 1 ])
171+ else :
172+ names [module ].add (entry .rsplit ("." , 1 )[- 1 ])
173+ else :
174+ names [module ].add (entry )
175+ elif module in PUBLIC_NAMESPACES :
176+ names [module ].add (entry .rsplit ("." , 1 )[- 1 ])
177+ return names
178+
179+
180+ PUBLIC_NAMESPACES = ("cuda.core" , * (f"cuda.core.{ sub } " for sub in PUBLIC_SUBPACKAGES ))
115181
116182
117183@pytest .fixture (scope = "module" )
@@ -123,11 +189,21 @@ def exported():
123189
124190@pytest .fixture (scope = "module" )
125191def docs_dir ():
126- if not ( DOCS_SOURCE_DIR / "api.rst" ). is_file ():
192+ if not DOCS_SOURCE_DIR . is_dir ():
127193 pytest .skip ("docs sources not available (not running from a source checkout)" )
128194 return DOCS_SOURCE_DIR
129195
130196
197+ @pytest .fixture (scope = "module" )
198+ def public_documented (docs_dir ):
199+ return _documented_names (_public_doc_paths (docs_dir ))
200+
201+
202+ @pytest .fixture (scope = "module" )
203+ def private_documented (docs_dir ):
204+ return _private_documented_names (docs_dir )
205+
206+
131207def test_public_subpackages_discovered ():
132208 # Guards against a broken __path__ walk silently turning every
133209 # parametrized subpackage check into a no-op.
@@ -141,22 +217,20 @@ def test_all_exports_resolve():
141217 assert missing == [], f"cuda.core.__all__ lists names that do not resolve: { missing } "
142218
143219
144- def test_public_symbols_are_documented (exported , docs_dir ):
145- documented = _flat_names ( _documented_names ( docs_dir / "api.rst" ))
220+ def test_public_symbols_are_documented (exported , public_documented , private_documented ):
221+ documented = public_documented [ "cuda.core" ]
146222 # Returned helpers are deliberately documented in api_private.rst; accept
147- # them by their trailing name (e.g. _device_resources.DeviceResources).
148- private_documented = {name .rsplit ("." , 1 )[- 1 ] for name in _documented_names (docs_dir / "api_private.rst" )}
149- undocumented = exported - documented - private_documented
150- assert not undocumented , (
151- f"public by cuda.core.__all__ but missing from public docs (api.rst): { sorted (undocumented )} "
152- )
223+ # them by their trailing name.
224+ private = private_documented ["cuda.core" ]
225+ undocumented = exported - documented - private
226+ assert not undocumented , f"public by cuda.core.__all__ but missing from docs/source/*.rst: { sorted (undocumented )} "
153227
154228
155- def test_documented_symbols_are_exported (exported , docs_dir ):
156- documented = _flat_names ( _documented_names ( docs_dir / "api.rst" ))
229+ def test_documented_symbols_are_exported (exported , public_documented ):
230+ documented = public_documented [ "cuda.core" ]
157231 unexported = documented - exported
158232 assert not unexported , (
159- f"documented as public in api .rst but not exported by cuda.core.__all__: { sorted (unexported )} "
233+ f"documented as public in docs/source/* .rst but not exported by cuda.core.__all__: { sorted (unexported )} "
160234 )
161235
162236
@@ -169,18 +243,17 @@ def test_subpackage_defines_all(sub):
169243
170244
171245@pytest .mark .parametrize ("sub" , PUBLIC_SUBPACKAGES )
172- def test_subpackage_exports_match_docs (sub , docs_dir ):
173- documented = _documented_subpackage_names (docs_dir / "api.rst" , sub )
174- if not documented :
175- pytest .skip (f"cuda.core.{ sub } is not documented in api.rst (its reference lives elsewhere)" )
246+ def test_subpackage_exports_match_docs (sub , public_documented , private_documented ):
247+ documented = public_documented [f"cuda.core.{ sub } " ]
176248 module = importlib .import_module (f"cuda.core.{ sub } " )
177249 exported = set (module .__all__ )
178- private_documented = { name . rsplit ( "." , 1 )[ - 1 ] for name in _documented_names ( docs_dir / "api_private.rst" )}
179- undocumented = exported - documented - private_documented
250+ private = private_documented [ f"cuda.core. { sub } " ]
251+ undocumented = exported - documented - private
180252 assert not undocumented , (
181- f"public by cuda.core.{ sub } .__all__ but missing from public docs (api .rst) : { sorted (undocumented )} "
253+ f"public by cuda.core.{ sub } .__all__ but missing from docs/source/* .rst: { sorted (undocumented )} "
182254 )
183255 unexported = documented - exported
184256 assert not unexported , (
185- f"documented as public in api.rst under { sub } but not exported by cuda.core.{ sub } .__all__: { sorted (unexported )} "
257+ f"documented as public in docs/source/*.rst under { sub } but not exported by cuda.core.{ sub } .__all__: "
258+ f"{ sorted (unexported )} "
186259 )
0 commit comments