Skip to content

Commit 3697afa

Browse files
committed
improve docstrings unit test for deprecation, seq args; skip duplicates
1 parent b767e52 commit 3697afa

File tree

1 file changed

+111
-24
lines changed

1 file changed

+111
-24
lines changed

control/tests/docstrings_test.py

+111-24
Original file line numberDiff line numberDiff line change
@@ -17,71 +17,158 @@
1717
import control.flatsys
1818

1919
# List of functions that we can skip testing (special cases)
20-
skiplist = [
20+
function_skiplist = [
2121
control.ControlPlot.reshape, # needed for legacy interface
22+
control.phase_plot, # legacy function
2223
]
2324

25+
# List of keywords that we can skip testing (special cases)
26+
keyword_skiplist = {
27+
control.input_output_response: ['method'],
28+
control.nyquist_plot: ['color'], # checked separately
29+
control.optimal.solve_ocp: ['method'], # deprecated
30+
control.sisotool: ['kvect'], # deprecated
31+
}
32+
33+
# Decide on the level of verbosity (use -rP when running pytest)
34+
verbose = 1
35+
2436
@pytest.mark.parametrize("module, prefix", [
2537
(control, ""), (control.flatsys, "flatsys."),
2638
(control.optimal, "optimal."), (control.phaseplot, "phaseplot.")
2739
])
2840
def test_docstrings(module, prefix):
2941
# Look through every object in the package
30-
print(f"Checking module {module}")
42+
if verbose > 1:
43+
print(f"Checking module {module}")
3144
for name, obj in inspect.getmembers(module):
3245
# Skip anything that is outside of this module
33-
if inspect.getmodule(obj) is not None and \
34-
not inspect.getmodule(obj).__name__.startswith('control'):
46+
if inspect.getmodule(obj) is not None and (
47+
not inspect.getmodule(obj).__name__.startswith('control')
48+
or prefix != "" and inspect.getmodule(obj) != module):
3549
# Skip anything that isn't part of the control package
3650
continue
3751

3852
if inspect.isclass(obj):
39-
print(f" Checking class {name}")
53+
if verbose > 1:
54+
print(f" Checking class {name}")
4055
# Check member functions within the class
41-
test_docstrings(obj, prefix + obj.__name__ + '.')
56+
test_docstrings(obj, prefix + name + '.')
4257

4358
if inspect.isfunction(obj):
44-
# Skip anything that is inherited or hidden
45-
if inspect.isclass(module) and obj.__name__ not in module.__dict__ \
46-
or obj.__name__.startswith('_') or obj in skiplist:
59+
# Skip anything that is inherited, hidden, or deprecated
60+
if inspect.isclass(module) and name not in module.__dict__ \
61+
or name.startswith('_') or obj in function_skiplist:
4762
continue
4863

49-
# Make sure there is a docstring
50-
print(f" Checking function {name}")
64+
# Get the docstring (skip w/ warning if there isn't one)
65+
if verbose > 1:
66+
print(f" Checking function {name}")
5167
if obj.__doc__ is None:
5268
warnings.warn(
53-
f"{module.__name__}.{obj.__name__} is missing docstring")
69+
f"{module.__name__}.{name} is missing docstring")
70+
continue
71+
else:
72+
docstring = inspect.getdoc(obj)
73+
source = inspect.getsource(obj)
74+
75+
# Skip deprecated functions
76+
if f"{name} is deprecated" in docstring or \
77+
"function is deprecated" in docstring or \
78+
".. deprecated::" in docstring:
79+
if verbose > 1:
80+
print(" [deprecated]")
5481
continue
55-
82+
83+
elif f"{name} is deprecated" in source:
84+
if verbose:
85+
print(f" {name} is deprecated, but not documented")
86+
warnings.warn(f"{name} deprecated, but not documented")
87+
continue
88+
5689
# Get the signature for the function
5790
sig = inspect.signature(obj)
5891

