Merge pull request #794 from murrayrm/mutable_default_args-13Nov2022 · python-control/python-control@2a799e9

@@ -38,6 +38,10 @@ def test_kwarg_search(module, prefix):

3838

# Skip anything that isn't part of the control package

3939

continue

404041+

# 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}")

727673-

# Look for classes and then check member functions

74-

if inspect.isclass(obj):

75-

test_kwarg_search(obj, prefix + obj.__name__ + '.')

76-77777878

@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+