@@ -38,6 +38,10 @@ def test_kwarg_search(module, prefix):
3838 # Skip anything that isn't part of the control package
3939 continue
4040
41+ # Look for classes and then check member functions
42+ if inspect .isclass (obj ):
43+ test_kwarg_search (obj , prefix + obj .__name__ + '.' )
44+
4145 # Only look for functions with keyword arguments
4246 if not inspect .isfunction (obj ):
4347 continue
@@ -70,10 +74,6 @@ def test_kwarg_search(module, prefix):
7074 f"'unrecognized keyword' not found in unit test "
7175 f"for { name } " )
7276
73- # Look for classes and then check member functions
74- if inspect .isclass (obj ):
75- test_kwarg_search (obj , prefix + obj .__name__ + '.' )
76-
7777
7878@pytest .mark .parametrize (
7979 "function, nsssys, ntfsys, moreargs, kwargs" ,
@@ -201,3 +201,66 @@ def test_matplotlib_kwargs(function, nsysargs, moreargs, kwargs, mplcleanup):
201201 'TimeResponseData.__call__' : trdata_test .test_response_copy ,
202202 'TransferFunction.__init__' : test_unrecognized_kwargs ,
203203}
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