]> granicus.if.org Git - python/commitdiff
forward port r66386
authorBenjamin Peterson <benjamin@python.org>
Thu, 16 Oct 2008 23:24:44 +0000 (23:24 +0000)
committerBenjamin Peterson <benjamin@python.org>
Thu, 16 Oct 2008 23:24:44 +0000 (23:24 +0000)
Doc/library/test.rst
Doc/library/warnings.rst
Lib/test/support.py
Lib/test/test_import.py
Lib/test/test_structmembers.py
Lib/test/test_sundry.py
Lib/test/test_threading.py
Lib/test/test_warnings.py
Lib/warnings.py

index d7a9760a0d1cf4d8b177ae72fcafb091979fdc38..7bb1e016d2da88f3ac215b4a2ef9397db73129a7 100644 (file)
@@ -278,18 +278,26 @@ The :mod:`test.support` module defines the following functions:
    This will run all tests defined in the named module.
 
 
-.. function:: catch_warning(module=warnings, record=True)
+.. function:: check_warnings()
 
-   Return a context manager that guards the warnings filter from being
-   permanently changed and optionally alters the :func:`showwarning`
-   function to record the details of any warnings that are issued in the
-   managed context. Attributes of the most recent warning are saved
-   directly on the context manager, while details of previous warnings
-   can be retrieved from the ``warnings`` list.
+   A convenience wrapper for ``warnings.catch_warnings()`` that makes
+   it easier to test that a warning was correctly raised with a single
+   assertion. It is approximately equivalent to calling
+   ``warnings.catch_warnings(record=True)``.
+
+   The main difference is that on entry to the context manager, a
+   :class:`WarningRecorder` instance is returned instead of a simple list.
+   The underlying warnings list is available via the recorder object's
+   :attr:`warnings` attribute, while the attributes of the last raised
+   warning are also accessible directly on the object. If no warning has
+   been raised, then the latter attributes will all be :const:`None`.
+
+   A :meth:`reset` method is also provided on the recorder object. This
+   method simply clears the warning list.
 
    The context manager is used like this::
 
-      with catch_warning() as w:
+      with check_warnings() as w:
           warnings.simplefilter("always")
           warnings.warn("foo")
           assert str(w.message) == "foo"
@@ -297,15 +305,9 @@ The :mod:`test.support` module defines the following functions:
           assert str(w.message) == "bar"
           assert str(w.warnings[0].message) == "foo"
           assert str(w.warnings[1].message) == "bar"
+          w.reset()
+          assert len(w.warnings) == 0
 
-   By default, the real :mod:`warnings` module is affected - the ability
-   to select a different module is provided for the benefit of the
-   :mod:`warnings` module's  own unit tests.
-   The ``record`` argument specifies whether or not the :func:`showwarning`
-   function is replaced. Note that recording the warnings in this fashion
-   also prevents them from being written to sys.stderr. If set to ``False``,
-   the standard handling of warning messages is left in place (however, the
-   original handling is still restored at the end of the block).
 
 .. function:: captured_stdout()
 
@@ -346,4 +348,10 @@ The :mod:`test.support` module defines the following classes:
 
    Temporarily unset the environment variable ``envvar``.
 
+.. class:: WarningsRecorder()
+
+   Class used to record warnings for unit tests. See documentation of
+   :func:`check_warnings` above for more details.
+
+   .. versionadded:: 2.6
 
index f9d01b25b1f45a3a04e1c57b21381f7e4c144887..6a1b0df2f25776d0ce619ea46ad20d273028c2b9 100644 (file)
@@ -165,9 +165,9 @@ ImportWarning can also be enabled explicitly in Python code using::
 Temporarily Suppressing Warnings
 --------------------------------
 
-If you are using code that you know will raise a warning, such some deprecated
-function, but do not want to see the warning, then suppress the warning using
-the :class:`catch_warnings` context manager::
+If you are using code that you know will raise a warning, such as a deprecated
+function, but do not want to see the warning, then it is possible to suppress
+the warning using the :class:`catch_warnings` context manager::
 
     import warnings
 
@@ -218,7 +218,15 @@ the warning has been cleared.
 Once the context manager exits, the warnings filter is restored to its state
 when the context was entered. This prevents tests from changing the warnings
 filter in unexpected ways between tests and leading to indeterminate test
-results.
+results. The :func:`showwarning` function in the module is also restored to
+its original value.
+
+When testing multiple operations that raise the same kind of warning, it
+is important to test them in a manner that confirms each operation is raising
+a new warning (e.g. set warnings to be raised as exceptions and check the
+operations raise exceptions, check that the length of the warning list
+continues to increase after each operation, or else delete the previous
+entries from the warnings list before each new operation).
 
 
 .. _warning-functions:
