Skip to content

Commit a86d33f

Browse files
committed
add kwargs_test + checks for unrecognized keywords
1 parent 57e3751 commit a86d33f

7 files changed

Lines changed: 208 additions & 17 deletions

File tree

control/iosys.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2238,7 +2238,15 @@ def ss(*args, **kwargs):
22382238
>>> sys2 = ss(sys_tf)
22392239
22402240
"""
2241-
sys = _ss(*args, keywords=kwargs)
2241+
# Extract the keyword arguments needed for StateSpace (via _ss)
2242+
ss_kwlist = ('dt', 'remove_useless_states')
2243+
ss_kwargs = {}
2244+
for kw in ss_kwlist:
2245+
if kw in kwargs:
2246+
ss_kwargs[kw] = kwargs.pop(kw)
2247+
2248+
# Create the statespace system and then convert to I/O system
2249+
sys = _ss(*args, keywords=ss_kwargs)
22422250
return LinearIOSystem(sys, **kwargs)
22432251

22442252

control/statefbk.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ def place_varga(A, B, p, dtime=False, alpha=None):
261261

262262

263263
# contributed by Sawyer B. Fuller <minster@uw.edu>
264-
def lqe(*args, **keywords):
264+
def lqe(*args, method=None):
265265
"""lqe(A, G, C, QN, RN, [, NN])
266266
267267
Linear quadratic estimator design (Kalman filter) for continuous-time
@@ -356,18 +356,15 @@ def lqe(*args, **keywords):
356356
# Process the arguments and figure out what inputs we received
357357
#
358358

359-
# Get the method to use (if specified as a keyword)
360-
method = keywords.get('method', None)
359+
# If we were passed a discrete time system as the first arg, use dlqe()
360+
if isinstance(args[0], LTI) and isdtime(args[0], strict=True):
361+
# Call dlqe
362+
return dlqe(*args, method=method)
361363

362364
# Get the system description
363365
if (len(args) < 3):
364366
raise ControlArgument("not enough input arguments")
365367

366-
# If we were passed a discrete time system as the first arg, use dlqe()
367-
if isinstance(args[0], LTI) and isdtime(args[0], strict=True):
368-
# Call dlqe
369-
return dlqe(*args, **keywords)
370-
371368
# If we were passed a state space system, use that to get system matrices
372369
if isinstance(args[0], StateSpace):
373370
A = np.array(args[0].A, ndmin=2, dtype=float)
@@ -409,7 +406,7 @@ def lqe(*args, **keywords):
409406

410407

411408
# contributed by Sawyer B. Fuller <minster@uw.edu>
412-
def dlqe(*args, **keywords):
409+
def dlqe(*args, method=None):
413410
"""dlqe(A, G, C, QN, RN, [, N])
414411
415412
Linear quadratic estimator design (Kalman filter) for discrete-time
@@ -480,9 +477,6 @@ def dlqe(*args, **keywords):
480477
# Process the arguments and figure out what inputs we received
481478
#
482479

483-
# Get the method to use (if specified as a keyword)
484-
method = keywords.get('method', None)
485-
486480
# Get the system description
487481
if (len(args) < 3):
488482
raise ControlArgument("not enough input arguments")

control/statesp.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,10 @@ def __init__(self, *args, keywords=None, **kwargs):
340340
self.dt = dt
341341
self.nstates = A.shape[1]
342342

343+
# Make sure there were no extraneous keywords
344+
if keywords:
345+
raise TypeError("unrecognized keywords: ", str(keywords))
346+
343347
if 0 == self.nstates:
344348
# static gain
345349
# matrix's default "empty" shape is 1x0

control/tests/frd_test.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,3 +472,9 @@ def test_repr_str(self):
472472
10.000 0.2 +4j
473473
100.000 0.1 +6j"""
474474
assert str(sysm) == refm
475+
476+
def test_unrecognized_keyword(self):
477+
h = TransferFunction([1], [1, 2, 2])
478+
omega = np.logspace(-1, 2, 10)
479+
with pytest.raises(TypeError, match="unrecognized keyword"):
480+
frd = FRD(h, omega, unknown=None)

control/tests/iosys_test.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1580,7 +1580,8 @@ def test_interconnect_unused_input():
15801580
outputs=['u'],
15811581
name='k')
15821582

1583-
with pytest.warns(UserWarning, match=r"Unused input\(s\) in InterconnectedSystem"):
1583+
with pytest.warns(
1584+
UserWarning, match=r"Unused input\(s\) in InterconnectedSystem"):
15841585
h = ct.interconnect([g,s,k],
15851586
inputs=['r'],
15861587
outputs=['y'])
@@ -1611,13 +1612,19 @@ def test_interconnect_unused_input():
16111612

16121613

