From: Nick Coghlan Date: Sun, 13 Jul 2008 12:23:47 +0000 (+0000) Subject: Make test.test_support.catch_warnings more robust as discussed on python-dev. Also... X-Git-Tag: v2.6b2~61 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=38469e271929399c12d6770d91958d8abac7433a;p=thirdparty%2FPython%2Fcpython.git Make test.test_support.catch_warnings more robust as discussed on python-dev. Also add explicit tests for itto test_warnings. --- diff --git a/Doc/library/test.rst b/Doc/library/test.rst index 3c58243706a7..46d21ccae45d 100644 --- a/Doc/library/test.rst +++ b/Doc/library/test.rst @@ -291,19 +291,34 @@ The :mod:`test.test_support` module defines the following functions: This will run all tests defined in the named module. -.. function:: catch_warning(record=True) +.. function:: catch_warning(module=warnings, record=True) Return a context manager that guards the warnings filter from being - permanently changed and records the data of the last warning that has been - issued. The ``record`` argument specifies whether any raised warnings are - captured by the object returned by :func:`warnings.catch_warning` or allowed - to propagate as normal. + permanently changed and optionally alters the :func:`showwarning` + function to record the details of any warnings that are issued in the + managed context. Details of the most recent call to :func:`showwarning` + are saved directly on the context manager, while details of previous + warnings can be retrieved from the ``warnings`` list. - The context manager is typically used like this:: + The context manager is used like this:: with catch_warning() as w: + warnings.simplefilter("always") warnings.warn("foo") - assert str(w.message) == "foo" + assert w.last == "foo" + warnings.warn("bar") + assert w.last == "bar" + assert str(w.warnings[0].message) == "foo" + assert str(w.warnings[1].message) == "bar" + + 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). .. versionadded:: 2.6 @@ -334,8 +349,6 @@ The :mod:`test.test_support` module defines the following classes: attributes on the exception is :exc:`ResourceDenied` raised. .. versionadded:: 2.6 - - .. class:: EnvironmentVarGuard() Class used to temporarily set or unset environment variables. Instances can be @@ -352,3 +365,5 @@ The :mod:`test.test_support` module defines the following classes: .. method:: EnvironmentVarGuard.unset(envvar) Temporarily unset the environment variable ``envvar``. + + diff --git a/Lib/test/test_py3kwarn.py b/Lib/test/test_py3kwarn.py index a289cc765db2..0d0157208825 100644 --- a/Lib/test/test_py3kwarn.py +++ b/Lib/test/test_py3kwarn.py @@ -26,41 +26,30 @@ class TestPy3KWarnings(unittest.TestCase): with catch_warning() as w: safe_exec("True = False") self.assertWarning(None, w, expected) - with catch_warning() as w: safe_exec("False = True") self.assertWarning(None, w, expected) - with catch_warning() as w: try: safe_exec("obj.False = True") except NameError: pass self.assertWarning(None, w, expected) - with catch_warning() as w: try: safe_exec("obj.True = False") except NameError: pass self.assertWarning(None, w, expected) - with catch_warning() as w: safe_exec("def False(): pass") self.assertWarning(None, w, expected) - with catch_warning() as w: safe_exec("def True(): pass") self.assertWarning(None, w, expected) - with catch_warning() as w: safe_exec("class False: pass") self.assertWarning(None, w, expected) - with catch_warning() as w: safe_exec("class True: pass") self.assertWarning(None, w, expected) - with catch_warning() as w: safe_exec("def f(True=43): pass") self.assertWarning(None, w, expected) - with catch_warning() as w: safe_exec("def f(False=None): pass") self.assertWarning(None, w, expected) - with catch_warning() as w: safe_exec("f(False=True)") self.assertWarning(None, w, expected) - with catch_warning() as w: safe_exec("f(True=1)") self.assertWarning(None, w, expected) @@ -69,25 +58,20 @@ class TestPy3KWarnings(unittest.TestCase): expected = 'type inequality comparisons not supported in 3.x' with catch_warning() as w: self.assertWarning(int < str, w, expected) - with catch_warning() as w: self.assertWarning(type < object, w, expected) def test_object_inequality_comparisons(self): expected = 'comparing unequal types not supported in 3.x' with catch_warning() as w: self.assertWarning(str < [], w, expected) - with catch_warning() as w: self.assertWarning(object() < (1, 2), w, expected) def test_dict_inequality_comparisons(self): expected = 'dict inequality comparisons not supported in 3.x' with catch_warning() as w: self.assertWarning({} < {2:3}, w, expected) - with catch_warning() as w: self.assertWarning({} <= {}, w, expected) - with catch_warning() as w: self.assertWarning({} > {2:3}, w, expected) - with catch_warning() as w: self.assertWarning({2:3} >= {}, w, expected) def test_cell_inequality_comparisons(self): @@ -100,7 +84,6 @@ class TestPy3KWarnings(unittest.TestCase): cell1, = f(1).func_closure with catch_warning() as w: self.assertWarning(cell0 == cell1, w, expected) - with catch_warning() as w: self.assertWarning(cell0 < cell1, w, expected) def test_code_inequality_comparisons(self): @@ -111,11 +94,8 @@ class TestPy3KWarnings(unittest.TestCase): pass with catch_warning() as w: self.assertWarning(f.func_code < g.func_code, w, expected) - with catch_warning() as w: self.assertWarning(f.func_code <= g.func_code, w, expected) - with catch_warning() as w: self.assertWarning(f.func_code >= g.func_code, w, expected) - with catch_warning() as w: self.assertWarning(f.func_code > g.func_code, w, expected) def test_builtin_function_or_method_comparisons(self): @@ -125,11 +105,8 @@ class TestPy3KWarnings(unittest.TestCase): meth = {}.get with catch_warning() as w: self.assertWarning(func < meth, w, expected) - with catch_warning() as w: self.assertWarning(func > meth, w, expected) - with catch_warning() as w: self.assertWarning(meth <= func, w, expected) - with catch_warning() as w: self.assertWarning(meth >= func, w, expected) def assertWarning(self, _, warning, expected_message): @@ -142,11 +119,8 @@ class TestPy3KWarnings(unittest.TestCase): with catch_warning() as w: self.assertWarning(lst.sort(cmp=cmp), w, expected) - with catch_warning() as w: self.assertWarning(sorted(lst, cmp=cmp), w, expected) - with catch_warning() as w: self.assertWarning(lst.sort(cmp), w, expected) - with catch_warning() as w: self.assertWarning(sorted(lst, cmp), w, expected) def test_sys_exc_clear(self): @@ -229,7 +203,7 @@ class TestStdlibRemovals(unittest.TestCase): """Make sure the specified module, when imported, raises a DeprecationWarning and specifies itself in the message.""" with CleanImport(module_name): - with catch_warning(record=False) as w: + with catch_warning(record=False): warnings.filterwarnings("error", ".+ removed", DeprecationWarning) try: @@ -290,7 +264,7 @@ class TestStdlibRemovals(unittest.TestCase): def test_main(): - with catch_warning(record=True): + with catch_warning(): warnings.simplefilter("always") run_unittest(TestPy3KWarnings, TestStdlibRemovals) diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py index 7fd058b07282..5e4f0cccc35a 100644 --- a/Lib/test/test_struct.py +++ b/Lib/test/test_struct.py @@ -35,12 +35,9 @@ def with_warning_restore(func): @wraps(func) def decorator(*args, **kw): with catch_warning(): - # Grrr, we need this function to warn every time. Without removing - # the warningregistry, running test_tarfile then test_struct would fail - # on 64-bit platforms. - globals = func.func_globals - if '__warningregistry__' in globals: - del globals['__warningregistry__'] + # We need this function to warn every time, so stick an + # unqualifed 'always' at the head of the filter list + warnings.simplefilter("always") warnings.filterwarnings("error", category=DeprecationWarning) return func(*args, **kw) return decorator @@ -53,7 +50,7 @@ def deprecated_err(func, *args): pass except DeprecationWarning: if not PY_STRUCT_OVERFLOW_MASKING: - raise TestFailed, "%s%s expected to raise struct.error" % ( + raise TestFailed, "%s%s expected to raise DeprecationWarning" % ( func.__name__, args) else: raise TestFailed, "%s%s did not raise error" % ( diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index 848c7d15da60..02a8cca3e08c 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -383,36 +383,49 @@ def open_urlresource(url): class WarningMessage(object): - "Holds the result of the latest showwarning() call" + "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.message = None - self.category = None - self.filename = None - self.lineno = None - - def _showwarning(self, message, category, filename, lineno, file=None, - line=None): - self.message = message - self.category = category - self.filename = filename - self.lineno = lineno - self.line = line + 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._showwarning(*((None,)*6)) + self.warnings = [] + self._set_last(None) def __str__(self): - return ("{message : %r, category : %r, filename : %r, lineno : %s, " - "line : %r}" % (self.message, - self.category.__name__ if self.category else None, - self.filename, self.lineno, self.line)) - + 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 record the - data of the last warning that has been issued. + """Guard the warnings filter from being permanently changed and + optionally record the details of any warnings that are issued. Use like this: @@ -420,13 +433,17 @@ def catch_warning(module=warnings, record=True): warnings.warn("foo") assert str(w.message) == "foo" """ - original_filters = module.filters[:] + original_filters = module.filters original_showwarning = module.showwarning if record: - warning_obj = WarningMessage() - module.showwarning = warning_obj._showwarning + recorder = WarningRecorder() + module.showwarning = recorder._showwarning + else: + recorder = None try: - yield warning_obj if record else None + # Replace the filters with a copy of the original + module.filters = module.filters[:] + yield recorder finally: module.showwarning = original_showwarning module.filters = original_filters @@ -436,7 +453,7 @@ class CleanImport(object): """Context manager to force import to return a new module reference. This is useful for testing module-level behaviours, such as - the emission of a DepreciationWarning on import. + the emission of a DeprecationWarning on import. Use like this: diff --git a/Lib/test/test_warnings.py b/Lib/test/test_warnings.py index ed498e00aae7..7c1706aaa027 100644 --- a/Lib/test/test_warnings.py +++ b/Lib/test/test_warnings.py @@ -488,6 +488,49 @@ class PyWarningsDisplayTests(BaseTest, WarningsDisplayTests): module = py_warnings + +class WarningsSupportTests(object): + """Test the warning tools from test support module""" + + def test_catch_warning_restore(self): + wmod = self.module + orig_filters = wmod.filters + orig_showwarning = wmod.showwarning + with test_support.catch_warning(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): + 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): + wmod = self.module + with test_support.catch_warning(wmod) 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, []) + orig_showwarning = wmod.showwarning + with test_support.catch_warning(wmod, record=False) as w: + self.assert_(w is None) + self.assert_(wmod.showwarning is orig_showwarning) + + +class CWarningsSupportTests(BaseTest, WarningsSupportTests): + module = c_warnings + +class PyWarningsSupportTests(BaseTest, WarningsSupportTests): + module = py_warnings + + class ShowwarningDeprecationTests(BaseTest): """Test the deprecation of the old warnings.showwarning() API works.""" @@ -513,7 +556,6 @@ class PyShowwarningDeprecationTests(ShowwarningDeprecationTests): module = py_warnings - def test_main(): py_warnings.onceregistry.clear() c_warnings.onceregistry.clear() @@ -524,6 +566,7 @@ def test_main(): CWCmdLineTests, PyWCmdLineTests, _WarningsTests, CWarningsDisplayTests, PyWarningsDisplayTests, + CWarningsSupportTests, PyWarningsSupportTests, CShowwarningDeprecationTests, PyShowwarningDeprecationTests, ) diff --git a/Misc/NEWS b/Misc/NEWS index ecd5567c0a56..0cee84e5cf96 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -82,6 +82,14 @@ Library redundant ":443" port number designation when the connection is using the default https port (443). +Tests +----- + +- test.test_support.catch_warning now keeps track of all warnings it sees + and is now better documented. Explicit unit tests for this context manager + have been added to test_warnings. + + Build -----