From 21ef29e53de87b30bf87a03ce1189cd546257fab Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Thu, 29 Apr 2021 22:06:45 +0200 Subject: [PATCH 1/3] add DefaultDict for deprecation handling --- control/config.py | 38 +++++++++++++++++++++++++++++++++++- control/freqplot.py | 8 ++++++++ control/tests/config_test.py | 37 +++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/control/config.py b/control/config.py index 99245dd2f..c720065ae 100644 --- a/control/config.py +++ b/control/config.py @@ -20,7 +20,43 @@ 'control.squeeze_time_response': None, 'forced_response.return_x': False, } -defaults = dict(_control_defaults) + + +class DefaultDict(dict): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __getitem__(self, key): + return super().__getitem__(self._check_deprecation(key)) + + def __setitem__(self, key, value): + super().__setitem__(self._check_deprecation(key), value) + + def __missing__(self, key): + repl = self._check_deprecation(key) + if self.__contains__(repl): + return self[repl] + else: + raise KeyError + + def copy(self): + return DefaultDict(self) + + def get(self, key, default=None): + return super().get(self._check_deprecation(key), default) + + def _check_deprecation(self, key): + if self.__contains__(f"deprecated.{key}"): + repl = self[f"deprecated.{key}"] + warnings.warn(f"config.defaults['{key}'] has been renamed to " + f"config.defaults['{repl}'].", + DeprecationWarning) + return repl + else: + return key + + +defaults = DefaultDict(_control_defaults) def set_defaults(module, **keywords): diff --git a/control/freqplot.py b/control/freqplot.py index f6e995bee..8dbf998d3 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -68,8 +68,16 @@ 'freqplot.Hz': False, # Plot frequency in Hertz 'freqplot.grid': True, # Turn on grid for gain and phase 'freqplot.wrap_phase': False, # Wrap the phase plot at a given value + + # deprecations + 'deprecated.bode.dB': 'freqplot.dB', + 'deprecated.bode.deg': 'freqplot.deg', + 'deprecated.bode.Hz': 'freqplot.Hz', + 'deprecated.bode.grid': 'freqplot.grid', + 'deprecated.bode.wrap_phase': 'freqplot.wrap_phase', } + # # Main plotting functions # diff --git a/control/tests/config_test.py b/control/tests/config_test.py index 45fd8de22..1e18504a0 100644 --- a/control/tests/config_test.py +++ b/control/tests/config_test.py @@ -49,6 +49,43 @@ def test_get_param(self): assert ct.config._get_param('config', 'test4', {'test4': 1}, None) == 1 + def test_default_deprecation(self): + ct.config.defaults['config.newkey'] = 1 + ct.config.defaults['deprecated.config.oldkey'] = 'config.newkey' + ct.config.defaults['deprecated.config.oldmiss'] = 'config.newmiss' + + msgpattern = r'config\.oldkey.* has been renamed to .*config\.newkey' + + with pytest.warns(DeprecationWarning, match=msgpattern): + assert ct.config.defaults['config.oldkey'] == 1 + with pytest.warns(DeprecationWarning, match=msgpattern): + ct.config.defaults['config.oldkey'] = 2 + with pytest.warns(DeprecationWarning, match=msgpattern): + assert ct.config.defaults['config.oldkey'] == 2 + assert ct.config.defaults['config.newkey'] == 2 + + ct.config.set_defaults('config', newkey=3) + with pytest.warns(DeprecationWarning, match=msgpattern): + assert ct.config._get_param('config', 'oldkey') == 3 + with pytest.warns(DeprecationWarning, match=msgpattern): + ct.config.set_defaults('config', oldkey=4) + with pytest.warns(DeprecationWarning, match=msgpattern): + assert ct.config.defaults['config.oldkey'] == 4 + assert ct.config.defaults['config.newkey'] == 4 + + with pytest.raises(KeyError): + with pytest.warns(DeprecationWarning, match=msgpattern): + ct.config.defaults['config.oldmiss'] + with pytest.raises(KeyError): + ct.config.defaults['config.neverdefined'] + + # assert that reset defaults keeps the custom type + ct.config.reset_defaults() + with pytest.warns(DeprecationWarning, + match='bode.* has been renamed to.*freqplot'): + assert ct.config.defaults['bode.Hz'] \ + == ct.config.defaults['freqplot.Hz'] + @mplcleanup def test_fbs_bode(self): ct.use_fbs_defaults() From c37df52ee23dc1eda96e5d272085ab052788fc7e Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Thu, 29 Apr 2021 22:38:38 +0200 Subject: [PATCH 2/3] remove __getitem__, covered by __missing__ --- control/config.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/control/config.py b/control/config.py index c720065ae..cdf723b47 100644 --- a/control/config.py +++ b/control/config.py @@ -26,9 +26,6 @@ class DefaultDict(dict): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def __getitem__(self, key): - return super().__getitem__(self._check_deprecation(key)) - def __setitem__(self, key, value): super().__setitem__(self._check_deprecation(key), value) From e0dab934ce364fd4d59cac9ae9964f8ad054bee3 Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Sat, 1 May 2021 15:15:42 +0200 Subject: [PATCH 3/3] use collections.UserDict for DefaultDict --- control/config.py | 23 ++++++++++++++--------- control/tests/config_test.py | 24 +++++++++++++++--------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/control/config.py b/control/config.py index cdf723b47..afd7615ca 100644 --- a/control/config.py +++ b/control/config.py @@ -7,6 +7,8 @@ # files. For now, you can just choose between MATLAB and FBS default # values + tweak a few other things. + +import collections import warnings __all__ = ['defaults', 'set_defaults', 'reset_defaults', @@ -22,7 +24,14 @@ } -class DefaultDict(dict): +class DefaultDict(collections.UserDict): + """Map names for settings from older version to their renamed ones. + + If a user wants to write to an old setting, issue a warning and write to + the renamed setting instead. Accessing the old setting returns the value + from the new name. + """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -30,24 +39,20 @@ def __setitem__(self, key, value): super().__setitem__(self._check_deprecation(key), value) def __missing__(self, key): + # An old key should never have been set. If it is being accessed + # through __getitem__, return the value from the new name. repl = self._check_deprecation(key) if self.__contains__(repl): return self[repl] else: - raise KeyError - - def copy(self): - return DefaultDict(self) - - def get(self, key, default=None): - return super().get(self._check_deprecation(key), default) + raise KeyError(key) def _check_deprecation(self, key): if self.__contains__(f"deprecated.{key}"): repl = self[f"deprecated.{key}"] warnings.warn(f"config.defaults['{key}'] has been renamed to " f"config.defaults['{repl}'].", - DeprecationWarning) + FutureWarning, stacklevel=3) return repl else: return key diff --git a/control/tests/config_test.py b/control/tests/config_test.py index 1e18504a0..e198254bf 100644 --- a/control/tests/config_test.py +++ b/control/tests/config_test.py @@ -50,38 +50,44 @@ def test_get_param(self): assert ct.config._get_param('config', 'test4', {'test4': 1}, None) == 1 def test_default_deprecation(self): - ct.config.defaults['config.newkey'] = 1 ct.config.defaults['deprecated.config.oldkey'] = 'config.newkey' ct.config.defaults['deprecated.config.oldmiss'] = 'config.newmiss' msgpattern = r'config\.oldkey.* has been renamed to .*config\.newkey' - with pytest.warns(DeprecationWarning, match=msgpattern): + ct.config.defaults['config.newkey'] = 1 + with pytest.warns(FutureWarning, match=msgpattern): assert ct.config.defaults['config.oldkey'] == 1 - with pytest.warns(DeprecationWarning, match=msgpattern): + with pytest.warns(FutureWarning, match=msgpattern): ct.config.defaults['config.oldkey'] = 2 - with pytest.warns(DeprecationWarning, match=msgpattern): + with pytest.warns(FutureWarning, match=msgpattern): assert ct.config.defaults['config.oldkey'] == 2 assert ct.config.defaults['config.newkey'] == 2 ct.config.set_defaults('config', newkey=3) - with pytest.warns(DeprecationWarning, match=msgpattern): + with pytest.warns(FutureWarning, match=msgpattern): assert ct.config._get_param('config', 'oldkey') == 3 - with pytest.warns(DeprecationWarning, match=msgpattern): + with pytest.warns(FutureWarning, match=msgpattern): ct.config.set_defaults('config', oldkey=4) - with pytest.warns(DeprecationWarning, match=msgpattern): + with pytest.warns(FutureWarning, match=msgpattern): assert ct.config.defaults['config.oldkey'] == 4 assert ct.config.defaults['config.newkey'] == 4 + ct.config.defaults.update({'config.newkey': 5}) + with pytest.warns(FutureWarning, match=msgpattern): + ct.config.defaults.update({'config.oldkey': 6}) + with pytest.warns(FutureWarning, match=msgpattern): + assert ct.config.defaults.get('config.oldkey') == 6 + with pytest.raises(KeyError): - with pytest.warns(DeprecationWarning, match=msgpattern): + with pytest.warns(FutureWarning, match=msgpattern): ct.config.defaults['config.oldmiss'] with pytest.raises(KeyError): ct.config.defaults['config.neverdefined'] # assert that reset defaults keeps the custom type ct.config.reset_defaults() - with pytest.warns(DeprecationWarning, + with pytest.warns(FutureWarning, match='bode.* has been renamed to.*freqplot'): assert ct.config.defaults['bode.Hz'] \ == ct.config.defaults['freqplot.Hz']