@@ -38,6 +38,10 @@ def test_kwarg_search(module, prefix):
38
38
# Skip anything that isn't part of the control package
39
39
continue
40
40
41
+ # Look for classes and then check member functions
42
+ if inspect .isclass (obj ):
43
+ test_kwarg_search (obj , prefix + obj .__name__ + '.' )
44
+
41
45
# Only look for functions with keyword arguments
42
46
if not inspect .isfunction (obj ):
43
47
continue
@@ -70,10 +74,6 @@ def test_kwarg_search(module, prefix):
70
74
f"'unrecognized keyword' not found in unit test "
71
75
f"for { name } " )
72
76
73
- # Look for classes and then check member functions
74
- if inspect .isclass (obj ):
75
- test_kwarg_search (obj , prefix + obj .__name__ + '.' )
76
-
77
77
78
78
@pytest .mark .parametrize (
79
79
"function, nsssys, ntfsys, moreargs, kwargs" ,
@@ -201,3 +201,66 @@ def test_matplotlib_kwargs(function, nsysargs, moreargs, kwargs, mplcleanup):
201
201
'TimeResponseData.__call__' : trdata_test .test_response_copy ,
202
202
'TransferFunction.__init__' : test_unrecognized_kwargs ,
203
203
}
204
+
205
+ #
206
+ # Look for keywords with mutable defaults
207
+ #
208
+ # This test goes through every function and looks for signatures that have a
209
+ # default value for a keyword that is mutable. An error is generated unless
210
+ # the function is listed in the `mutable_ok` set (which should only be used
211
+ # for cases were the code has been explicitly checked to make sure that the
212
+ # value of the mutable is not modified in the code).
213
+ #
214
+ mutable_ok = { # initial and date
215
+ control .flatsys .SystemTrajectory .__init__ , # RMM, 18 Nov 2022
216
+ control .freqplot ._add_arrows_to_line2D , # RMM, 18 Nov 2022
217
+ control .namedio ._process_dt_keyword , # RMM, 13 Nov 2022
218
+ control .namedio ._process_namedio_keywords , # RMM, 18 Nov 2022
219
+ control .optimal .OptimalControlProblem .__init__ , # RMM, 18 Nov 2022
220
+ control .optimal .solve_ocp , # RMM, 18 Nov 2022
221
+ control .optimal .create_mpc_iosystem , # RMM, 18 Nov 2022
222
+ }
223
+
224
+ @pytest .mark .parametrize ("module" , [control , control .flatsys ])
225
+ def test_mutable_defaults (module , recurse = True ):
226
+ # Look through every object in the package
227
+ for name , obj in inspect .getmembers (module ):
228
+ # Skip anything that is outside of this module
229
+ if inspect .getmodule (obj ) is not None and \
230
+ not inspect .getmodule (obj ).__name__ .startswith ('control' ):
231
+ # Skip anything that isn't part of the control package
232
+ continue
233
+
234
+ # Look for classes and then check member functions
235
+ if inspect .isclass (obj ):
236
+ test_mutable_defaults (obj , True )
237
+
238
+ # Look for modules and check for internal functions (w/ no recursion)
239
+ if inspect .ismodule (obj ) and recurse :
240
+ test_mutable_defaults (obj , False )
241
+
242
+ # Only look at functions and skip any that are marked as OK
243
+ if not inspect .isfunction (obj ) or obj in mutable_ok :
244
+ continue
245
+
246
+ # Get the signature for the function
247
+ sig = inspect .signature (obj )
248
+
249
+ # Skip anything that is inherited
250
+ if inspect .isclass (module ) and obj .__name__ not in module .__dict__ :
251
+ continue
252
+
253
+ # See if there is a variable keyword argument
254
+ for argname , par in sig .parameters .items ():
255
+ if par .default is inspect ._empty or \
256
+ not par .kind == inspect .Parameter .KEYWORD_ONLY and \
257
+ not par .kind == inspect .Parameter .POSITIONAL_OR_KEYWORD :
258
+ continue
259
+
260
+ # Check to see if the default value is mutable
261
+ if par .default is not None and not \
262
+ isinstance (par .default , (bool , int , float , tuple , str )):
263
+ pytest .fail (
264
+ f"function '{ obj .__name__ } ' in module '{ module .__name__ } '"
265
+ f" has mutable default for keyword '{ par .name } '" )
266
+
0 commit comments