5992
# Go through each parameter and make sure it is in the docstring
6093
for argname, par in sig.parameters.items():
61-
if argname == 'self' or argname[0] == '_':
94+
95+
# Look for arguments that we can skip
96+
if argname == 'self' or argname[0] == '_' or \
97+
obj in keyword_skiplist and argname in keyword_skiplist[obj]:
6298
continue
63-
64-
if par.kind == inspect.Parameter.VAR_KEYWORD:
65-
# Found a keyword argument; look at code for parsing
66-
warnings.warn("keyword argument checks not yet implemented")
99+
100+
# Check for positional arguments
101+
if par.kind == inspect.Parameter.VAR_POSITIONAL:
102+
# Too complicated to check
103+
if f"*{argname}" not in docstring and verbose:
104+
print(f" {name} has positional arguments; "
105+
"check manually")
106+
continue
107+
108+
# Check for keyword arguments (then look at code for parsing)
109+
elif par.kind == inspect.Parameter.VAR_KEYWORD:
110+
# See if we documented the keyward argumnt directly
111+
if f"**{argname}" in docstring:
112+
continue
113+
114+
# Look for direct kwargs argument access
115+
kwargnames = set()
116+
for _, kwargname in re.findall(
117+
argname + r"(\[|\.pop\(|\.get\()'([\w]+)'",
118+
source):
119+
if verbose > 2:
120+
print(" Found direct keyword argument",
121+
kwargname)
122+
kwargnames.add(kwargname)
123+
124+
# Look for kwargs access via _process_legacy_keyword
125+
for kwargname in re.findall(
126+
r"_process_legacy_keyword\([\s]*" + argname +
127+
r",[\s]*'[\w]+',[\s]*'([\w]+)'", source):
128+
if verbose > 2:
129+
print(" Found legacy keyword argument",
130+
{kwargname})
131+
kwargnames.add(kwargname)
132+
133+
for kwargname in kwargnames:
134+
if obj in keyword_skiplist and \
135+
kwargname in keyword_skiplist[obj]:
136+
continue
137+
if verbose > 3:
138+
print(f" Checking keyword argument {kwargname}")
139+
assert _check_docstring(
140+
name, kwargname, inspect.getdoc(obj),
141+
prefix=prefix)
67142

68143
# Make sure this argument is documented properly in docstring
69144
else:
70-
assert _check_docstring(obj.__name__, argname, obj.__doc__)
145+
if verbose > 3:
146+
print(f" Checking argument {argname}")
147+
assert _check_docstring(
148+
name, argname, docstring, prefix=prefix)
71149

72150

73151
# Utility function to check for an argument in a docstring
74-
def _check_docstring(funcname, argname, docstring):
75-
if re.search(f" ([ \\w]+, )*{argname}(,[ \\w]+)*[^ ]:", docstring):
152+
def _check_docstring(funcname, argname, docstring, prefix=""):
153+
funcname = prefix + funcname
154+
if re.search(
155+
"\n" + r"((\w+|\.{3}), )*" + argname + r"(, (\w+|\.{3}))*:",
156+
docstring):
76157
# Found the string, but not in numpydoc form
158+
if verbose:
159+
print(f" {funcname}: {argname} docstring missing space")
77160
warnings.warn(f"{funcname} '{argname}' docstring missing space")
78161
return True
79-
80-
elif not re.search(f" ([ \\w]+, )*{argname}(,[ \\w]+)* :", docstring):
162+
163+
elif not re.search(
164+
"\n" + r"((\w+|\.{3}), )*" + argname + r"(, (\w+|\.{3}))* :",
165+
docstring):
81166
# return False
82167
#
83168
# Just issue a warning for now
169+
if verbose:
170+
print(f" {funcname}: {argname} not documented")
84171
warnings.warn(f"{funcname} '{argname}' not documented")
85172
return True
86-
173+
87174
return True

0 commit comments

Comments
 (0)