diff options
author | Brett Cannon <bcannon@gmail.com> | 2008-09-02 01:25:16 (GMT) |
---|---|---|
committer | Brett Cannon <bcannon@gmail.com> | 2008-09-02 01:25:16 (GMT) |
commit | 1eaf0742d877fd9d84d6ed82a04bc33b027e9ad0 (patch) | |
tree | 1c1ee3a5dee04f5f4657b707e9d54ca2e28b1505 | |
parent | 86533776c291c031853609ceaeda96eb2808e4ee (diff) | |
download | cpython-1eaf0742d877fd9d84d6ed82a04bc33b027e9ad0.zip cpython-1eaf0742d877fd9d84d6ed82a04bc33b027e9ad0.tar.gz cpython-1eaf0742d877fd9d84d6ed82a04bc33b027e9ad0.tar.bz2 |
Move test.test_support.catch_warning() to the warnings module, rename it
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.
-rw-r--r-- | Doc/library/warnings.rst | 50 | ||||
-rw-r--r-- | Lib/BaseHTTPServer.py | 10 | ||||
-rw-r--r-- | Lib/asynchat.py | 8 | ||||
-rwxr-xr-x | Lib/cgi.py | 13 | ||||
-rw-r--r-- | Lib/httplib.py | 9 | ||||
-rw-r--r-- | Lib/mimetools.py | 9 | ||||
-rw-r--r-- | Lib/test/test_support.py | 67 | ||||
-rw-r--r-- | Lib/test/test_warnings.py | 74 | ||||
-rw-r--r-- | Lib/warnings.py | 75 | ||||
-rw-r--r-- | Misc/NEWS | 5 | ||||
-rw-r--r-- | Python/_warnings.c | 14 |
11 files changed, 208 insertions, 126 deletions
diff --git a/Doc/library/warnings.rst b/Doc/library/warnings.rst index 888cb84..ae3ab68 100644 --- a/Doc/library/warnings.rst +++ b/Doc/library/warnings.rst @@ -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 diff --git a/Lib/BaseHTTPServer.py b/Lib/BaseHTTPServer.py index 0a6381e..acd8394 100644 --- a/Lib/BaseHTTPServer.py +++ b/Lib/BaseHTTPServer.py @@ -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 diff --git a/Lib/asynchat.py b/Lib/asynchat.py index 121b467..a97de93 100644 --- a/Lib/asynchat.py +++ b/Lib/asynchat.py @@ -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() @@ -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: diff --git a/Lib/httplib.py b/Lib/httplib.py index 62cd0c7..2830ad7 100644 --- a/Lib/httplib.py +++ b/Lib/httplib.py @@ -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: diff --git a/Lib/mimetools.py b/Lib/mimetools.py index 097eda4..fc5a2a5 100644 --- a/Lib/mimetools.py +++ b/Lib/mimetools.py @@ -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 diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index adcbdd1..695bd6d 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -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): diff --git a/Lib/test/test_warnings.py b/Lib/test/test_warnings.py index 7c1706a..1520bf2 100644 --- a/Lib/test/test_warnings.py +++ b/Lib/test/test_warnings.py @@ -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, ) diff --git a/Lib/warnings.py b/Lib/warnings.py index 2e5c512..b699c43 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -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 @@ -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. diff --git a/Python/_warnings.c b/Python/_warnings.c index 5ed8b55..331ad6c 100644 --- a/Python/_warnings.c +++ b/Python/_warnings.c @@ -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); |