16131614
# warn if explicity ignored input in fact used
1614-
with pytest.warns(UserWarning, match=r"Input\(s\) specified as ignored is \(are\) used:") as record:
1615+
with pytest.warns(
1616+
UserWarning,
1617+
match=r"Input\(s\) specified as ignored is \(are\) used:") \
1618+
as record:
16151619
h = ct.interconnect([g,s,k],
16161620
inputs=['r'],
16171621
outputs=['y'],
16181622
ignore_inputs=['u','n'])
16191623

1620-
with pytest.warns(UserWarning, match=r"Input\(s\) specified as ignored is \(are\) used:") as record:
1624+
with pytest.warns(
1625+
UserWarning,
1626+
match=r"Input\(s\) specified as ignored is \(are\) used:") \
1627+
as record:
16211628
h = ct.interconnect([g,s,k],
16221629
inputs=['r'],
16231630
outputs=['y'],

control/tests/kwargs_test.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# kwargs_test.py - test for uncrecognized keywords
2+
# RMM, 20 Mar 2022
3+
#
4+
# Allowing unrecognized keywords to be passed to a function without
5+
# generating and error message can generate annoying bugs, since you
6+
# sometimes think you are telling the function to do something and actually
7+
# you have a misspelling or other error and your input is being ignored.
8+
#
9+
# This unit test looks through all functions in the package for any that
10+
# allow kwargs as part of the function signature and makes sure that there
11+
# is a unit test that checks for unrecognized keywords.
12+
13+
import inspect
14+
import pytest
15+
import warnings
16+
17+
import control
18+
import control.flatsys
19+
20+
# List of all of the test modules where kwarg unit tests are defined
21+
import control.tests.flatsys_test as flatsys_test
22+
import control.tests.frd_test as frd_test
23+
import control.tests.interconnect_test as interconnect_test
24+
import control.tests.statefbk_test as statefbk_test
25+
import control.tests.trdata_test as trdata_test
26+
27+
28+
@pytest.mark.parametrize("module, prefix", [
29+
(control, ""), (control.flatsys, "flatsys.")
30+
])
31+
def test_kwarg_search(module, prefix):
32+
# Look through every object in the package
33+
for name, obj in inspect.getmembers(module):
34+
# Skip anything that is outside of this module
35+
if inspect.getmodule(obj) is not None and \
36+
not inspect.getmodule(obj).__name__.startswith('control'):
37+
# Skip anything that isn't part of the control package
38+
continue
39+
40+
# Look for functions with keyword arguments
41+
if inspect.isfunction(obj):
42+
# Get the signature for the function
43+
sig = inspect.signature(obj)
44+
45+
# See if there is a variable keyword argument
46+
for argname, par in sig.parameters.items():
47+
if par.kind == inspect.Parameter.VAR_KEYWORD:
48+
# Make sure there is a unit test defined
49+
assert prefix + name in kwarg_unittest
50+
51+
# Make sure there is a unit test
52+
if not hasattr(kwarg_unittest[prefix + name], '__call__'):
53+
warnings.warn("No unit test defined for '%s'"
54+
% prefix + name)
55+
56+
# Look for classes and then check member functions
57+
if inspect.isclass(obj):
58+
test_kwarg_search(obj, prefix + obj.__name__ + '.')
59+
60+
61+
# Create a SISO system for use in parameterized tests
62+
sys = control.ss([[-1, 1], [0, -1]], [[0], [1]], [[1, 0]], 0, dt=None)
63+
64+
65+
# Parameterized tests for looking for unrecognized keyword errors
66+
@pytest.mark.parametrize("function, args, kwargs", [
67+
[control.dlqr, (sys, [[1, 0], [0, 1]], [[1]]), {}],
68+
[control.drss, (2, 1, 1), {}],
69+
[control.input_output_response, (sys, [0, 1, 2], [1, 1, 1]), {}],
70+
[control.lqr, (sys, [[1, 0], [0, 1]], [[1]]), {}],
71+
[control.pzmap, (sys,), {}],
72+
[control.rlocus, (control.tf([1], [1, 1]), ), {}],
73+
[control.root_locus, (control.tf([1], [1, 1]), ), {}],
74+
[control.rss, (2, 1, 1), {}],
75+
[control.ss, (0, 0, 0, 0), {'dt': 1}],
76+
[control.ss2io, (sys,), {}],
77+
[control.summing_junction, (2,), {}],
78+
[control.tf, ([1], [1, 1]), {}],
79+
[control.tf2io, (control.tf([1], [1, 1]),), {}],
80+
[control.InputOutputSystem, (1, 1, 1), {}],
81+
[control.StateSpace, ([[-1, 0], [0, -1]], [[1], [1]], [[1, 1]], 0), {}],
82+
[control.TransferFunction, ([1], [1, 1]), {}],
83+
])
84+
def test_unrecognized_kwargs(function, args, kwargs):
85+
# Call the function normally and make sure it works
86+
function(*args, **kwargs)
87+
88+
# Now add an unrecognized keyword and make sure there is an error
89+
with pytest.raises(TypeError, match="unrecognized keyword"):
90+
function(*args, **kwargs, unknown=None)
91+
92+
93+
# Parameterized tests for looking for keyword errors handled by matplotlib
94+
@pytest.mark.parametrize("function, args, kwargs", [
95+
[control.bode, (sys, ), {}],
96+
[control.bode_plot, (sys, ), {}],
97+
[control.gangof4, (sys, sys), {}],
98+
[control.gangof4_plot, (sys, sys), {}],
99+
[control.nyquist, (sys, ), {}],
100+
[control.nyquist_plot, (sys, ), {}],
101+
])
102+
def test_matplotlib_kwargs(function, args, kwargs):
103+
# Call the function normally and make sure it works
104+
function(*args, **kwargs)
105+
106+
# Now add an unrecognized keyword and make sure there is an error
107+
with pytest.raises(AttributeError, match="has no property"):
108+
function(*args, **kwargs, unknown=None)
109+
110+
111+
#
112+
# List of all unit tests that check for unrecognized keywords
113+
#
114+
# Every function that accepts variable keyword arguments (**kwargs) should
115+
# have an entry in this table, to make sure that nothing is missing. This
116+
# will also force people who add new functions to put in an appropriate unit
117+
# test.
118+
#
119+
120+
kwarg_unittest = {
121+
'bode': test_matplotlib_kwargs,
122+
'bode_plot': test_matplotlib_kwargs,
123+
'describing_function_plot': None,
124+
'dlqr': statefbk_test.TestStatefbk.test_lqr_errors,
125+
'drss': test_unrecognized_kwargs,
126+
'find_eqpt': None,
127+
'gangof4': test_matplotlib_kwargs,
128+
'gangof4_plot': test_matplotlib_kwargs,
129+
'input_output_response': test_unrecognized_kwargs,
130+
'interconnect': interconnect_test.test_interconnect_exceptions,
131+
'linearize': None,
132+
'lqr': statefbk_test.TestStatefbk.test_lqr_errors,
133+
'nyquist': test_matplotlib_kwargs,
134+
'nyquist_plot': test_matplotlib_kwargs,
135+
'pzmap': None,
136+
'rlocus': test_unrecognized_kwargs,
137+
'root_locus': test_unrecognized_kwargs,
138+
'rss': test_unrecognized_kwargs,
139+
'set_defaults': None,
140+
'singular_values_plot': None,
141+
'ss': test_unrecognized_kwargs,
142+
'ss2io': test_unrecognized_kwargs,
143+
'ss2tf': test_unrecognized_kwargs,
144+
'summing_junction': interconnect_test.test_interconnect_exceptions,
145+
'tf': test_unrecognized_kwargs,
146+
'tf2io' : test_unrecognized_kwargs,
147+
'flatsys.point_to_point':
148+
flatsys_test.TestFlatSys.test_point_to_point_errors,
149+
'FrequencyResponseData.__init__':
150+
frd_test.TestFRD.test_unrecognized_keyword,
151+
'InputOutputSystem.__init__': None,
152+
'InputOutputSystem.linearize': None,
153+
'InterconnectedSystem.__init__':
154+
interconnect_test.test_interconnect_exceptions,
155+
'InterconnectedSystem.linearize': None,
156+
'LinearICSystem.linearize': None,
157+
'LinearIOSystem.__init__':
158+
interconnect_test.test_interconnect_exceptions,
159+
'LinearIOSystem.linearize': None,
160+
'NonlinearIOSystem.__init__':
161+
interconnect_test.test_interconnect_exceptions,
162+
'NonlinearIOSystem.linearize': None,
163+
'StateSpace.__init__': None,
164+
'TimeResponseData.__call__': trdata_test.test_response_copy,
165+
'TransferFunction.__init__': None,
166+
'flatsys.FlatSystem.linearize': None,
167+
'flatsys.LinearFlatSystem.linearize': None,
168+
}

control/xferfcn.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1574,7 +1574,11 @@ def ss2tf(*args, **kwargs):
15741574
# Assume we were given the A, B, C, D matrix and (optional) dt
15751575
return _convert_to_transfer_function(StateSpace(*args, **kwargs))
15761576

1577-
elif len(args) == 1:
1577+
# Make sure there were no extraneous keywords
1578+
if kwargs:
1579+
raise TypeError("unrecognized keywords: ", str(kwargs))
1580+
1581+
if len(args) == 1:
15781582
sys = args[0]
15791583
if isinstance(sys, StateSpace):
15801584
return _convert_to_transfer_function(sys)

0 commit comments

Comments
 (0)