From 10e6506aa8261aacc89b49e629ae1c927fa5151c Mon Sep 17 00:00:00 2001 From: Hai Shi Date: Thu, 11 Jun 2020 23:36:06 +0800 Subject: [PATCH] bpo-40275: Add warnings_helper submodule in test.support (GH-20797) --- Doc/library/test.rst | 195 +++++++++++++++------------- Lib/test/support/__init__.py | 180 +------------------------ Lib/test/support/warnings_helper.py | 180 +++++++++++++++++++++++++ 3 files changed, 288 insertions(+), 267 deletions(-) create mode 100644 Lib/test/support/warnings_helper.py diff --git a/Doc/library/test.rst b/Doc/library/test.rst index a18197aed3f..843201885ad 100644 --- a/Doc/library/test.rst +++ b/Doc/library/test.rst @@ -497,79 +497,6 @@ The :mod:`test.support` module defines the following functions: check_impl_detail(cpython=False) # Everywhere except CPython. -.. function:: check_warnings(\*filters, quiet=True) - - A convenience wrapper for :func:`warnings.catch_warnings()` that makes it - easier to test that a warning was correctly raised. It is approximately - equivalent to calling ``warnings.catch_warnings(record=True)`` with - :meth:`warnings.simplefilter` set to ``always`` and with the option to - automatically validate the results that are recorded. - - ``check_warnings`` accepts 2-tuples of the form ``("message regexp", - WarningCategory)`` as positional arguments. If one or more *filters* are - provided, or if the optional keyword argument *quiet* is ``False``, - it checks to make sure the warnings are as expected: each specified filter - must match at least one of the warnings raised by the enclosed code or the - test fails, and if any warnings are raised that do not match any of the - specified filters the test fails. To disable the first of these checks, - set *quiet* to ``True``. - - If no arguments are specified, it defaults to:: - - check_warnings(("", Warning), quiet=True) - - In this case all warnings are caught and no errors are raised. - - On entry to the context manager, a :class:`WarningRecorder` instance is - returned. The underlying warnings list from - :func:`~warnings.catch_warnings` is available via the recorder object's - :attr:`warnings` attribute. As a convenience, the attributes of the object - representing the most recent warning can also be accessed directly through - the recorder object (see example below). If no warning has been raised, - then any of the attributes that would otherwise be expected on an object - representing a warning will return ``None``. - - The recorder object also has a :meth:`reset` method, which clears the - warnings list. - - The context manager is designed to be used like this:: - - with check_warnings(("assertion is always true", SyntaxWarning), - ("", UserWarning)): - exec('assert(False, "Hey!")') - warnings.warn(UserWarning("Hide me!")) - - In this case if either warning was not raised, or some other warning was - raised, :func:`check_warnings` would raise an error. - - When a test needs to look more deeply into the warnings, rather than - just checking whether or not they occurred, code like this can be used:: - - with check_warnings(quiet=True) as w: - warnings.warn("foo") - assert str(w.args[0]) == "foo" - warnings.warn("bar") - assert str(w.args[0]) == "bar" - assert str(w.warnings[0].args[0]) == "foo" - assert str(w.warnings[1].args[0]) == "bar" - w.reset() - assert len(w.warnings) == 0 - - - Here all warnings will be caught, and the test code tests the captured - warnings directly. - - .. versionchanged:: 3.2 - New optional arguments *filters* and *quiet*. - - -.. function:: check_no_resource_warning(testcase) - - Context manager to check that no :exc:`ResourceWarning` was raised. You - must remove the object which may emit :exc:`ResourceWarning` before the - end of the context manager. - - .. function:: set_memlimit(limit) Set the values for :data:`max_memuse` and :data:`real_max_memuse` for big @@ -851,20 +778,6 @@ The :mod:`test.support` module defines the following functions: the offset of the exception. -.. function:: check_syntax_warning(testcase, statement, errtext='', *, lineno=1, offset=None) - - Test for syntax warning in *statement* by attempting to compile *statement*. - Test also that the :exc:`SyntaxWarning` is emitted only once, and that it - will be converted to a :exc:`SyntaxError` when turned into error. - *testcase* is the :mod:`unittest` instance for the test. *errtext* is the - regular expression which should match the string representation of the - emitted :exc:`SyntaxWarning` and raised :exc:`SyntaxError`. If *lineno* - is not ``None``, compares to the line of the warning and exception. - If *offset* is not ``None``, compares to the offset of the exception. - - .. versionadded:: 3.8 - - .. function:: open_urlresource(url, *args, **kw) Open *url*. If open fails, raises :exc:`TestFailed`. @@ -1051,12 +964,6 @@ The :mod:`test.support` module defines the following classes: Try to match a single stored value (*dv*) with a supplied value (*v*). -.. class:: WarningsRecorder() - - Class used to record warnings for unit tests. See documentation of - :func:`check_warnings` above for more details. - - .. class:: BasicTestRunner() .. method:: run(test) @@ -1659,3 +1566,105 @@ The :mod:`test.support.import_helper` module provides support for import tests. will be reverted at the end of the block. +:mod:`test.support.warnings_helper` --- Utilities for warnings tests +==================================================================== + +.. module:: test.support.warnings_helper + :synopsis: Support for warnings tests. + +The :mod:`test.support.warnings_helper` module provides support for warnings tests. + +.. versionadded:: 3.10 + + +.. function:: check_no_resource_warning(testcase) + + Context manager to check that no :exc:`ResourceWarning` was raised. You + must remove the object which may emit :exc:`ResourceWarning` before the + end of the context manager. + + +.. function:: check_syntax_warning(testcase, statement, errtext='', *, lineno=1, offset=None) + + Test for syntax warning in *statement* by attempting to compile *statement*. + Test also that the :exc:`SyntaxWarning` is emitted only once, and that it + will be converted to a :exc:`SyntaxError` when turned into error. + *testcase* is the :mod:`unittest` instance for the test. *errtext* is the + regular expression which should match the string representation of the + emitted :exc:`SyntaxWarning` and raised :exc:`SyntaxError`. If *lineno* + is not ``None``, compares to the line of the warning and exception. + If *offset* is not ``None``, compares to the offset of the exception. + + .. versionadded:: 3.8 + + +.. function:: check_warnings(\*filters, quiet=True) + + A convenience wrapper for :func:`warnings.catch_warnings()` that makes it + easier to test that a warning was correctly raised. It is approximately + equivalent to calling ``warnings.catch_warnings(record=True)`` with + :meth:`warnings.simplefilter` set to ``always`` and with the option to + automatically validate the results that are recorded. + + ``check_warnings`` accepts 2-tuples of the form ``("message regexp", + WarningCategory)`` as positional arguments. If one or more *filters* are + provided, or if the optional keyword argument *quiet* is ``False``, + it checks to make sure the warnings are as expected: each specified filter + must match at least one of the warnings raised by the enclosed code or the + test fails, and if any warnings are raised that do not match any of the + specified filters the test fails. To disable the first of these checks, + set *quiet* to ``True``. + + If no arguments are specified, it defaults to:: + + check_warnings(("", Warning), quiet=True) + + In this case all warnings are caught and no errors are raised. + + On entry to the context manager, a :class:`WarningRecorder` instance is + returned. The underlying warnings list from + :func:`~warnings.catch_warnings` is available via the recorder object's + :attr:`warnings` attribute. As a convenience, the attributes of the object + representing the most recent warning can also be accessed directly through + the recorder object (see example below). If no warning has been raised, + then any of the attributes that would otherwise be expected on an object + representing a warning will return ``None``. + + The recorder object also has a :meth:`reset` method, which clears the + warnings list. + + The context manager is designed to be used like this:: + + with check_warnings(("assertion is always true", SyntaxWarning), + ("", UserWarning)): + exec('assert(False, "Hey!")') + warnings.warn(UserWarning("Hide me!")) + + In this case if either warning was not raised, or some other warning was + raised, :func:`check_warnings` would raise an error. + + When a test needs to look more deeply into the warnings, rather than + just checking whether or not they occurred, code like this can be used:: + + with check_warnings(quiet=True) as w: + warnings.warn("foo") + assert str(w.args[0]) == "foo" + warnings.warn("bar") + assert str(w.args[0]) == "bar" + assert str(w.warnings[0].args[0]) == "foo" + assert str(w.warnings[1].args[0]) == "bar" + w.reset() + assert len(w.warnings) == 0 + + + Here all warnings will be caught, and the test code tests the captured + warnings directly. + + .. versionchanged:: 3.2 + New optional arguments *filters* and *quiet*. + + +.. class:: WarningsRecorder() + + Class used to record warnings for unit tests. See documentation of + :func:`check_warnings` above for more details. diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 1ac65533a7b..fa54ebe5620 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -14,7 +14,6 @@ import sysconfig import time import types import unittest -import warnings from .import_helper import ( CleanImport, DirsOnSysPath, _ignore_deprecated_imports, @@ -30,6 +29,10 @@ from .os_helper import ( rmtree, skip_unless_symlink, skip_unless_xattr, temp_cwd, temp_dir, temp_umask, unlink, EnvironmentVarGuard, FakePath, _longpath) +from .warnings_helper import ( + WarningsRecorder, _filterwarnings, + check_no_resource_warning, check_no_warnings, + check_syntax_warning, check_warnings, ignore_warnings) from .testresult import get_test_runner @@ -45,7 +48,7 @@ __all__ = [ # unittest "is_resource_enabled", "requires", "requires_freebsd_version", "requires_linux_version", "requires_mac_ver", - "check_syntax_error", "check_syntax_warning", + "check_syntax_error", "TransientResource", "time_out", "socket_peer_reset", "ioerror_peer_reset", "BasicTestRunner", "run_unittest", "run_doctest", "requires_gzip", "requires_bz2", "requires_lzma", @@ -53,7 +56,6 @@ __all__ = [ "requires_IEEE_754", "requires_zlib", "anticipate_failure", "load_package_tests", "detect_api_mismatch", "check__all__", "skip_if_buggy_ucrt_strfptime", - "ignore_warnings", # sys "is_jython", "is_android", "check_impl_detail", "unix_shell", "setswitchinterval", @@ -62,7 +64,6 @@ __all__ = [ # processes "reap_children", # miscellaneous - "check_warnings", "check_no_resource_warning", "check_no_warnings", "run_with_locale", "swap_item", "findfile", "swap_attr", "Matcher", "set_memlimit", "SuppressCrashReport", "sortdict", "run_with_tz", "PGO", "missing_compiler_executable", @@ -128,22 +129,6 @@ class ResourceDenied(unittest.SkipTest): and unexpected skips. """ -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 - - def anticipate_failure(condition): """Decorator to mark a test that is known to be broken in some cases @@ -511,32 +496,6 @@ def check_syntax_error(testcase, statement, errtext='', *, lineno=None, offset=N if offset is not None: testcase.assertEqual(err.offset, offset) -def check_syntax_warning(testcase, statement, errtext='', *, lineno=1, offset=None): - # Test also that a warning is emitted only once. - with warnings.catch_warnings(record=True) as warns: - warnings.simplefilter('always', SyntaxWarning) - compile(statement, '', '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.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 open_urlresource(url, *args, **kw): import urllib.request, urllib.parse @@ -592,134 +551,6 @@ def open_urlresource(url, *args, **kw): raise TestFailed('invalid resource %r' % fn) -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) - - -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 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(). - """ - 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 - - class TransientResource(object): """Raise ResourceDenied if an exception is raised while the context manager @@ -978,6 +809,7 @@ class _MemoryWatchdog: self.started = False def start(self): + import warnings try: f = open(self.procfile, 'r') except OSError as e: diff --git a/Lib/test/support/warnings_helper.py b/Lib/test/support/warnings_helper.py new file mode 100644 index 00000000000..c9f9045405b --- /dev/null +++ b/Lib/test/support/warnings_helper.py @@ -0,0 +1,180 @@ +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, '', '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.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])