Skip to content

Commit

Permalink
Make test.test_support.catch_warnings more robust as discussed on pyt…
Browse files Browse the repository at this point in the history
…hon-dev. Also add explicit tests for it to test_warnings. (forward port of r64910 from trunk)
  • Loading branch information
ncoghlan committed Jul 13, 2008
1 parent 628b1b3 commit b130493
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 42 deletions.
32 changes: 24 additions & 8 deletions Doc/library/test.rst
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ This module defines the following exceptions:
Subclass of :exc:`TestSkipped`. Raised when a resource (such as a network
connection) is not available. Raised by the :func:`requires` function.

The :mod:`test.test_support` module defines the following constants:
The :mod:`test.support` module defines the following constants:


.. data:: verbose
Expand Down Expand Up @@ -278,20 +278,34 @@ The :mod:`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. 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.

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"

warnings.warn("bar")
assert str(w.message) == "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).

.. function:: captured_stdout()

Expand Down Expand Up @@ -331,3 +345,5 @@ The :mod:`test.support` module defines the following classes:
.. method:: EnvironmentVarGuard.unset(envvar)

Temporarily unset the environment variable ``envvar``.


71 changes: 44 additions & 27 deletions Lib/test/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,50 +368,67 @@ def open_urlresource(url, *args, **kw):


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:
with catch_warning() as w:
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
Expand All @@ -421,7 +438,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:
Expand Down
11 changes: 4 additions & 7 deletions Lib/test/test_struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.__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
Expand All @@ -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" % (
Expand Down
42 changes: 42 additions & 0 deletions Lib/test/test_warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,47 @@ class CWarningsDisplayTests(BaseTest, WarningsDisplayTests):
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 support.catch_warning(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):
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 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 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


def test_main():
py_warnings.onceregistry.clear()
Expand All @@ -498,6 +539,7 @@ def test_main():
CWCmdLineTests, PyWCmdLineTests,
_WarningsTests,
CWarningsDisplayTests, PyWarningsDisplayTests,
CWarningsSupportTests, PyWarningsSupportTests,
)


Expand Down

0 comments on commit b130493

Please sign in to comment.