Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b130493

Browse files
committed
Make test.test_support.catch_warnings more robust as discussed on python-dev. Also add explicit tests for it to test_warnings. (forward port of r64910 from trunk)
1 parent 628b1b3 commit b130493

4 files changed

Lines changed: 114 additions & 42 deletions

File tree

Doc/library/test.rst

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ This module defines the following exceptions:
211211
Subclass of :exc:`TestSkipped`. Raised when a resource (such as a network
212212
connection) is not available. Raised by the :func:`requires` function.
213213

214-
The :mod:`test.test_support` module defines the following constants:
214+
The :mod:`test.support` module defines the following constants:
215215

216216

217217
.. data:: verbose
@@ -278,20 +278,34 @@ The :mod:`test.support` module defines the following functions:
278278
This will run all tests defined in the named module.
279279

280280

281-
.. function:: catch_warning(record=True)
281+
.. function:: catch_warning(module=warnings, record=True)
282282

283283
Return a context manager that guards the warnings filter from being
284-
permanently changed and records the data of the last warning that has been
285-
issued. The ``record`` argument specifies whether any raised warnings are
286-
captured by the object returned by :func:`warnings.catch_warning` or allowed
287-
to propagate as normal.
284+
permanently changed and optionally alters the :func:`showwarning`
285+
function to record the details of any warnings that are issued in the
286+
managed context. Attributes of the most recent warning are saved
287+
directly on the context manager, while details of previous warnings
288+
can be retrieved from the ``warnings`` list.
288289

289-
The context manager is typically used like this::
290+
The context manager is used like this::
290291

291292
with catch_warning() as w:
293+
warnings.simplefilter("always")
292294
warnings.warn("foo")
293295
assert str(w.message) == "foo"
294-
296+
warnings.warn("bar")
297+
assert str(w.message) == "bar"
298+
assert str(w.warnings[0].message) == "foo"
299+
assert str(w.warnings[1].message) == "bar"
300+
301+
By default, the real :mod:`warnings` module is affected - the ability
302+
to select a different module is provided for the benefit of the
303+
:mod:`warnings` module's own unit tests.
304+
The ``record`` argument specifies whether or not the :func:`showwarning`
305+
function is replaced. Note that recording the warnings in this fashion
306+
also prevents them from being written to sys.stderr. If set to ``False``,
307+
the standard handling of warning messages is left in place (however, the
308+
original handling is still restored at the end of the block).
295309

296310
.. function:: captured_stdout()
297311

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

333347
Temporarily unset the environment variable ``envvar``.
348+
349+

Lib/test/support.py

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -368,50 +368,67 @@ def open_urlresource(url, *args, **kw):
368368

369369

370370
class WarningMessage(object):
371-
"Holds the result of the latest showwarning() call"
371+
"Holds the result of a single showwarning() call"
372+
_WARNING_DETAILS = "message category filename lineno line".split()
373+
def __init__(self, message, category, filename, lineno, line=None):
374+
for attr in self._WARNING_DETAILS:
375+
setattr(self, attr, locals()[attr])
376+
self._category_name = category.__name__ if category else None
377+
378+
def __str__(self):
379+
return ("{message : %r, category : %r, filename : %r, lineno : %s, "
380+
"line : %r}" % (self.message, self._category_name,
381+
self.filename, self.lineno, self.line))
382+
383+
class WarningRecorder(object):
384+
"Records the result of any showwarning calls"
372385
def __init__(self):
373-
self.message = None
374-
self.category = None
375-
self.filename = None
376-
self.lineno = None
377-
378-
def _showwarning(self, message, category, filename, lineno, file=None,
379-
line=None):
380-
self.message = message
381-
self.category = category
382-
self.filename = filename
383-
self.lineno = lineno
384-
self.line = line
386+
self.warnings = []
387+
self._set_last(None)
388+
389+
def _showwarning(self, message, category, filename, lineno,
390+
file=None, line=None):
391+
wm = WarningMessage(message, category, filename, lineno, line)
392+
self.warnings.append(wm)
393+
self._set_last(wm)
394+
395+
def _set_last(self, last_warning):
396+
if last_warning is None:
397+
for attr in WarningMessage._WARNING_DETAILS:
398+
setattr(self, attr, None)
399+
else:
400+
for attr in WarningMessage._WARNING_DETAILS:
401+
setattr(self, attr, getattr(last_warning, attr))
385402

386403
def reset(self):
387-
self._showwarning(*((None,)*6))
404+
self.warnings = []
405+
self._set_last(None)
388406

389407
def __str__(self):
390-
return ("{message : %r, category : %r, filename : %r, lineno : %s, "
391-
"line : %r}" % (self.message,
392-
self.category.__name__ if self.category else None,
393-
self.filename, self.lineno, self.line))
394-
408+
return '[%s]' % (', '.join(map(str, self.warnings)))
395409

