]> granicus.if.org Git - python/commitdiff
Move test.test_support.catch_warning() to the warnings module, rename it
authorBrett Cannon <bcannon@gmail.com>
Tue, 2 Sep 2008 01:25:16 +0000 (01:25 +0000)
committerBrett Cannon <bcannon@gmail.com>
Tue, 2 Sep 2008 01:25:16 +0000 (01:25 +0000)
catch_warnings(), and clean up the API.

While expanding the test suite, a bug was found where a warning about the
'line' argument to showwarning() was not letting functions with '*args' go
without a warning.

Closes issue 3602.
Code review by Benjamin Peterson.

Doc/library/warnings.rst
Lib/BaseHTTPServer.py
Lib/asynchat.py
Lib/cgi.py
Lib/httplib.py
Lib/mimetools.py
Lib/test/test_support.py
Lib/test/test_warnings.py
Lib/warnings.py
Misc/NEWS
Python/_warnings.c

index 888cb84ef047f9dcc74ee1928f426e8b7b1dd691..ae3ab680f31e530cbc9a907e3960a1bb252ab6c8 100644 (file)
@@ -263,3 +263,53 @@ Available Functions
    :func:`filterwarnings`, including that of the :option:`-W` command line options
    and calls to :func:`simplefilter`.
 
+
+Available Classes
+-----------------
+
+.. class:: catch_warnings([record=False[, module=None]])
+
+    A context manager that guards the warnings filter from being permanentally
+    mutated. The manager returns an instance of :class:`WarningsRecorder`. The 
+    *record* argument specifies whether warnings that would typically be
+    handled by :func:`showwarning` should instead be recorded by the
+    :class:`WarningsRecorder` instance. This argument is typically set when
+    testing for expected warnings behavior. The *module* argument may be a
+    module object that is to be used instead of the :mod:`warnings` module.
+    This argument should only be set when testing the :mod:`warnings` module 
+    or some similar use-case.
+
+    Typical usage of the context manager is like so::
+
+        def fxn():
+            warn("fxn is deprecated", DeprecationWarning)
+            return "spam spam bacon spam"
+
+        # The function 'fxn' is known to raise a DeprecationWarning.
+        with catch_warnings() as w:
+            warnings.filterwarning('ignore', 'fxn is deprecated', DeprecationWarning)
+            fxn()  # DeprecationWarning is temporarily suppressed.
+
+    .. note::
+
+        In Python 3.0, the arguments to the constructor for
+        :class:`catch_warnings` are keyword-only arguments.
+
+    .. versionadded:: 2.6
+
+
+.. class:: WarningsRecorder()
+
+    A subclass of :class:`list` that stores all warnings passed to
+    :func:`showwarning` when returned by a :class:`catch_warnings` context
+    manager created with its *record* argument set to ``True``. Each recorded
+    warning is represented by an object whose attributes correspond to the
+    arguments to :func:`showwarning`. As a convenience, a
+    :class:`WarningsRecorder` instance has the attributes of the last
+    recorded warning set on the :class:`WarningsRecorder` instance as well.
+
+    .. method:: reset()
+
+        Delete all recorded warnings.
+
+    .. versionadded:: 2.6
index 0a6381e422d7f0bd0f605768df3fad4d3ecee5b4..acd839410e0a2eda79425044548c1d2b0e691133 100644 (file)
@@ -73,11 +73,11 @@ __all__ = ["HTTPServer", "BaseHTTPRequestHandler"]
 import sys
 import time
 import socket # For gethostbyaddr()
-from test.test_support import catch_warning
-from warnings import filterwarnings
-with catch_warning(record=False):
-    filterwarnings("ignore", ".*mimetools has been removed",
-                    DeprecationWarning)
+from warnings import filterwarnings, catch_warnings
+with catch_warnings():
+    if sys.py3kwarning:
+        filterwarnings("ignore", ".*mimetools has been removed",
+                        DeprecationWarning)
     import mimetools
 import SocketServer
 