@@ -314,20 +322,20 @@ Available Context Managers
 
 .. class:: catch_warnings([\*, record=False, module=None])
 
-    A context manager that copies and, upon exit, restores the warnings filter.
-    If the *record* argument is False (the default) the context manager returns
-    :class:`None`. If *record* is true, a list is returned that is populated
-    with objects as seen by a custom :func:`showwarning` function (which also
-    suppresses output to ``sys.stdout``). Each object has attributes with the
-    same names as the arguments to :func:`showwarning`.
+    A context manager that copies and, upon exit, restores the warnings filter
+    and the :func:`showwarning` function.
+    If the *record* argument is :const:`False` (the default) the context manager
+    returns :class:`None` on entry. If *record* is :const:`True`, a list is
+    returned that is progressively populated with objects as seen by a custom
+    :func:`showwarning` function (which also suppresses output to ``sys.stdout``).
+    Each object in the list has attributes with the same names as the arguments to
+    :func:`showwarning`.
 
     The *module* argument takes a module that will be used instead of the
     module returned when you import :mod:`warnings` whose filter will be
-    protected. This arguments exists primarily for testing the :mod:`warnings`
+    protected. This argument exists primarily for testing the :mod:`warnings`
     module itself.
 
-    .. versionadded:: 2.6
-
     .. versionchanged:: 3.0
 
        Constructor arguments turned into keyword-only arguments.
