From e2aeda0deb197268962130c4f9df7444af194b00 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Mon, 14 Oct 2019 07:13:52 +0100 Subject: [PATCH 1/2] Allow deprecating by date. Includes tests. --- salt/utils/versions.py | 124 +++++++++++++++++++++++++----- tests/unit/utils/test_versions.py | 67 +++++++++++++++- 2 files changed, 171 insertions(+), 20 deletions(-) diff --git a/salt/utils/versions.py b/salt/utils/versions.py index aa95b8afb71e..9334c9d7c034 100644 --- a/salt/utils/versions.py +++ b/salt/utils/versions.py @@ -17,6 +17,9 @@ import numbers import sys import warnings +import datetime +import inspect +import contextlib # pylint: disable=blacklisted-module,no-name-in-module from distutils.version import StrictVersion as _StrictVersion from distutils.version import LooseVersion as _LooseVersion @@ -75,6 +78,24 @@ def _cmp(self, other): return 1 +def _format_warning(message, category, filename, lineno, line=None): + ''' + Replacement for warnings.formatwarning that disables the echoing of + the 'line' parameter. + ''' + return '{}:{}: {}: {}\n'.format( + filename, lineno, category.__name__, message + ) + + +@contextlib.contextmanager +def _patched_format_warning(): + saved = warnings.formatwarning + warnings.formatwarning = _format_warning + yield + warnings.formatwarning = saved + + def warn_until(version, message, category=DeprecationWarning, @@ -125,7 +146,6 @@ def warn_until(version, _version_ = salt.version.SaltStackVersion(*_version_info_) if _version_ >= version: - import inspect caller = inspect.getframeinfo(sys._getframe(stacklevel - 1)) raise RuntimeError( 'The warning triggered on filename \'{filename}\', line number ' @@ -140,26 +160,92 @@ def warn_until(version, ) if _dont_call_warnings is False: - def _formatwarning(message, - category, - filename, - lineno, - line=None): # pylint: disable=W0613 - ''' - Replacement for warnings.formatwarning that disables the echoing of - the 'line' parameter. - ''' - return '{0}:{1}: {2}: {3}\n'.format( - filename, lineno, category.__name__, message + with _patched_format_warning(): + warnings.warn( + message.format(version=version.formatted_version), + category, + stacklevel=stacklevel ) - saved = warnings.formatwarning - warnings.formatwarning = _formatwarning - warnings.warn( - message.format(version=version.formatted_version), - category, - stacklevel=stacklevel + + +def _get_utcnow_date(): + ''' + This function exists because we can't easily patch builtin objects when mocking + ''' + return datetime.datetime.utcnow().date() + + +def warn_until_date(date, + message, + category=DeprecationWarning, + stacklevel=None, + _dont_call_warnings=False): + ''' + Helper function to raise a warning, by default, a ``DeprecationWarning``, + until the provided ``date``, after which, a ``RuntimeError`` will + be raised to remind the developers to remove the warning because the + target date has been reached. + + :param date: A ``datetime.date`` or ``datetime.datetime`` instance. + :param message: The warning message to be displayed. + :param category: The warning class to be thrown, by default + ``DeprecationWarning`` + :param stacklevel: There should be no need to set the value of + ``stacklevel``. Salt should be able to do the right thing. + :param _dont_call_warnings: This parameter is used just to get the + functionality until the actual error is to be + issued. When we're only after the date + checks to raise a ``RuntimeError``. + ''' + _strptime_fmt = '%Y%m%d' + if not isinstance(date, (six.string_types, datetime.date, datetime.datetime)): + raise RuntimeError( + 'The \'date\' argument should be passed as a \'datetime.date()\' or ' + '\'datetime.datetime()\' objects or as string parserable by ' + '\'datetime.datetime.strptime()\' with the following format \'{}\'.'.format( + _strptime_fmt + ) + ) + elif isinstance(date, six.text_type): + date = datetime.datetime.strptime(date, _strptime_fmt) + + # We're really not interested in the time + if isinstance(date, datetime.datetime): + date = date.date() + + if stacklevel is None: + # Attribute the warning to the calling function, not to warn_until_date() + stacklevel = 2 + + today = _get_utcnow_date() + if today >= date: + caller = inspect.getframeinfo(sys._getframe(stacklevel - 1)) + raise RuntimeError( + '{message} This warning(now exception) triggered on ' + 'filename \'{filename}\', line number {lineno}, is ' + 'supposed to be shown until {date}. Today is {today}. ' + 'Please remove the warning.'.format( + message=message.format( + date=date.isoformat(), + today=today.isoformat() + ), + filename=caller.filename, + lineno=caller.lineno, + date=date.isoformat(), + today=today.isoformat(), + ), ) - warnings.formatwarning = saved + + if _dont_call_warnings is False: + with _patched_format_warning(): + warnings.warn( + message.format( + date=date.isoformat(), + today=today.isoformat() + ), + category, + stacklevel=stacklevel + ) def kwargs_warn_until(kwargs, diff --git a/tests/unit/utils/test_versions.py b/tests/unit/utils/test_versions.py index 4790b921281a..745007caee97 100644 --- a/tests/unit/utils/test_versions.py +++ b/tests/unit/utils/test_versions.py @@ -12,6 +12,7 @@ from __future__ import absolute_import, print_function, unicode_literals import os import sys +import datetime import warnings # Import Salt Testing libs @@ -106,7 +107,7 @@ def test_spelling_version_name(self): names in the salt.utils.versions.warn_until call ''' salt_dir = integration.CODE_DIR - query = 'salt.utils.versions.warn_until' + query = 'salt.utils.versions.warn_until(' names = salt.version.SaltStackVersion.NAMES salt_dir += '/salt/' @@ -291,3 +292,67 @@ def raise_warning(**kwargs): r'0.17.0 is released. Current version is now 0.17.0. ' r'Please remove the warning.'): raise_warning(bar='baz', qux='quux', _version_info_=(0, 17)) # some kwargs + + def test_warn_until_date_warning_raised(self): + # We *always* want *all* warnings thrown on this module + warnings.filterwarnings('always', '', DeprecationWarning, __name__) + + fake_utcnow = datetime.date(2000, 1, 1) + + with patch('salt.utils.versions._get_utcnow_date', return_value=fake_utcnow): + + # Test warning with datetime.date instance + with warnings.catch_warnings(record=True) as recorded_warnings: + salt.utils.versions.warn_until_date(datetime.date(2000, 1, 2), 'Deprecation Message!') + self.assertEqual( + 'Deprecation Message!', six.text_type(recorded_warnings[0].message) + ) + + # Test warning with datetime.datetime instance + with warnings.catch_warnings(record=True) as recorded_warnings: + salt.utils.versions.warn_until_date(datetime.datetime(2000, 1, 2), 'Deprecation Message!') + self.assertEqual( + 'Deprecation Message!', six.text_type(recorded_warnings[0].message) + ) + + # Test warning with date as a string + with warnings.catch_warnings(record=True) as recorded_warnings: + salt.utils.versions.warn_until_date('20000102', 'Deprecation Message!') + self.assertEqual( + 'Deprecation Message!', six.text_type(recorded_warnings[0].message) + ) + + # the deprecation warning is not issued because we passed + # _dont_call_warning + with warnings.catch_warnings(record=True) as recorded_warnings: + salt.utils.versions.warn_until_date('20000102', 'Deprecation Message!', _dont_call_warnings=True) + self.assertEqual(0, len(recorded_warnings)) + + # Let's test for RuntimeError raise + with self.assertRaisesRegex( + RuntimeError, + r'Deprecation Message! This warning\(now exception\) triggered on ' + r'filename \'(.*)test_versions.py\', line number ([\d]+), is ' + r'supposed to be shown until ([\d-]+). Today is ([\d-]+). ' + r'Please remove the warning.'): + salt.utils.versions.warn_until_date('20000101', 'Deprecation Message!') + + # Even though we're calling warn_until_date, we pass _dont_call_warnings + # because we're only after the RuntimeError + with self.assertRaisesRegex( + RuntimeError, + r'Deprecation Message! This warning\(now exception\) triggered on ' + r'filename \'(.*)test_versions.py\', line number ([\d]+), is ' + r'supposed to be shown until ([\d-]+). Today is ([\d-]+). ' + r'Please remove the warning.'): + salt.utils.versions.warn_until_date('20000101', 'Deprecation Message!', _dont_call_warnings=True) + + def test_warn_until_date_bad_strptime_format(self): + # We *always* want *all* warnings thrown on this module + warnings.filterwarnings('always', '', DeprecationWarning, __name__) + + # Let's test for RuntimeError raise + with self.assertRaisesRegex( + ValueError, + 'time data \'0022\' does not match format \'%Y%m%d\''): + salt.utils.versions.warn_until_date('0022', 'Deprecation Message!') From e5cd1a26387ba2e91a9d4a597bd18c0e41a5470a Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Fri, 18 Oct 2019 09:53:46 +0100 Subject: [PATCH 2/2] We don't have to patch `warnings.formatwarning` under Py3 --- salt/utils/versions.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/salt/utils/versions.py b/salt/utils/versions.py index 9334c9d7c034..5f63c42ec7eb 100644 --- a/salt/utils/versions.py +++ b/salt/utils/versions.py @@ -90,10 +90,14 @@ def _format_warning(message, category, filename, lineno, line=None): @contextlib.contextmanager def _patched_format_warning(): - saved = warnings.formatwarning - warnings.formatwarning = _format_warning - yield - warnings.formatwarning = saved + if six.PY2: + saved = warnings.formatwarning + warnings.formatwarning = _format_warning + yield + warnings.formatwarning = saved + else: + # Under Py3 we no longer have to patch warnings.formatwarning + yield def warn_until(version,