index 121b467d4903e1621b09a064b02b9357dfe907e7..a97de93f27053edc6a95610872a36552a205b50a 100644 (file)
@@ -49,8 +49,9 @@ you - by calling your self.found_terminator() method.
 import socket
 import asyncore
 from collections import deque
+from sys import py3kwarning
 from test.test_support import catch_warning
-from warnings import filterwarnings
+from warnings import filterwarnings, catch_warnings
 
 class async_chat (asyncore.dispatcher):
     """This is an abstract class.  You must derive from this class, and add
@@ -218,8 +219,9 @@ class async_chat (asyncore.dispatcher):
             # handle classic producer behavior
             obs = self.ac_out_buffer_size
             try:
-                with catch_warning(record=False):
-                    filterwarnings("ignore", ".*buffer", DeprecationWarning)
+                with catch_warnings():
+                    if py3kwarning:
+                        filterwarnings("ignore", ".*buffer", DeprecationWarning)
                     data = buffer(first, 0, obs)
             except TypeError:
                 data = first.more()
index 3a795c962c1d5ada70423c6dc02a1edce2a66dd1..dd1138940519a98dd99fe7d2c1effb641fbb373f 100755 (executable)
@@ -39,13 +39,14 @@ import sys
 import os
 import urllib
 import UserDict
-from test.test_support import catch_warning
-from warnings import filterwarnings
-with catch_warning(record=False):
-    filterwarnings("ignore", ".*mimetools has been removed",
-                    DeprecationWarning)
+from warnings import filterwarnings, catch_warnings
+with catch_warnings():
+    if sys.py3kwarning:
+        filterwarnings("ignore", ".*mimetools has been removed",
+                        DeprecationWarning)
     import mimetools
-    filterwarnings("ignore", ".*rfc822 has been removed", DeprecationWarning)
+    if sys.py3kwarning:
+        filterwarnings("ignore", ".*rfc822 has been removed", DeprecationWarning)
     import rfc822
 
 try:
index 62cd0c74974f908b79ab52fbeea0855f0aba5925..2830ad726dce160edf66fa8372b095535280f08f 100644 (file)
@@ -67,12 +67,13 @@ Req-sent-unread-response       _CS_REQ_SENT       <response_class>
 """
 
 import socket
+from sys import py3kwarning
 from urlparse import urlsplit
 import warnings
-from test.test_support import catch_warning
-with catch_warning(record=False):
-    warnings.filterwarnings("ignore", ".*mimetools has been removed",
-                            DeprecationWarning)
+with warnings.catch_warnings():
+    if py3kwarning:
+        warnings.filterwarnings("ignore", ".*mimetools has been removed",
+                                DeprecationWarning)
     import mimetools
 
 try:
index 097eda4a75bd2fcbd4fef71a6c6e223594a6094c..fc5a2a5f18613a3c1d1c057d9e4e4f965d12bbc7 100644 (file)
@@ -2,11 +2,12 @@
 
 
 import os
+import sys
 import tempfile
-from test.test_support import catch_warning
-from warnings import filterwarnings
-with catch_warning(record=False):
-    filterwarnings("ignore", ".*rfc822 has been removed", DeprecationWarning)
+from warnings import filterwarnings, catch_warnings
+with catch_warnings(record=False):
+    if sys.py3kwarning:
+        filterwarnings("ignore", ".*rfc822 has been removed", DeprecationWarning)
     import rfc822
 
 from warnings import warnpy3k
