[Python-checkins] bpo-40275: Add warnings_helper submodule in test.support (GH-20797)

Hai Shi webhook-mailer at python.org
Thu Jun 11 11:36:11 EDT 2020


https://github.com/python/cpython/commit/10e6506aa8261aacc89b49e629ae1c927fa5151c
commit: 10e6506aa8261aacc89b49e629ae1c927fa5151c
branch: master
author: Hai Shi <shihai1992 at gmail.com>
committer: GitHub <noreply at github.com>
date: 2020-06-11T17:36:06+02:00
summary:

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

files:
A Lib/test/support/warnings_helper.py
M Doc/library/test.rst
M Lib/test/support/__init__.py

diff --git a/Doc/library/test.rst b/Doc/library/test.rst
index a18197aed3f4a..843201885ad24 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 1ac65533a7b54..fa54ebe5620f4 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -14,7 +14,6 @@
 import time
 import types
 import unittest
-import warnings
 
 from .import_helper import (
     CleanImport, DirsOnSysPath, _ignore_deprecated_imports,
@@ -30,6 +29,10 @@
     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 @@
     # 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 @@
     "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 @@
     # 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 check_valid_file(fn):
     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])
-
-
- at 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)
-
-
- at 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, [])
-
-
- at 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 @@ def __init__(self):
         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 0000000000000..c9f9045405b80
--- /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, '<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)
+
+
+ at 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)
+
+
+ at 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, [])
+
+
+ at 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])



More information about the Python-checkins mailing list