From 18081a18ab4b0205be375b57873ff039c8f3ae9d Mon Sep 17 00:00:00 2001 From: ischwabacher Date: Mon, 11 Aug 2014 15:12:09 -0500 Subject: [PATCH] Allow deprecate_kwarg to transform arguments Add a keyword argument `mapping` to `pandas.util.deprecate_kwarg` to allow remapping deprecated keyword arguments. For example, in #7963, which inspired this PR, one could write this: ```python @deprecate_kwarg(old_arg_name='infer_dst', new_arg_name='ambiguous', mapping={True: 'infer', False: 'raise'}) def tz_localize(self, tz, ambiguous=None): ... ``` Tests of this functionality are added to `pandas/tests/test_util.py`, created for this purpose; the `if __name__ == '__main__'` section is cribbed directly from `test_index.py`. --- pandas/tests/test_util.py | 64 +++++++++++++++++++++++++++++++++++++++ pandas/util/decorators.py | 38 ++++++++++++++++++++--- 2 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 pandas/tests/test_util.py diff --git a/pandas/tests/test_util.py b/pandas/tests/test_util.py new file mode 100644 index 0000000000000..76b49a5f976bd --- /dev/null +++ b/pandas/tests/test_util.py @@ -0,0 +1,64 @@ + +import warnings + +import nose + +import pandas.util +from pandas.util.decorators import deprecate_kwarg +import pandas.util.testing as tm + +class TestDecorators(tm.TestCase): + def setUp(self): + @deprecate_kwarg('old', 'new') + def _f1(new=False): + return new + + @deprecate_kwarg('old', 'new', {'yes': True, 'no': False}) + def _f2(new=False): + return new + + @deprecate_kwarg('old', 'new', lambda x: x+1) + def _f3(new=0): + return new + + self.f1 = _f1 + self.f2 = _f2 + self.f3 = _f3 + + def test_deprecate_kwarg(self): + x = 78 + with tm.assert_produces_warning(FutureWarning): + result = self.f1(old=x) + self.assertIs(result, x) + with tm.assert_produces_warning(None): + self.f1(new=x) + + def test_dict_deprecate_kwarg(self): + x = 'yes' + with tm.assert_produces_warning(FutureWarning): + result = self.f2(old=x) + self.assertEqual(result, True) + + def test_missing_deprecate_kwarg(self): + x = 'bogus' + with tm.assert_produces_warning(FutureWarning): + result = self.f2(old=x) + self.assertEqual(result, 'bogus') + + def test_callable_deprecate_kwarg(self): + x = 5 + with tm.assert_produces_warning(FutureWarning): + result = self.f3(old=x) + self.assertEqual(result, x+1) + with tm.assertRaises(TypeError): + self.f3(old='hello') + + def test_bad_deprecate_kwarg(self): + with tm.assertRaises(TypeError): + @deprecate_kwarg('old', 'new', 0) + def f4(new=None): + pass + +if __name__ == '__main__': + nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'], + exit=False) diff --git a/pandas/util/decorators.py b/pandas/util/decorators.py index d94897a6685a2..288ec164198e4 100644 --- a/pandas/util/decorators.py +++ b/pandas/util/decorators.py @@ -15,7 +15,7 @@ def wrapper(*args, **kwargs): return wrapper -def deprecate_kwarg(old_arg_name, new_arg_name): +def deprecate_kwarg(old_arg_name, new_arg_name, mapping=None): """Decorator to deprecate a keyword argument of a function Parameters @@ -24,6 +24,10 @@ def deprecate_kwarg(old_arg_name, new_arg_name): Name of argument in function to deprecate new_arg_name : str Name of prefered argument in function + mapping : dict or callable + If mapping is present, use it to translate old arguments to + new arguments. A callable must do its own value checking; + values not found in a dict will be forwarded unchanged. Examples -------- @@ -31,7 +35,7 @@ def deprecate_kwarg(old_arg_name, new_arg_name): >>> @deprecate_kwarg(old_arg_name='cols', new_arg_name='columns') ... def f(columns=''): - ... print columns + ... print(columns) ... >>> f(columns='should work ok') should work ok @@ -41,22 +45,46 @@ def deprecate_kwarg(old_arg_name, new_arg_name): should raise warning >>> f(cols='should error', columns="can't pass do both") TypeError: Can only specify 'cols' or 'columns', not both + >>> @deprecate_kwarg('old', 'new', {'yes': True, 'no', False}) + ... def f(new=False): + ... print('yes!' if new else 'no!') + ... + >>> f(old='yes') + FutureWarning: old='yes' is deprecated, use new=True instead + warnings.warn(msg, FutureWarning) + yes! """ + if mapping is not None and not hasattr(mapping, 'get') and \ + not callable(mapping): + raise TypeError("mapping from old to new argument values " + "must be dict or callable!") def _deprecate_kwarg(func): @wraps(func) def wrapper(*args, **kwargs): old_arg_value = kwargs.pop(old_arg_name, None) if old_arg_value is not None: - msg = "the '%s' keyword is deprecated, use '%s' instead" % \ - (old_arg_name, new_arg_name) + if mapping is not None: + if hasattr(mapping, 'get'): + new_arg_value = mapping.get(old_arg_value, + old_arg_value) + else: + new_arg_value = mapping(old_arg_value) + msg = "the %s=%r keyword is deprecated, " \ + "use %s=%r instead" % \ + (old_arg_name, old_arg_value, + new_arg_name, new_arg_value) + else: + new_arg_value = old_arg_value + msg = "the '%s' keyword is deprecated, " \ + "use '%s' instead" % (old_arg_name, new_arg_name) warnings.warn(msg, FutureWarning) if kwargs.get(new_arg_name, None) is not None: msg = "Can only specify '%s' or '%s', not both" % \ (old_arg_name, new_arg_name) raise TypeError(msg) else: - kwargs[new_arg_name] = old_arg_value + kwargs[new_arg_name] = new_arg_value return func(*args, **kwargs) return wrapper return _deprecate_kwarg