index adcbdd18e872b354c603313916adb35de5f9db1e..695bd6d0c3aeb8645ff7e63b9bbf1e2eafafe023 100644 (file)
@@ -18,7 +18,7 @@ __all__ = ["Error", "TestFailed", "TestSkipped", "ResourceDenied", "import_modul
            "is_resource_enabled", "requires", "find_unused_port", "bind_port",
            "fcmp", "have_unicode", "is_jython", "TESTFN", "HOST", "FUZZ",
            "findfile", "verify", "vereq", "sortdict", "check_syntax_error",
-           "open_urlresource", "WarningMessage", "catch_warning", "CleanImport",
+           "open_urlresource", "catch_warning", "CleanImport",
            "EnvironmentVarGuard", "captured_output",
            "captured_stdout", "TransientResource", "transient_internet",
            "run_with_locale", "set_memlimit", "bigmemtest", "bigaddrspacetest",
@@ -381,71 +381,8 @@ def open_urlresource(url):
     return open(fn)
 
 
-class WarningMessage(object):
-    "Holds the result of a single showwarning() call"
-    _WARNING_DETAILS = "message category filename lineno line".split()
-    def __init__(self, message, category, filename, lineno, line=None):
-        for attr in self._WARNING_DETAILS:
-            setattr(self, attr, locals()[attr])
-        self._category_name = category.__name__ if category else None
-
-    def __str__(self):
-        return ("{message : %r, category : %r, filename : %r, lineno : %s, "
-                    "line : %r}" % (self.message, self._category_name,
-                                    self.filename, self.lineno, self.line))
-
-class WarningRecorder(object):
-    "Records the result of any showwarning calls"
-    def __init__(self):
-        self.warnings = []
-        self._set_last(None)
-
-    def _showwarning(self, message, category, filename, lineno,
-                    file=None, line=None):
-        wm = WarningMessage(message, category, filename, lineno, line)
-        self.warnings.append(wm)
-        self._set_last(wm)
-
-    def _set_last(self, last_warning):
-        if last_warning is None:
-            for attr in WarningMessage._WARNING_DETAILS:
-                setattr(self, attr, None)
-        else:
-            for attr in WarningMessage._WARNING_DETAILS:
-                setattr(self, attr, getattr(last_warning, attr))
-
-    def reset(self):
-        self.warnings = []
-        self._set_last(None)
-
-    def __str__(self):
-        return '[%s]' % (', '.join(map(str, self.warnings)))
-
-@contextlib.contextmanager
 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.
-
-    Use like this:
-
-        with catch_warning() as w:
-            warnings.warn("foo")
-            assert str(w.message) == "foo"
-    """
-    original_filters = module.filters
-    original_showwarning = module.showwarning
-    if record:
-        recorder = WarningRecorder()
-        module.showwarning = recorder._showwarning
-    else:
-        recorder = None
-    try:
-        # Replace the filters with a copy of the original
-        module.filters = module.filters[:]
-        yield recorder
-    finally:
-        module.showwarning = original_showwarning
-        module.filters = original_filters
+    return warnings.catch_warnings(record=record, module=module)
 
 
 class CleanImport(object):
index 7c1706aaa0279f08b93534c1277b4ffafbf0468d..1520bf2b350a95ecef01d1ca75f6dbe10d08b399 100644 (file)
@@ -79,20 +79,19 @@ class FilterTests(object):
                                 "FilterTests.test_error")
 
     def test_ignore(self):
-        with test_support.catch_warning(self.module) as w:
+        with test_support.catch_warning(module=self.module) as w:
             self.module.resetwarnings()
             self.module.filterwarnings("ignore", category=UserWarning)
             self.module.warn("FilterTests.test_ignore", UserWarning)
-            self.assert_(not w.message)
+            self.assertEquals(len(w), 0)
 
     def test_always(self):
-        with test_support.catch_warning(self.module) as w:
+        with test_support.catch_warning(module=self.module) as w:
             self.module.resetwarnings()
             self.module.filterwarnings("always", category=UserWarning)
             message = "FilterTests.test_always"
             self.module.warn(message, UserWarning)
             self.assert_(message, w.message)
-            w.message = None  # Reset.
             self.module.warn(message, UserWarning)
             self.assert_(w.message, message)
 
@@ -107,7 +106,7 @@ class FilterTests(object):
                     self.assertEquals(w.message, message)
                     w.reset()
                 elif x == 1:
-                    self.assert_(not w.message, "unexpected warning: " + str(w))
+                    self.assert_(not len(w), "unexpected warning: " + str(w))
                 else:
                     raise ValueError("loop variant unhandled")
 
@@ -120,7 +119,7 @@ class FilterTests(object):
             self.assertEquals(w.message, message)
             w.reset()
             self.module.warn(message, UserWarning)
-            self.assert_(not w.message, "unexpected message: " + str(w))
+            self.assert_(not len(w), "unexpected message: " + str(w))
 
     def test_once(self):
         with test_support.catch_warning(self.module) as w:
@@ -133,10 +132,10 @@ class FilterTests(object):
             w.reset()
             self.module.warn_explicit(message, UserWarning, "test_warnings.py",
                                     13)
-            self.assert_(not w.message)
+            self.assertEquals(len(w), 0)
             self.module.warn_explicit(message, UserWarning, "test_warnings2.py",
                                     42)
-            self.assert_(not w.message)
+            self.assertEquals(len(w), 0)
 
     def test_inheritance(self):
         with test_support.catch_warning(self.module) as w:
@@ -156,7 +155,7 @@ class FilterTests(object):
                 self.module.warn("FilterTests.test_ordering", UserWarning)
             except UserWarning:
                 self.fail("order handling for actions failed")
-            self.assert_(not w.message)
+            self.assertEquals(len(w), 0)
 
     def test_filterwarnings(self):
         # Test filterwarnings().
@@ -317,7 +316,6 @@ class WarnTests(unittest.TestCase):
                             None, Warning, None, 1, registry=42)
 
 
-
 class CWarnTests(BaseTest, WarnTests):
     module = c_warnings
 
@@ -377,7 +375,7 @@ class _WarningsTests(BaseTest):
                 self.failUnlessEqual(w.message, message)
                 w.reset()
                 self.module.warn_explicit(message, UserWarning, "file", 42)
-                self.assert_(not w.message)
+                self.assertEquals(len(w), 0)
                 # Test the resetting of onceregistry.
                 self.module.onceregistry = {}
                 __warningregistry__ = {}
@@ -388,7 +386,7 @@ class _WarningsTests(BaseTest):
                 del self.module.onceregistry
                 __warningregistry__ = {}
                 self.module.warn_explicit(message, UserWarning, "file", 42)
-                self.failUnless(not w.message)
+                self.assertEquals(len(w), 0)
         finally:
             self.module.onceregistry = original_registry
 
@@ -489,45 +487,45 @@ class PyWarningsDisplayTests(BaseTest, WarningsDisplayTests):
 
 
 
-class WarningsSupportTests(object):
-    """Test the warning tools from test support module"""
+class CatchWarningTests(BaseTest):
 
-    def test_catch_warning_restore(self):
+    """Test catch_warnings()."""
+
+    def test_catch_warnings_restore(self):
         wmod = self.module
         orig_filters = wmod.filters
         orig_showwarning = wmod.showwarning
-        with test_support.catch_warning(wmod):
+        with wmod.catch_warnings(record=True, module=wmod):
             wmod.filters = wmod.showwarning = object()
         self.assert_(wmod.filters is orig_filters)
         self.assert_(wmod.showwarning is orig_showwarning)
-        with test_support.catch_warning(wmod, record=False):
+        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_warning_recording(self):
+    def test_catch_warnings_recording(self):
         wmod = self.module
-        with test_support.catch_warning(wmod) as w:
-            self.assertEqual(w.warnings, [])
+        with wmod.catch_warnings(module=wmod, record=True) as w:
+            self.assertEqual(w, [])
             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")
+            self.assertEqual(str(w[0].message), "foo")
+            self.assertEqual(str(w[1].message), "bar")
             w.reset()
-            self.assertEqual(w.warnings, [])
+            self.assertEqual(w, [])
         orig_showwarning = wmod.showwarning
-        with test_support.catch_warning(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)
 
-
-class CWarningsSupportTests(BaseTest, WarningsSupportTests):
+class CCatchWarningTests(CatchWarningTests):
     module = c_warnings
 
-class PyWarningsSupportTests(BaseTest, WarningsSupportTests):
+class PyCatchWarningTests(CatchWarningTests):
     module = py_warnings
 
 
@@ -539,14 +537,24 @@ class ShowwarningDeprecationTests(BaseTest):
     def bad_showwarning(message, category, filename, lineno, file=None):
         pass
 
+    @staticmethod
+    def ok_showwarning(*args):
+        pass
+
     def test_deprecation(self):
         # message, category, filename, lineno[, file[, line]]
         args = ("message", UserWarning, "file name", 42)
-        with test_support.catch_warning(self.module):
+        with test_support.catch_warning(module=self.module):
             self.module.filterwarnings("error", category=DeprecationWarning)
             self.module.showwarning = self.bad_showwarning
             self.assertRaises(DeprecationWarning, self.module.warn_explicit,
                                 *args)
+            self.module.showwarning = self.ok_showwarning
+            try:
+                self.module.warn_explicit(*args)
+            except DeprecationWarning as exc:
+                self.fail('showwarning(*args) should not trigger a '
+                            'DeprecationWarning')
 
 class CShowwarningDeprecationTests(ShowwarningDeprecationTests):
     module = c_warnings
@@ -559,16 +567,14 @@ class PyShowwarningDeprecationTests(ShowwarningDeprecationTests):
 def test_main():
     py_warnings.onceregistry.clear()
     c_warnings.onceregistry.clear()
-    test_support.run_unittest(CFilterTests,
-                                PyFilterTests,
-                                CWarnTests,
-                                PyWarnTests,
+    test_support.run_unittest(CFilterTests, PyFilterTests,
+                                CWarnTests, PyWarnTests,
                                 CWCmdLineTests, PyWCmdLineTests,
                                 _WarningsTests,
                                 CWarningsDisplayTests, PyWarningsDisplayTests,
-                                CWarningsSupportTests, PyWarningsSupportTests,
+                                CCatchWarningTests, PyCatchWarningTests,
                                 CShowwarningDeprecationTests,
-                                PyShowwarningDeprecationTests,
+                                    PyShowwarningDeprecationTests,
                              )
 
 
index 2e5c51270f6e7119a077d4a3eecf143854aacfb1..b699c439cf7c972ab1e0fca78aa992c10fa38bff 100644 (file)
@@ -272,7 +272,8 @@ def warn_explicit(message, category, filename, lineno,
         fxn_code = showwarning.__func__.func_code
     if fxn_code:
         args = fxn_code.co_varnames[:fxn_code.co_argcount]
-        if 'line' not in args:
+        CO_VARARGS = 0x4
+        if 'line' not in args and not fxn_code.co_flags & CO_VARARGS:
             showwarning_msg = ("functions overriding warnings.showwarning() "
                                 "must support the 'line' argument")
             if message == showwarning_msg:
@@ -283,6 +284,78 @@ def warn_explicit(message, category, filename, lineno,
     showwarning(message, category, filename, lineno)
 
 
+class WarningMessage(object):
+
+    """Holds the result of a single showwarning() call."""
+
+    _WARNING_DETAILS = ("message", "category", "filename", "lineno", "file",
+                        "line")
+
+    def __init__(self, message, category, filename, lineno, file=None,
+                    line=None):
+        local_values = locals()
+        for attr in self._WARNING_DETAILS:
+            setattr(self, attr, local_values[attr])
+        self._category_name = category.__name__ if category else None
+
+    def __str__(self):
+        return ("{message : %r, category : %r, filename : %r, lineno : %s, "
+                    "line : %r}" % (self.message, self._category_name,
+                                    self.filename, self.lineno, self.line))
+
+
+class WarningsRecorder(list):
+
+    """Record the result of various showwarning() calls."""
+
+    # Explicitly stated arguments so as to not trigger DeprecationWarning
+    # about adding 'line'.
+    def showwarning(self, *args, **kwargs):
+        self.append(WarningMessage(*args, **kwargs))
+
+    def __getattr__(self, attr):
+        return getattr(self[-1], attr)
+
+    def reset(self):
+        del self[:]
+
+
+class catch_warnings(object):
+
+    """Guard the warnings filter from being permanently changed and optionally
+    record the details of any warnings that are issued.
+
+    Context manager returns an instance of warnings.WarningRecorder which is a
+    list of WarningMessage instances. Attributes on WarningRecorder are
+    redirected to the last created WarningMessage instance.
+
+    """
+
+    def __init__(self, record=False, module=None):
+        """Specify whether to record warnings and if an alternative module
+        should be used other than sys.modules['warnings'].
+
+        For compatibility with Python 3.0, please consider all arguments to be
+        keyword-only.
+
+        """
+        self._recorder = WarningsRecorder() if record else None
+        self._module = sys.modules['warnings'] if module is None else module
+
+    def __enter__(self):
+        self._filters = self._module.filters
+        self._module.filters = self._filters[:]
+        self._showwarning = self._module.showwarning
+        if self._recorder is not None:
+            self._recorder.reset()  # In case the instance is being reused.
+            self._module.showwarning = self._recorder.showwarning
+        return self._recorder
+
+    def __exit__(self, *exc_info):
+        self._module.filters = self._filters
+        self._module.showwarning = self._showwarning
+
+
 # filters contains a sequence of filter 5-tuples
 # The components of the 5-tuple are:
 # - an action: error, ignore, always, default, module, or once
index 73bcf08b628dd7a42bb646e95f0081debb541d30..76df45c8c3cc0292e218f2072fe5fb1a71ea6e98 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -53,6 +53,11 @@ C-API
 Library
 -------
 
+- Issue 3602: Moved test.test_support.catch_warning() to
+  warnings.catch_warnings() along with some API cleanup. Expanding the tests
+  for catch_warnings() also led to an improvement in the raising of a
+  DeprecationWarning related to warnings.warn_explicit().
+
 - The deprecation warnings for the old camelCase threading API were removed.
 
 - logging: fixed lack of use of encoding attribute specified on a stream.
index 5ed8b55b9f71d36584247525fd809823cd4da5df..331ad6cb9751f1a3a61b8b70f0fedc7fde46819a 100644 (file)
@@ -1,4 +1,5 @@
 #include "Python.h"
+#include "code.h"  /* For DeprecationWarning about adding 'line'. */
 #include "frameobject.h"
 
 #define MODULE_NAME "_warnings"
@@ -416,11 +417,16 @@ warn_explicit(PyObject *category, PyObject *message,
                 /* A proper implementation of warnings.showwarning() should
                     have at least two default arguments. */
                 if ((defaults == NULL) || (PyTuple_Size(defaults) < 2)) {
-                    if (PyErr_WarnEx(PyExc_DeprecationWarning, msg, 1) < 0) {
-                        Py_DECREF(show_fxn);
-                        goto cleanup;
+                   PyCodeObject *code = (PyCodeObject *)
+                                               PyFunction_GetCode(check_fxn);
+                   if (!(code->co_flags & CO_VARARGS)) {
+                       if (PyErr_WarnEx(PyExc_DeprecationWarning, msg, 1) <
+                               0) {
+                            Py_DECREF(show_fxn);
+                            goto cleanup;
+                        }
                     }
-                }
+               }
                 res = PyObject_CallFunctionObjArgs(show_fxn, message, category,
                                                     filename, lineno_obj,
                                                     NULL);