bpo-40275: Add warnings_helper submodule in test.support (GH-20797)

This commit is contained in:
Hai Shi 2020-06-11 23:36:06 +08:00 committed by GitHub
parent bdfe9b633a
commit 10e6506aa8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 288 additions and 267 deletions

View File

@ -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.

View File

@ -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, '<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 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:

View File

@ -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, '<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])