2020function_skiplist = [
2121 control .ControlPlot .reshape , # needed for legacy interface
2222 control .phase_plot , # legacy function
23+ control .drss , # documention in rss
2324]
2425
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+
2544# List of keywords that we can skip testing (special cases)
2645keyword_skiplist = {
2746 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
3153}
3254
3355# Decide on the level of verbosity (use -rP when running pytest)
3860 (control .optimal , "optimal." ), (control .phaseplot , "phaseplot." )
3961])
4062def test_docstrings (module , prefix ):
63+ checked = set () # Keep track of functions we have checked
64+
4165 # Look through every object in the package
4266 if verbose > 1 :
4367 print (f"Checking module { module } " )
@@ -56,10 +80,13 @@ def test_docstrings(module, prefix):
5680 test_docstrings (obj , prefix + name + '.' )
5781
5882 if inspect .isfunction (obj ):
59- # Skip anything that is inherited, hidden, or deprecated
83+ # Skip anything that is inherited, hidden, deprecated, or checked
6084 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 :
6287 continue
88+ else :
89+ checked .add (obj )
6390
6491 # Get the docstring (skip w/ warning if there isn't one)
6592 if verbose > 1 :
@@ -73,12 +100,18 @@ def test_docstrings(module, prefix):
73100 source = inspect .getsource (obj )
74101
75102 # 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 :
79104 if verbose > 1 :
80105 print (" [deprecated]" )
81106 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
82115
83116 elif f"{ name } is deprecated" in source :
84117 if verbose :
@@ -99,17 +132,27 @@ def test_docstrings(module, prefix):
99132
100133 # Check for positional arguments
101134 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+
102142 # Too complicated to check
103143 if f"*{ argname } " not in docstring and verbose :
104144 print (f" { name } has positional arguments; "
105145 "check manually" )
146+ warnings .warn (
147+ f"{ name } { argname } has positional arguments; "
148+ "docstring not checked" )
106149 continue
107150
108151 # Check for keyword arguments (then look at code for parsing)
109152 elif par .kind == inspect .Parameter .VAR_KEYWORD :
110153 # See if we documented the keyward argumnt directly
111- if f"**{ argname } " in docstring :
112- continue
154+ # if f"**{argname} : " in docstring:
155+ # continue
113156
114157 # Look for direct kwargs argument access
115158 kwargnames = set ()
@@ -121,7 +164,16 @@ def test_docstrings(module, prefix):
121164 kwargname )
122165 kwargnames .add (kwargname )
123166
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
125177 for kwargname in re .findall (
126178 r"_process_legacy_keyword\([\s]*" + argname +
127179 r",[\s]*'[\w]+',[\s]*'([\w]+)'" , source ):
@@ -169,6 +221,6 @@ def _check_docstring(funcname, argname, docstring, prefix=""):
169221 if verbose :
170222 print (f" { funcname } : { argname } not documented" )
171223 warnings .warn (f"{ funcname } '{ argname } ' not documented" )
172- return True
224+ return False
173225
174226 return True
0 commit comments