Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[master] Allow deprecating by date #55047

Merged
merged 2 commits into from
Oct 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 109 additions & 19 deletions salt/utils/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -75,6 +78,28 @@ 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():
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,
message,
category=DeprecationWarning,
Expand Down Expand Up @@ -125,7 +150,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 '
Expand All @@ -140,26 +164,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
)


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
)
saved = warnings.formatwarning
warnings.formatwarning = _formatwarning
warnings.warn(
message.format(version=version.formatted_version),
category,
stacklevel=stacklevel
)
warnings.formatwarning = saved
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(),
),
)

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,
Expand Down
67 changes: 66 additions & 1 deletion tests/unit/utils/test_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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/'
Expand Down Expand Up @@ -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!')