index a23c99bf4b9a70b4186f1af189f382f7514fc15f..5f8b1ba1cdd57a369d73c90ac3d795c8f31a8b19 100644 (file)
@@ -19,7 +19,7 @@ __all__ = ["Error", "TestFailed", "TestSkipped", "ResourceDenied", "import_modul
            "is_resource_enabled", "requires", "find_unused_port", "bind_port",
            "fcmp", "is_jython", "TESTFN", "HOST", "FUZZ", "findfile", "verify",
            "vereq", "sortdict", "check_syntax_error", "open_urlresource",
-           "catch_warning", "CleanImport", "EnvironmentVarGuard",
+           "check_warnings", "CleanImport", "EnvironmentVarGuard",
            "TransientResource", "captured_output", "captured_stdout",
            "TransientResource", "transient_internet", "run_with_locale",
            "set_memlimit", "bigmemtest", "bigaddrspacetest", "BasicTestRunner",
@@ -53,7 +53,7 @@ class ResourceDenied(TestSkipped):
 def import_module(name, deprecated=False):
     """Import the module to be tested, raising TestSkipped if it is not
     available."""
-    with catch_warning(record=False):
+    with warnings.catch_warnings():
         if deprecated:
             warnings.filterwarnings("ignore", ".+ (module|package)",
                                     DeprecationWarning)
@@ -368,17 +368,27 @@ def open_urlresource(url, *args, **kw):
     return open(fn, *args, **kw)
 
 
-def catch_warning(module=warnings, record=True):
-    """Guard the warnings filter from being permanently changed and
-    optionally record the details of any warnings that are issued.
+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
 
-    Use like this:
+    def __getattr__(self, attr):
+        if self.warnings:
+            return getattr(self.warnings[-1], attr)
+        elif attr in warnings.WarningMessage._WARNING_DETAILS:
+            return None
+        raise AttributeError("%r has no attribute %r" % (self, attr))
 
-        with catch_warning() as w:
-            warnings.warn("foo")
-            assert str(w.message) == "foo"
-    """
-    return warnings.catch_warnings(record=record, module=module)
+    def reset(self):
+        del self.warnings[:]
+
+@contextlib.contextmanager
+def check_warnings():
+    with warnings.catch_warnings(record=True) as w:
+        yield WarningsRecorder(w)
 
 
 class CleanImport(object):
index 0dbbbf04c50ca921bf2f255dc07c4b8baa30fdf1..6598d4ee5e73ddab61ae8097e14303fef8fbc5de 100644 (file)
@@ -266,21 +266,24 @@ class RelativeImport(unittest.TestCase):
         self.assertTrue(hasattr(relimport, "RelativeImport"))
 
     def test_issue3221(self):
+        # Note for mergers: the 'absolute' tests from the 2.x branch
+        # are missing in Py3k because implicit relative imports are
+        # a thing of the past
         def check_relative():
             exec("from . import relimport", ns)
-        # Check both OK with __package__ and __name__ correct
+        # Check relative import OK with __package__ and __name__ correct
         ns = dict(__package__='test', __name__='test.notarealmodule')
         check_relative()
-        # Check both OK with only __name__ wrong
+        # Check relative import OK with only __name__ wrong
         ns = dict(__package__='test', __name__='notarealpkg.notarealmodule')
         check_relative()
-        # Check relative fails with only __package__ wrong
+        # Check relative import fails with only __package__ wrong
         ns = dict(__package__='foo', __name__='test.notarealmodule')
         self.assertRaises(SystemError, check_relative)
-        # Check relative fails with __package__ and __name__ wrong
+        # Check relative import fails with __package__ and __name__ wrong
         ns = dict(__package__='foo', __name__='notarealpkg.notarealmodule')
         self.assertRaises(SystemError, check_relative)
-        # Check both fail with package set to a non-string
+        # Check relative import fails with package set to a non-string
         ns = dict(__package__=object())
         self.assertRaises(ValueError, check_relative)
 
index 878223a38fa7e8b721ea965e72aabfc92fe19f20..93e040cdc85e5cbd56bbdecc1093d49f56ffe594 100644 (file)
@@ -66,35 +66,35 @@ class ReadWriteTests(unittest.TestCase):
 
 class TestWarnings(unittest.TestCase):
     def has_warned(self, w):
-        self.assertEqual(w[-1].category, RuntimeWarning)
+        self.assertEqual(w.category, RuntimeWarning)
 
     def test_byte_max(self):
-        with warnings.catch_warnings(record=True) as w:
+        with support.check_warnings() as w:
             ts.T_BYTE = CHAR_MAX+1
             self.has_warned(w)
 
     def test_byte_min(self):
-        with warnings.catch_warnings(record=True) as w:
+        with support.check_warnings() as w:
             ts.T_BYTE = CHAR_MIN-1
             self.has_warned(w)
 
     def test_ubyte_max(self):
-        with warnings.catch_warnings(record=True) as w:
+        with support.check_warnings() as w:
             ts.T_UBYTE = UCHAR_MAX+1
             self.has_warned(w)
 
     def test_short_max(self):
-        with warnings.catch_warnings(record=True) as w:
+        with support.check_warnings() as w:
             ts.T_SHORT = SHRT_MAX+1
             self.has_warned(w)
 
     def test_short_min(self):
-        with warnings.catch_warnings(record=True) as w:
+        with support.check_warnings() as w:
             ts.T_SHORT = SHRT_MIN-1
             self.has_warned(w)
 
     def test_ushort_max(self):
-        with warnings.catch_warnings(record=True) as w:
+        with support.check_warnings() as w:
             ts.T_USHORT = USHRT_MAX+1
             self.has_warned(w)
 
index 6b425c6fa546278481551fae9753eac73e7a67e7..49813a32d43b14f33bda38dccc0ea3892a36eef6 100644 (file)
@@ -7,7 +7,7 @@ import warnings
 
 class TestUntestedModules(unittest.TestCase):
     def test_at_least_import_untested_modules(self):
-        with warnings.catch_warnings(record=True):
+        with warnings.catch_warnings():
             import aifc
             import bdb
             import cgitb
index f26e7bb4e72f011a1ebadfaf81161684badbe280..956672d43e17096863b49b9436d5b57d3391d71d 100644 (file)
@@ -1,7 +1,7 @@
 # Very rudimentary test of threading module
 
 import test.support
-from test.support import verbose, catch_warning
+from test.support import verbose
 import random
 import re
 import sys
index 199e6fca3d96e3948d3cbbff046f9f2d0758275a..4b6feb37cb9102c6e86baa5de8be555a75a8c67e 100644 (file)
@@ -214,7 +214,8 @@ class WarnTests(unittest.TestCase):
     def test_warn_nonstandard_types(self):
         # warn() should handle non-standard types without issue.
         for ob in (Warning, None, 42):
-            with support.catch_warning(self.module) as w:
+            with original_warnings.catch_warnings(record=True,
+                    module=self.module) as w:
                 self.module.warn(ob)
                 # Don't directly compare objects since
                 # ``Warning() != Warning()``.
@@ -526,19 +527,23 @@ class CatchWarningTests(BaseTest):
         wmod = self.module
         orig_filters = wmod.filters
         orig_showwarning = wmod.showwarning
-        with support.catch_warning(module=wmod):
+        # Ensure both showwarning and filters are restored when recording
+        with wmod.catch_warnings(module=wmod, record=True):
             wmod.filters = wmod.showwarning = object()
         self.assert_(wmod.filters is orig_filters)
         self.assert_(wmod.showwarning is orig_showwarning)
-        with support.catch_warning(module=wmod, record=False):
+        # Same test, but with recording disabled
+        with wmod.catch_warnings(module=wmod, record=False):
             wmod.filters = wmod.showwarning = object()
         self.assert_(wmod.filters is orig_filters)
         self.assert_(wmod.showwarning is orig_showwarning)
 
     def test_catch_warnings_recording(self):
         wmod = self.module
-        with support.catch_warning(module=wmod) as w:
+        # Ensure warnings are recorded when requested
+        with wmod.catch_warnings(module=wmod, record=True) as w:
             self.assertEqual(w, [])
+            self.assert_(type(w) is list)
             wmod.simplefilter("always")
             wmod.warn("foo")
             self.assertEqual(str(w[-1].message), "foo")
@@ -548,11 +553,59 @@ class CatchWarningTests(BaseTest):
             self.assertEqual(str(w[1].message), "bar")
             del w[:]
             self.assertEqual(w, [])
+        # Ensure warnings are not recorded when not requested
         orig_showwarning = wmod.showwarning
-        with support.catch_warning(module=wmod, record=False) as w:
+        with wmod.catch_warnings(module=wmod, record=False) as w:
             self.assert_(w is None)
             self.assert_(wmod.showwarning is orig_showwarning)
 
+    def test_catch_warnings_reentry_guard(self):
+        wmod = self.module
+        # Ensure catch_warnings is protected against incorrect usage
+        x = wmod.catch_warnings(module=wmod, record=True)
+        self.assertRaises(RuntimeError, x.__exit__)
+        with x:
+            self.assertRaises(RuntimeError, x.__enter__)
+        # Same test, but with recording disabled
+        x = wmod.catch_warnings(module=wmod, record=False)
+        self.assertRaises(RuntimeError, x.__exit__)
+        with x:
+            self.assertRaises(RuntimeError, x.__enter__)
+
+    def test_catch_warnings_defaults(self):
+        wmod = self.module
+        orig_filters = wmod.filters
+        orig_showwarning = wmod.showwarning
+        # Ensure default behaviour is not to record warnings
+        with wmod.catch_warnings(module=wmod) as w:
+            self.assert_(w is None)
+            self.assert_(wmod.showwarning is orig_showwarning)
+            self.assert_(wmod.filters is not orig_filters)
+        self.assert_(wmod.filters is orig_filters)
+        if wmod is sys.modules['warnings']:
+            # Ensure the default module is this one
+            with wmod.catch_warnings() as w:
+                self.assert_(w is None)
+                self.assert_(wmod.showwarning is orig_showwarning)
+                self.assert_(wmod.filters is not orig_filters)
+            self.assert_(wmod.filters is orig_filters)
+
+    def test_check_warnings(self):
+        # Explicit tests for the test.support convenience wrapper
+        wmod = self.module
+        if wmod is sys.modules['warnings']:
+            with support.check_warnings() as w:
+                self.assertEqual(w.warnings, [])
+                wmod.simplefilter("always")
+                wmod.warn("foo")
+                self.assertEqual(str(w.message), "foo")
+                wmod.warn("bar")
+                self.assertEqual(str(w.message), "bar")
+                self.assertEqual(str(w.warnings[0].message), "foo")
+                self.assertEqual(str(w.warnings[1].message), "bar")
+                w.reset()
+                self.assertEqual(w.warnings, [])
+
 class CCatchWarningTests(CatchWarningTests):
     module = c_warnings
 
index 0e147db871540eee28ff61703dcdb41ccaacc1a9..b87d1be176ddaf4c964204e790f0aa3c6072ee11 100644 (file)
@@ -301,8 +301,21 @@ class catch_warnings(object):
         """
         self._record = record
         self._module = sys.modules['warnings'] if module is None else module
+        self._entered = False
+
+    def __repr__(self):
+        args = []
+        if self._record:
+            args.append("record=True")
+        if self._module is not sys.modules['warnings']:
+            args.append("module=%r" % self._module)
+        name = type(self).__name__
+        return "%s(%s)" % (name, ", ".join(args))
 
     def __enter__(self):
+        if self._entered:
+            raise RuntimeError("Cannot enter %r twice" % self)
+        self._entered = True
         self._filters = self._module.filters
         self._module.filters = self._filters[:]
         self._showwarning = self._module.showwarning
@@ -316,6 +329,8 @@ class catch_warnings(object):
             return None
 
     def __exit__(self, *exc_info):
+        if not self._entered:
+            raise RuntimeError("Cannot exit %r without entering first" % self)
         self._module.filters = self._filters
         self._module.showwarning = self._showwarning