From ec92e181fb912e9dc0935fc10bd61bfd4fb237d7 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Tue, 2 Sep 2008 02:46:59 +0000 Subject: Merge in r66135. Doing also required removing a stale DeprecationWarning along with moving warnings.catch_warnings() over to keyword-only parameters for its constructor (as documented in the 2.6 docs). --- Doc/library/warnings.rst | 49 +++++++++++++++++++++++++++++++++ Lib/test/support.py | 58 ++------------------------------------- Lib/test/test_warnings.py | 54 ++++++++++++++++++------------------ Lib/warnings.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++ Misc/NEWS | 3 ++ Python/_warnings.c | 54 ++++++++++-------------------------- 6 files changed, 164 insertions(+), 124 deletions(-) diff --git a/Doc/library/warnings.rst b/Doc/library/warnings.rst index 27acc71..ccd7b44 100644 --- a/Doc/library/warnings.rst +++ b/Doc/library/warnings.rst @@ -247,3 +247,52 @@ 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. + + .. versionadded:: 2.6 + + .. versionchanged:: 3.0 + + Constructor arguments turned into keyword-only arguments. + + +.. 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/test/support.py b/Lib/test/support.py index 24aff5e..a23c99b 100644 --- a/Lib/test/support.py +++ b/Lib/test/support.py @@ -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", - "WarningMessage", "catch_warning", "CleanImport", "EnvironmentVarGuard", + "catch_warning", "CleanImport", "EnvironmentVarGuard", "TransientResource", "captured_output", "captured_stdout", "TransientResource", "transient_internet", "run_with_locale", "set_memlimit", "bigmemtest", "bigaddrspacetest", "BasicTestRunner", @@ -368,47 +368,6 @@ def open_urlresource(url, *args, **kw): return open(fn, *args, **kw) -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. @@ -419,20 +378,7 @@ def catch_warning(module=warnings, record=True): 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 b6634c0..cd408cf 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 support.catch_warning(self.module) as w: + with 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 support.catch_warning(self.module) as w: + with 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 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 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 @@ -487,45 +485,45 @@ class CWarningsDisplayTests(BaseTest, WarningsDisplayTests): class PyWarningsDisplayTests(BaseTest, WarningsDisplayTests): module = py_warnings -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 support.catch_warning(wmod): + with support.catch_warning(module=wmod): wmod.filters = wmod.showwarning = object() self.assert_(wmod.filters is orig_filters) self.assert_(wmod.showwarning is orig_showwarning) - with support.catch_warning(wmod, record=False): + with support.catch_warning(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 support.catch_warning(wmod) as w: - self.assertEqual(w.warnings, []) + with support.catch_warning(module=wmod) 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 support.catch_warning(wmod, record=False) as w: + with support.catch_warning(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,7 +537,7 @@ def test_main(): CWCmdLineTests, PyWCmdLineTests, _WarningsTests, CWarningsDisplayTests, PyWarningsDisplayTests, - CWarningsSupportTests, PyWarningsSupportTests, + CCatchWarningTests, PyCatchWarningTests, ) diff --git a/Lib/warnings.py b/Lib/warnings.py index 0be20e0..566e0b9 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -254,6 +254,76 @@ 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.""" + + 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 diff --git a/Misc/NEWS b/Misc/NEWS index 5947aeb..5290a83 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -60,6 +60,9 @@ C API Library ------- +- Issue 3602: As part of the merge of r66135, make the parameters on + warnings.catch_warnings() keyword-only. Also remove a DeprecationWarning. + - The deprecation warnings for the camelCase threading API names were removed. Extension Modules diff --git a/Python/_warnings.c b/Python/_warnings.c index fd3f629..a08219e 100644 --- a/Python/_warnings.c +++ b/Python/_warnings.c @@ -386,49 +386,23 @@ warn_explicit(PyObject *category, PyObject *message, show_warning(filename, lineno, text, category, sourceline); } else { - const char *msg = "functions overriding warnings.showwarning() " - "must support the 'line' argument"; - const char *text_char = _PyUnicode_AsString(text); - - if (strcmp(msg, text_char) == 0) { - /* Prevent infinite recursion by using built-in implementation - of showwarning(). */ - show_warning(filename, lineno, text, category, sourceline); - } - else { - PyObject *check_fxn; - PyObject *defaults; - PyObject *res; - - if (PyMethod_Check(show_fxn)) - check_fxn = PyMethod_Function(show_fxn); - else if (PyFunction_Check(show_fxn)) - check_fxn = show_fxn; - else { - PyErr_SetString(PyExc_TypeError, - "warnings.showwarning() must be set to a " - "function or method"); - Py_DECREF(show_fxn); - goto cleanup; - } + PyObject *res; - defaults = PyFunction_GetDefaults(check_fxn); - /* 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; - } - } - res = PyObject_CallFunctionObjArgs(show_fxn, message, category, - filename, lineno_obj, - NULL); + if (!PyMethod_Check(show_fxn) && !PyFunction_Check(show_fxn)) { + PyErr_SetString(PyExc_TypeError, + "warnings.showwarning() must be set to a " + "function or method"); Py_DECREF(show_fxn); - Py_XDECREF(res); - if (res == NULL) - goto cleanup; + goto cleanup; } + + res = PyObject_CallFunctionObjArgs(show_fxn, message, category, + filename, lineno_obj, + NULL); + Py_DECREF(show_fxn); + Py_XDECREF(res); + if (res == NULL) + goto cleanup; } } else /* if (rc == -1) */ -- cgit v0.12