396410
@contextlib.contextmanager
397411
def catch_warning(module=warnings, record=True):
398-
"""
399-
Guard the warnings filter from being permanently changed and record the
400-
data of the last warning that has been issued.
412+
"""Guard the warnings filter from being permanently changed and
413+
optionally record the details of any warnings that are issued.
401414
402415
Use like this:
403416
404417
with catch_warning() as w:
405418
warnings.warn("foo")
406419
assert str(w.message) == "foo"
407420
"""
408-
original_filters = module.filters[:]
421+
original_filters = module.filters
409422
original_showwarning = module.showwarning
410423
if record:
411-
warning_obj = WarningMessage()
412-
module.showwarning = warning_obj._showwarning
424+
recorder = WarningRecorder()
425+
module.showwarning = recorder._showwarning
426+
else:
427+
recorder = None
413428
try:
414-
yield warning_obj if record else None
429+
# Replace the filters with a copy of the original
430+
module.filters = module.filters[:]
431+
yield recorder
415432
finally:
416433
module.showwarning = original_showwarning
417434
module.filters = original_filters
@@ -421,7 +438,7 @@ class CleanImport(object):
421438
"""Context manager to force import to return a new module reference.
422439
423440
This is useful for testing module-level behaviours, such as
424-
the emission of a DepreciationWarning on import.
441+
the emission of a DeprecationWarning on import.
425442
426443
Use like this:
427444

Lib/test/test_struct.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,9 @@ def with_warning_restore(func):
3535
@wraps(func)
3636
def decorator(*args, **kw):
3737
with catch_warning():
38-
# Grrr, we need this function to warn every time. Without removing
39-
# the warningregistry, running test_tarfile then test_struct would fail
40-
# on 64-bit platforms.
41-
globals = func.__globals__
42-
if '__warningregistry__' in globals:
43-
del globals['__warningregistry__']
38+
# We need this function to warn every time, so stick an
39+
# unqualifed 'always' at the head of the filter list
40+
warnings.simplefilter("always")
4441
warnings.filterwarnings("error", category=DeprecationWarning)
4542
return func(*args, **kw)
4643
return decorator
@@ -53,7 +50,7 @@ def deprecated_err(func, *args):
5350
pass
5451
except DeprecationWarning:
5552
if not PY_STRUCT_OVERFLOW_MASKING:
56-
raise TestFailed("%s%s expected to raise struct.error" % (
53+
raise TestFailed("%s%s expected to raise DeprecationWarning" % (
5754
func.__name__, args))
5855
else:
5956
raise TestFailed("%s%s did not raise error" % (

Lib/test/test_warnings.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,47 @@ class CWarningsDisplayTests(BaseTest, WarningsDisplayTests):
487487
class PyWarningsDisplayTests(BaseTest, WarningsDisplayTests):
488488
module = py_warnings
489489

490+
class WarningsSupportTests(object):
491+
"""Test the warning tools from test support module"""
492+
493+
def test_catch_warning_restore(self):
494+
wmod = self.module
495+
orig_filters = wmod.filters
496+
orig_showwarning = wmod.showwarning
497+
with support.catch_warning(wmod):
498+
wmod.filters = wmod.showwarning = object()
499+
self.assert_(wmod.filters is orig_filters)
500+
self.assert_(wmod.showwarning is orig_showwarning)
501+
with support.catch_warning(wmod, record=False):
502+
wmod.filters = wmod.showwarning = object()
503+
self.assert_(wmod.filters is orig_filters)
504+
self.assert_(wmod.showwarning is orig_showwarning)
505+
506+
def test_catch_warning_recording(self):
507+
wmod = self.module
508+
with support.catch_warning(wmod) as w:
509+
self.assertEqual(w.warnings, [])
510+
wmod.simplefilter("always")
511+
wmod.warn("foo")
512+
self.assertEqual(str(w.message), "foo")
513+
wmod.warn("bar")
514+
self.assertEqual(str(w.message), "bar")
515+
self.assertEqual(str(w.warnings[0].message), "foo")
516+
self.assertEqual(str(w.warnings[1].message), "bar")
517+
w.reset()
518+
self.assertEqual(w.warnings, [])
519+
orig_showwarning = wmod.showwarning
520+
with support.catch_warning(wmod, record=False) as w:
521+
self.assert_(w is None)
522+
self.assert_(wmod.showwarning is orig_showwarning)
523+
524+
525+
class CWarningsSupportTests(BaseTest, WarningsSupportTests):
526+
module = c_warnings
527+
528+
class PyWarningsSupportTests(BaseTest, WarningsSupportTests):
529+
module = py_warnings
530+
490531

491532
def test_main():
492533
py_warnings.onceregistry.clear()
@@ -498,6 +539,7 @@ def test_main():
498539
CWCmdLineTests, PyWCmdLineTests,
499540
_WarningsTests,
500541
CWarningsDisplayTests, PyWarningsDisplayTests,
542+
CWarningsSupportTests, PyWarningsSupportTests,
501543
)
502544

503545

0 commit comments

Comments
 (0)