20
20
function_skiplist = [
21
21
control .ControlPlot .reshape , # needed for legacy interface
22
22
control .phase_plot , # legacy function
23
+ control .drss , # documention in rss
23
24
]
24
25
26
+ # Checksums to use for checking whether a docstring has changed
27
+ function_docstring_hash = {
28
+ control .append : 'be014503250ef73253a5372a0d082566' ,
29
+ control .describing_function_plot : '726a10eef8f2b50ef46a203653f398c7' ,
30
+ control .dlqe : '9f637afdf36c7e17b7524f9a736772b6' ,
31
+ control .dlqr : 'a9265c5ed66388661729edb2379fd9a1' ,
32
+ control .lqe : 'd265b0daf0369569e4b755fa35db7a54' ,
33
+ control .lqr : '0b76455c2b873abcbcd959e069d9d241' ,
34
+ control .frd : '7ac3076368f11e407653cd1046bbc98d' ,
35
+ control .margin : '8ee27989f1ca521ce9affe5900605b75' ,
36
+ control .parallel : 'daa3b8708200a364d9b5536b6cbb5c91' ,
37
+ control .series : '7241169911b641c43f9456bd12168271' ,
38
+ control .ss : 'aa77e816305850502c21bc40ce796f40' ,
39
+ control .ss2tf : '8d663d474ade2950dd22ec86fe3a53b7' ,
40
+ control .tf : '4e8d21e71312d83ba2e15b9c095fd962' ,
41
+ control .tf2ss : '0e5da4f3ed4aaf000f3b454c466f9013' ,
42
+ }
43
+
25
44
# List of keywords that we can skip testing (special cases)
26
45
keyword_skiplist = {
27
46
control .input_output_response : ['method' ],
28
- control .nyquist_plot : ['color' ], # checked separately
29
- control .optimal .solve_ocp : ['method' ], # deprecated
30
- control .sisotool : ['kvect' ], # deprecated
47
+ control .nyquist_plot : ['color' ], # separate check
48
+ control .optimal .solve_ocp : ['method' , 'return_x' ], # deprecated
49
+ control .sisotool : ['kvect' ], # deprecated
50
+ control .nyquist_response : ['return_contour' ], # deprecated
51
+ control .create_estimator_iosystem : ['state_labels' ], # deprecated
52
+ control .bode_plot : ['sharex' , 'sharey' , 'margin_info' ] # deprecated
31
53
}
32
54
33
55
# Decide on the level of verbosity (use -rP when running pytest)
38
60
(control .optimal , "optimal." ), (control .phaseplot , "phaseplot." )
39
61
])
40
62
def test_docstrings (module , prefix ):
63
+ checked = set () # Keep track of functions we have checked
64
+
41
65
# Look through every object in the package
42
66
if verbose > 1 :
43
67
print (f"Checking module { module } " )
@@ -56,10 +80,13 @@ def test_docstrings(module, prefix):
56
80
test_docstrings (obj , prefix + name + '.' )
57
81
58
82
if inspect .isfunction (obj ):
59
- # Skip anything that is inherited, hidden, or deprecated
83
+ # Skip anything that is inherited, hidden, deprecated, or checked
60
84
if inspect .isclass (module ) and name not in module .__dict__ \
61
- or name .startswith ('_' ) or obj in function_skiplist :
85
+ or name .startswith ('_' ) or obj in function_skiplist or \
86
+ obj in checked :
62
87
continue
88
+ else :
89
+ checked .add (obj )
63
90
64
91
# Get the docstring (skip w/ warning if there isn't one)
65
92
if verbose > 1 :
@@ -73,12 +100,18 @@ def test_docstrings(module, prefix):
73
100
source = inspect .getsource (obj )
74
101
75
102
# Skip deprecated functions
76
- if f"{ name } is deprecated" in docstring or \
77
- "function is deprecated" in docstring or \
78
- ".. deprecated::" in docstring :
103
+ if ".. deprecated::" in docstring :
79
104
if verbose > 1 :
80
105
print (" [deprecated]" )
81
106
continue
107
+ elif f"{ name } is deprecated" in docstring or \
108
+ "function is deprecated" in docstring :
109
+ if verbose > 1 :
110
+ print (" [deprecated, but not numpydoc compliant]" )
111
+ elif verbose :
112
+ print (f" { name } deprecation is not numpydoc compliant" )
113
+ warnings .warn (f"{ name } deprecated, but not numpydoc compliant" )
114
+ continue
82
115
83
116
elif f"{ name } is deprecated" in source :
84
117
if verbose :
@@ -99,17 +132,27 @@ def test_docstrings(module, prefix):
99
132
100
133
# Check for positional arguments
101
134
if par .kind == inspect .Parameter .VAR_POSITIONAL :
135
+ if obj in function_docstring_hash :
136
+ import hashlib
137
+ hash = hashlib .md5 (
138
+ docstring .encode ('utf-8' )).hexdigest ()
139
+ assert function_docstring_hash [obj ] == hash
140
+ continue
141
+
102
142
# Too complicated to check
103
143
if f"*{ argname } " not in docstring and verbose :
104
144
print (f" { name } has positional arguments; "
105
145
"check manually" )
146
+ warnings .warn (
147
+ f"{ name } { argname } has positional arguments; "
148
+ "docstring not checked" )
106
149
continue
107
150
108
151
# Check for keyword arguments (then look at code for parsing)
109
152
elif par .kind == inspect .Parameter .VAR_KEYWORD :
110
153
# See if we documented the keyward argumnt directly
111
- if f"**{ argname } " in docstring :
112
- continue
154
+ # if f"**{argname} : " in docstring:
155
+ # continue
113
156
114
157
# Look for direct kwargs argument access
115
158
kwargnames = set ()
@@ -121,7 +164,16 @@ def test_docstrings(module, prefix):
121
164
kwargname )
122
165
kwargnames .add (kwargname )
123
166
124
- # Look for kwargs access via _process_legacy_keyword
167
+ # Look for kwargs accessed via _get_param
168
+ for kwargname in re .findall (
169
+ r"_get_param\(\s*'\w*',\s*'([\w]+)',\s*" + argname ,
170
+ source ):
171
+ if verbose > 2 :
172
+ print (" Found config keyword argument" ,
173
+ {kwargname })
174
+ kwargnames .add (kwargname )
175
+
176
+ # Look for kwargs accessed via _process_legacy_keyword
125
177
70E8
for kwargname in re .findall (
126
178
r"_process_legacy_keyword\([\s]*" + argname +
127
179
r",[\s]*'[\w]+',[\s]*'([\w]+)'" , source ):
@@ -169,6 +221,6 @@ def _check_docstring(funcname, argname, docstring, prefix=""):
169
221
if verbose :
170
222
print (f" { funcname } : { argname } not documented" )
171
223
warnings .warn (f"{ funcname } '{ argname } ' not documented" )
172
- return True
224
+ return False
173
225
174
226
return True
0 commit comments