Skip to content

Commit dd078ae

Browse files
committed
add unit test for sphinx documentation
1 parent 624439a commit dd078ae

1 file changed

Lines changed: 138 additions & 0 deletions

File tree

doc/test_sphinxdocs.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# test_sphinxdocs.py - pytest checks for user guide
2+
# RMM, 23 Dec 2024
3+
#
4+
# This set of tests is used to make sure that all primary functions are
5+
# referenced in the documentation.
6+
7+
import inspect
8+
import os
9+
import sys
10+
import warnings
11+
from importlib import resources
12+
13+
import pytest
14+
import numpydoc.docscrape as npd
15+
16+
import control
17+
import control.flatsys
18+
19+
# Location of the documentation and files to check
20+
sphinx_dir = str(resources.files('control')) + '/../doc/generated/'
21+
22+
# Functions that should not be referenced
23+
legacy_functions = [
24+
'balred', # balanced_reduction
25+
'bode', # bode_plot
26+
'c2d', # sample_system
27+
'era', # eigensys_realization
28+
'evalfr', # use __call__()
29+
'find_eqpt', # find_operating_point
30+
'gangof4', # gangof4_plot
31+
'hsvd', # hankel_singular_values
32+
'minreal', # minimal_realization
33+
'modred', # model_reduction
34+
'nichols', # nichols_plot
35+
'nyquist', # nyquist_plot
36+
'pzmap', # pole_zero_plot
37+
'rlocus', # root_locus_plot
38+
'rlocus', # root_locus_plot
39+
'root_locus', # root_locus_plot
40+
]
41+
42+
# Functons that we can skip
43+
object_skiplist = [
44+
control.NamedSignal, # users don't need to know about this
45+
control.common_timebase, # mainly internal use
46+
control.cvxopt_check, # mainly internal use
47+
control.pandas_check, # mainly internal use
48+
control.slycot_check, # mainly internal use
49+
]
50+
51+
# Global list of objects we have checked
52+
checked = set()
53+
54+
# Decide on the level of verbosity (use -rP when running pytest)
55+
verbose = 0
56+
standalone = False
57+
58+
control_module_list = [
59+
control, control.flatsys, control.optimal, control.phaseplot
60+
]
61+
@pytest.mark.parametrize("module", control_module_list)
62+
def test_sphinx_functions(module):
63+
64+
# Look through every object in the package
65+
_info(f"Checking module {module}", 1)
66+
67+
for name, obj in inspect.getmembers(module):
68+
objname = ".".join([module.__name__, name])
69+
70+
# Skip anything that is outside of this module
71+
if inspect.getmodule(obj) is not None and \
72+
not inspect.getmodule(obj).__name__.startswith('control'):
73+
# Skip anything that isn't part of the control package
74+
continue
75+
76+
elif inspect.isclass(obj) and issubclass(obj, Exception):
77+
continue
78+
79+
elif inspect.isclass(obj) or inspect.isfunction(obj):
80+
# Skip anything that is inherited, hidden, deprecated, or checked
81+
if inspect.isclass(module) and name not in module.__dict__ \
82+
or name.startswith('_') or obj in checked:
83+
continue
84+
else:
85+
checked.add(obj)
86+
87+
# Get the relevant information about this object
88+
exists = os.path.exists(sphinx_dir + objname + ".rst")
89+
deprecated = _check_deprecated(obj)
90+
skip = obj in object_skiplist
91+
referenced = f" {objname} referenced in sphinx docs"
92+
legacy = name in legacy_functions
93+
94+
_info(f" Checking {objname}", 2)
95+
match exists, skip, deprecated, legacy:
96+
case True, True, _, _:
97+
_info(f"skipped object" + referenced, -1)
98+
case True, _, True, _:
99+
_warn(f"deprecated object" + referenced)
100+
case True, _, _, True:
101+
_warn(f"legacy object" + referenced)
102+
case False, False, False, False:
103+
_fail(f"{objname} not referenced in sphinx docs")
104+
105+
106+
def _check_deprecated(obj):
107+
with warnings.catch_warnings():
108+
warnings.simplefilter('ignore') # debug via sphinx, not here
109+
doc = npd.FunctionDoc(obj)
110+
111+
doc_extended = "" if doc is None else "\n".join(doc["Extended Summary"])
112+
return ".. deprecated::" in doc_extended
113+
114+
115+
# Utility function to warn with verbose output
116+
def _info(str, level):
117+
if verbose > level:
118+
print(("INFO: " if level < 0 else " " * level) + str)
119+
120+
def _warn(str, level=-1):
121+
if verbose > level:
122+
print("WARN: " + " " * level + str)
123+
if not standalone:
124+
warnings.warn(str, stacklevel=2)
125+
126+
def _fail(str, level=-1):
127+
if verbose > level:
128+
print("FAIL: " + " " * level + str)
129+
if not standalone:
130+
pytest.fail(str)
131+
132+
133+
if __name__ == "__main__":
134+
verbose = 0 if len(sys.argv) == 1 else int(sys.argv[1])
135+
standalone = True
136+
137+
for module in control_module_list:
138+
test_sphinx_functions(module)

0 commit comments

Comments
 (0)