diff options
author | Łukasz Langa <lukasz@langa.pl> | 2021-08-16 20:47:08 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-16 20:47:08 (GMT) |
commit | c7c4cbc58e18ef5a6f4f377b1ece0a84a54335a7 (patch) | |
tree | 0f767566b9a2a236cbc69a85d9e82733321510d7 | |
parent | 43bab0537ceb6e2ca3597f8f3a3c79733b897434 (diff) | |
download | cpython-c7c4cbc58e18ef5a6f4f377b1ece0a84a54335a7.zip cpython-c7c4cbc58e18ef5a6f4f377b1ece0a84a54335a7.tar.gz cpython-c7c4cbc58e18ef5a6f4f377b1ece0a84a54335a7.tar.bz2 |
[3.9] bpo-44852: Support ignoring specific DeprecationWarnings wholesale in regrtest (GH-27634) (GH-27785)
(cherry picked from commit a0a6d39295a30434b088f4b66439bf5ea21a3e4e)
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
-rw-r--r-- | Lib/test/support/__init__.py | 28 | ||||
-rw-r--r-- | Lib/test/support/warnings_helper.py | 199 | ||||
-rw-r--r-- | Lib/test/test_support.py | 40 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Tests/2021-08-06-18-36-04.bpo-44852.sUL8YX.rst | 2 |
4 files changed, 269 insertions, 0 deletions
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index ff7db991..39dea88 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -3241,3 +3241,31 @@ def infinite_recursion(max_depth=75): yield finally: sys.setrecursionlimit(original_depth) + +def ignore_deprecations_from(module: str, *, like: str) -> object: + token = object() + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + module=module, + message=like + fr"(?#support{id(token)})", + ) + return token + +def clear_ignored_deprecations(*tokens: object) -> None: + if not tokens: + raise ValueError("Provide token or tokens returned by ignore_deprecations_from") + + new_filters = [] + for action, message, category, module, lineno in warnings.filters: + if action == "ignore" and category is DeprecationWarning: + if isinstance(message, re.Pattern): + message = message.pattern + if tokens: + endswith = tuple(rf"(?#support{id(token)})" for token in tokens) + if message.endswith(endswith): + continue + new_filters.append((action, message, category, module, lineno)) + if warnings.filters != new_filters: + warnings.filters[:] = new_filters + warnings._filters_mutated() diff --git a/Lib/test/support/warnings_helper.py b/Lib/test/support/warnings_helper.py new file mode 100644 index 0000000..a024fbe --- /dev/null +++ b/Lib/test/support/warnings_helper.py @@ -0,0 +1,199 @@ +import contextlib +import functools +import re +import sys +import warnings + + +def check_syntax_warning(testcase, statement, errtext='', + *, lineno=1, offset=None): + # Test also that a warning is emitted only once. + from test.support import check_syntax_error + with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter('always', SyntaxWarning) + compile(statement, '<testcase>', 'exec') + testcase.assertEqual(len(warns), 1, warns) + + warn, = warns + testcase.assertTrue(issubclass(warn.category, SyntaxWarning), + warn.category) + if errtext: + testcase.assertRegex(str(warn.message), errtext) + testcase.assertEqual(warn.filename, '<testcase>') + testcase.assertIsNotNone(warn.lineno) + if lineno is not None: + testcase.assertEqual(warn.lineno, lineno) + + # SyntaxWarning should be converted to SyntaxError when raised, + # since the latter contains more information and provides better + # error report. + with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter('error', SyntaxWarning) + check_syntax_error(testcase, statement, errtext, + lineno=lineno, offset=offset) + # No warnings are leaked when a SyntaxError is raised. + testcase.assertEqual(warns, []) + + +def ignore_warnings(*, category): + """Decorator to suppress deprecation warnings. + + Use of context managers to hide warnings make diffs + more noisy and tools like 'git blame' less useful. + """ + def decorator(test): + @functools.wraps(test) + def wrapper(self, *args, **kwargs): + with warnings.catch_warnings(): + warnings.simplefilter('ignore', category=category) + return test(self, *args, **kwargs) + return wrapper + return decorator + + +class WarningsRecorder(object): + """Convenience wrapper for the warnings list returned on + entry to the warnings.catch_warnings() context manager. + """ + def __init__(self, warnings_list): + self._warnings = warnings_list + self._last = 0 + + def __getattr__(self, attr): + if len(self._warnings) > self._last: + return getattr(self._warnings[-1], attr) + elif attr in warnings.WarningMessage._WARNING_DETAILS: + return None + raise AttributeError("%r has no attribute %r" % (self, attr)) + + @property + def warnings(self): + return self._warnings[self._last:] + + def reset(self): + self._last = len(self._warnings) + + +@contextlib.contextmanager +def check_warnings(*filters, **kwargs): + """Context manager to silence warnings. + + Accept 2-tuples as positional arguments: + ("message regexp", WarningCategory) + + Optional argument: + - if 'quiet' is True, it does not fail if a filter catches nothing + (default True without argument, + default False if some filters are defined) + + Without argument, it defaults to: + check_warnings(("", Warning), quiet=True) + """ + quiet = kwargs.get('quiet') + if not filters: + filters = (("", Warning),) + # Preserve backward compatibility + if quiet is None: + quiet = True + return _filterwarnings(filters, quiet) + + +@contextlib.contextmanager +def check_no_warnings(testcase, message='', category=Warning, force_gc=False): + """Context manager to check that no warnings are emitted. + + This context manager enables a given warning within its scope + and checks that no warnings are emitted even with that warning + enabled. + + If force_gc is True, a garbage collection is attempted before checking + for warnings. This may help to catch warnings emitted when objects + are deleted, such as ResourceWarning. + + Other keyword arguments are passed to warnings.filterwarnings(). + """ + from test.support import gc_collect + with warnings.catch_warnings(record=True) as warns: + warnings.filterwarnings('always', + message=message, + category=category) + yield + if force_gc: + gc_collect() + testcase.assertEqual(warns, []) + + +@contextlib.contextmanager +def check_no_resource_warning(testcase): + """Context manager to check that no ResourceWarning is emitted. + + Usage: + + with check_no_resource_warning(self): + f = open(...) + ... + del f + + You must remove the object which may emit ResourceWarning before + the end of the context manager. + """ + with check_no_warnings(testcase, category=ResourceWarning, force_gc=True): + yield + + +def _filterwarnings(filters, quiet=False): + """Catch the warnings, then check if all the expected + warnings have been raised and re-raise unexpected warnings. + If 'quiet' is True, only re-raise the unexpected warnings. + """ + # Clear the warning registry of the calling module + # in order to re-raise the warnings. + frame = sys._getframe(2) + registry = frame.f_globals.get('__warningregistry__') + if registry: + registry.clear() + with warnings.catch_warnings(record=True) as w: + # Set filter "always" to record all warnings. Because + # test_warnings swap the module, we need to look up in + # the sys.modules dictionary. + sys.modules['warnings'].simplefilter("always") + yield WarningsRecorder(w) + # Filter the recorded warnings + reraise = list(w) + missing = [] + for msg, cat in filters: + seen = False + for w in reraise[:]: + warning = w.message + # Filter out the matching messages + if (re.match(msg, str(warning), re.I) and + issubclass(warning.__class__, cat)): + seen = True + reraise.remove(w) + if not seen and not quiet: + # This filter caught nothing + missing.append((msg, cat.__name__)) + if reraise: + raise AssertionError("unhandled warning %s" % reraise[0]) + if missing: + raise AssertionError("filter (%r, %s) did not catch any warning" % + missing[0]) + + +@contextlib.contextmanager +def save_restore_warnings_filters(): + old_filters = warnings.filters[:] + try: + yield + finally: + warnings.filters[:] = old_filters + + +def _warn_about_deprecation(): + warnings.warn( + "This is used in test_support test to ensure" + " support.ignore_deprecations_from() works as expected." + " You should not be seeing this.", + DeprecationWarning, + stacklevel=0, + ) diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index b5a16f9..60a7741 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -11,6 +11,8 @@ import tempfile import textwrap import time import unittest +import warnings + from test import support from test.support import script_helper from test.support import socket_helper @@ -19,6 +21,33 @@ TESTFN = support.TESTFN class TestSupport(unittest.TestCase): + @classmethod + def setUpClass(cls): + orig_filter_len = len(warnings.filters) + cls._warnings_helper_token = support.ignore_deprecations_from( + "test.test_support", like=".*used in test_support.*" + ) + cls._test_support_token = support.ignore_deprecations_from( + "test.test_support", like=".*You should NOT be seeing this.*" + ) + assert len(warnings.filters) == orig_filter_len + 2 + + @classmethod + def tearDownClass(cls): + orig_filter_len = len(warnings.filters) + support.clear_ignored_deprecations( + cls._warnings_helper_token, + cls._test_support_token, + ) + assert len(warnings.filters) == orig_filter_len - 2 + + def test_ignored_deprecations_are_silent(self): + """Test support.ignore_deprecations_from() silences warnings""" + with warnings.catch_warnings(record=True) as warning_objs: + _warn_about_deprecation() + warnings.warn("You should NOT be seeing this.", DeprecationWarning) + messages = [str(w.message) for w in warning_objs] + self.assertEqual(len(messages), 0, messages) def test_import_module(self): support.import_module("ftplib") @@ -676,6 +705,17 @@ class TestSupport(unittest.TestCase): # SuppressCrashReport +def _warn_about_deprecation(): + # In 3.10+ this lives in test.support.warnings_helper + warnings.warn( + "This is used in test_support test to ensure" + " support.ignore_deprecations_from() works as expected." + " You should not be seeing this.", + DeprecationWarning, + stacklevel=0, + ) + + def test_main(): tests = [TestSupport] support.run_unittest(*tests) diff --git a/Misc/NEWS.d/next/Tests/2021-08-06-18-36-04.bpo-44852.sUL8YX.rst b/Misc/NEWS.d/next/Tests/2021-08-06-18-36-04.bpo-44852.sUL8YX.rst new file mode 100644 index 0000000..41b5c2f --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2021-08-06-18-36-04.bpo-44852.sUL8YX.rst @@ -0,0 +1,2 @@ +Add ability to wholesale silence DeprecationWarnings while running the +regression test suite. |