Skip to content

Simplify interface for testing of warnings. #345

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 3, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 19 additions & 35 deletions nibabel/checkwarns.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,39 +12,23 @@

import warnings

from .testing import (error_warnings, suppress_warnings)

class ErrorWarnings(warnings.catch_warnings):
""" Context manager to check for warnings as errors. Usually used with
``assert_raises`` in the with block

Examples
--------
>>> with ErrorWarnings():
... try:
... warnings.warn('Message', UserWarning)
... except UserWarning:
... print('I consider myself warned')
I consider myself warned
"""
filter = 'error'

def __init__(self, record=True, module=None):
super(ErrorWarnings, self).__init__(record=record, module=module)

def __enter__(self):
mgr = super(ErrorWarnings, self).__enter__()
warnings.simplefilter(self.filter)
return mgr


class IgnoreWarnings(ErrorWarnings):
""" Context manager to ignore warnings

Examples
--------
>>> with IgnoreWarnings():
... warnings.warn('Message', UserWarning)

(and you get no warning)
"""
filter = 'ignore'

warnings.warn('The checkwarns module is deprecated and will be removed in nibabel v3.0', FutureWarning)


class ErrorWarnings(error_warnings):
def __init__(self, *args, **kwargs):
warnings.warn('ErrorWarnings is deprecated and will be removed in '
'nibabel v3.0; use nibabel.testing.error_warnings.',
FutureWarning)
super(ErrorWarnings, self).__init__(*args, **kwargs)


class IgnoreWarnings(suppress_warnings):
def __init__(self, *args, **kwargs):
warnings.warn('IgnoreWarnings is deprecated and will be removed in '
'nibabel v3.0; use nibabel.testing.suppress_warnings.',
FutureWarning)
super(IgnoreWarnings, self).__init__(*args, **kwargs)
120 changes: 98 additions & 22 deletions nibabel/testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,23 @@
#
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
''' Utilities for testing '''
from __future__ import division, print_function

import sys
import warnings
from os.path import dirname, abspath, join as pjoin

import numpy as np
from warnings import catch_warnings, simplefilter

# set path to example data
data_path = abspath(pjoin(dirname(__file__), '..', 'tests', 'data'))

# Allow failed import of nose if not now running tests
try:
import nose.tools as nt
except ImportError:
pass
else:
from nose.tools import (assert_equal, assert_not_equal,
assert_true, assert_false, assert_raises)
except ImportError:
pass

# set path to example data
data_path = abspath(pjoin(dirname(__file__), '..', 'tests', 'data'))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matthew-brett Any idea if try...catch is still necessary? I took the liberty to blow it away. I couldn't tell from the commits (5+ years ago) where this was being used. I didn't see nt being used anywhere in my cursory search, and tests pass...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so that people can still import nibabel if they don't have nose installed.



def assert_dt_equal(a, b):
Expand Down Expand Up @@ -56,35 +57,110 @@ def assert_allclose_safely(a, b, match_nans=True):
assert_true(np.allclose(a, b))


class suppress_warnings(catch_warnings):
""" Version of ``catch_warnings`` class that suppresses warnings
"""
def __enter__(self):
res = super(suppress_warnings, self).__enter__()
simplefilter('ignore')
return res
def get_fresh_mod(mod_name=__name__):
# Get this module, with warning registry empty
my_mod = sys.modules[mod_name]
try:
my_mod.__warningregistry__.clear()
except AttributeError:
pass
return my_mod


class clear_and_catch_warnings(warnings.catch_warnings):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a copy of code from https://github.com/numpy/numpy/pull/5682/files . I did NOT copy over the tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I just changed the default to record=True for the nibabel version of this code. This is what was used in all of these helper classes in the nibabel codebase previously, and I think that makes a lot of sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind copying over the tests as well?

""" Context manager that resets warning registry for catching warnings

Warnings can be slippery, because, whenever a warning is triggered, Python
adds a ``__warningregistry__`` member to the *calling* module. This makes
it impossible to retrigger the warning in this module, whatever you put in
the warnings filters. This context manager accepts a sequence of `modules`
as a keyword argument to its constructor and:

* stores and removes any ``__warningregistry__`` entries in given `modules`
on entry;
* resets ``__warningregistry__`` to its previous state on exit.

This makes it possible to trigger any warning afresh inside the context
manager without disturbing the state of warnings outside.

For compatibility with Python 3.0, please consider all arguments to be
keyword-only.

class catch_warn_reset(catch_warnings):
""" Version of ``catch_warnings`` class that resets warning registry
Parameters
----------
record : bool, optional
Specifies whether warnings should be captured by a custom
implementation of ``warnings.showwarning()`` and be appended to a list
returned by the context manager. Otherwise None is returned by the
context manager. The objects appended to the list are arguments whose
attributes mirror the arguments to ``showwarning()``.

NOTE: nibabel difference from numpy: default is True

modules : sequence, optional
Sequence of modules for which to reset warnings registry on entry and
restore on exit

Examples
--------
>>> import warnings
>>> with clear_and_catch_warnings(modules=[np.core.fromnumeric]):
... warnings.simplefilter('always')
... # do something that raises a warning in np.core.fromnumeric
"""
def __init__(self, *args, **kwargs):
self.modules = kwargs.pop('modules', [])
class_modules = ()

def __init__(self, record=True, modules=()):
self.modules = set(modules).union(self.class_modules)
self._warnreg_copies = {}
super(catch_warn_reset, self).__init__(*args, **kwargs)
super(clear_and_catch_warnings, self).__init__(record=record)

def __enter__(self):
for mod in self.modules:
if hasattr(mod, '__warningregistry__'):
mod_reg = mod.__warningregistry__
self._warnreg_copies[mod] = mod_reg.copy()
mod_reg.clear()
return super(catch_warn_reset, self).__enter__()
return super(clear_and_catch_warnings, self).__enter__()

def __exit__(self, *exc_info):
super(catch_warn_reset, self).__exit__(*exc_info)
super(clear_and_catch_warnings, self).__exit__(*exc_info)
for mod in self.modules:
if hasattr(mod, '__warningregistry__'):
mod.__warningregistry__.clear()
if mod in self._warnreg_copies:
mod.__warningregistry__.update(self._warnreg_copies[mod])


class error_warnings(clear_and_catch_warnings):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inherits from clear_and_catch_warnings.

""" Context manager to check for warnings as errors. Usually used with
``assert_raises`` in the with block

Examples
--------
>>> with error_warnings():
... try:
... warnings.warn('Message', UserWarning)
... except UserWarning:
... print('I consider myself warned')
I consider myself warned
"""
filter = 'error'

def __enter__(self):
mgr = super(error_warnings, self).__enter__()
warnings.simplefilter(self.filter)
return mgr


class suppress_warnings(error_warnings):
""" Version of ``catch_warnings`` class that suppresses warnings
"""
filter = 'ignore'


class catch_warn_reset(clear_and_catch_warnings):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed docstring, and all functions take *args, **kwargs for all deprecated classes.

def __init__(self, *args, **kwargs):
warnings.warn('catch_warn_reset is deprecated and will be removed in '
'nibabel v3.0; use nibabel.testing.clear_and_catch_warnings.',
FutureWarning)
13 changes: 4 additions & 9 deletions nibabel/tests/test_arraywriters.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,19 @@
import numpy as np

from ..externals.six import BytesIO

from ..arraywriters import (SlopeInterArrayWriter, SlopeArrayWriter,
WriterError, ScalingError, ArrayWriter,
make_array_writer, get_slope_inter)

from ..casting import int_abs, type_info, shared_range, on_powerpc

from ..volumeutils import array_from_file, apply_read_scaling, _dt_min_max

from numpy.testing import (assert_array_almost_equal,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drive-by linting. I couldn't resist!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:)

assert_array_equal)

from nose.tools import (assert_true, assert_false,
assert_equal, assert_not_equal,
assert_raises)

from ..testing import assert_allclose_safely, suppress_warnings
from ..checkwarns import ErrorWarnings
from ..testing import (assert_allclose_safely, suppress_warnings,
error_warnings)


FLOAT_TYPES = np.sctypes['float']
Expand Down Expand Up @@ -524,7 +519,7 @@ def test_nan2zero():
data_back = round_trip(aw)
assert_array_equal(np.isnan(data_back), [True, False])
# Deprecation warning for nan2zero as argument to `to_fileobj`
with ErrorWarnings():
with error_warnings():
assert_raises(DeprecationWarning,
aw.to_fileobj, BytesIO(), 'F', True)
assert_raises(DeprecationWarning,
Expand All @@ -545,7 +540,7 @@ def test_nan2zero():
astype_res = np.array(np.nan).astype(np.int32)
assert_array_equal(data_back, [astype_res, 99])
# Deprecation warning for nan2zero as argument to `to_fileobj`
with ErrorWarnings():
with error_warnings():
assert_raises(DeprecationWarning,
aw.to_fileobj, BytesIO(), 'F', False)
assert_raises(DeprecationWarning,
Expand Down
45 changes: 12 additions & 33 deletions nibabel/tests/test_checkwarns.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,21 @@
""" Tests for warnings context managers
"""

from __future__ import division, print_function, absolute_import

from warnings import warn, simplefilter, filters

from ..checkwarns import ErrorWarnings, IgnoreWarnings

from nose.tools import assert_true, assert_equal, assert_raises
from ..testing import clear_and_catch_warnings, suppress_warnings


def test_warn_error():
# Check warning error context manager
n_warns = len(filters)
with ErrorWarnings():
assert_raises(UserWarning, warn, 'A test')
with ErrorWarnings() as w: # w not used for anything
assert_raises(UserWarning, warn, 'A test')
assert_equal(n_warns, len(filters))
# Check other errors are propagated
def f():
with ErrorWarnings():
raise ValueError('An error')
assert_raises(ValueError, f)
def test_ignore_and_error_warnings():
with suppress_warnings():
from .. import checkwarns
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing what is happening here is that nose has previously imported checkwarns as part of test discovery for the while test-suite, and therefore there is no Future warning, because the import has already happened.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way - I do see the error if I run the full test-suite locally, but not if I run the individual test.


with clear_and_catch_warnings() as w:
checkwarns.IgnoreWarnings()
assert_equal(len(w), 1)
assert_equal(w[0].category, FutureWarning)

def test_warn_ignore():
# Check warning ignore context manager
n_warns = len(filters)
with IgnoreWarnings():
warn('Here is a warning, you will not see it')
warn('Nor this one', DeprecationWarning)
with IgnoreWarnings() as w: # w not used
warn('Here is a warning, you will not see it')
warn('Nor this one', DeprecationWarning)
assert_equal(n_warns, len(filters))
# Check other errors are propagated
def f():
with IgnoreWarnings():
raise ValueError('An error')
assert_raises(ValueError, f)
with clear_and_catch_warnings() as w:
checkwarns.ErrorWarnings()
assert_equal(len(w), 1)
assert_equal(w[0].category, FutureWarning)
5 changes: 3 additions & 2 deletions nibabel/tests/test_openers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@
from bz2 import BZ2File
from io import BytesIO, UnsupportedOperation

from ..checkwarns import ErrorWarnings
from ..py3k import asstr, asbytes
from ..openers import Opener, ImageOpener
from ..tmpdirs import InTemporaryDirectory
from ..volumeutils import BinOpener

from nose.tools import (assert_true, assert_false, assert_equal,
assert_not_equal, assert_raises)
from ..testing import error_warnings


class Lunk(object):
# bare file-like for testing
Expand Down Expand Up @@ -84,7 +85,7 @@ def test_Opener_various():
assert_not_equal(fobj.fileno(), 0)

def test_BinOpener():
with ErrorWarnings():
with error_warnings():
assert_raises(DeprecationWarning,
BinOpener, 'test.txt', 'r')

Expand Down
10 changes: 5 additions & 5 deletions nibabel/tests/test_parrec.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from nose.tools import (assert_true, assert_false, assert_raises,
assert_equal)

from ..testing import catch_warn_reset, suppress_warnings
from ..testing import clear_and_catch_warnings, suppress_warnings

from .test_arrayproxy import check_mmap
from . import test_spatialimages as tsi
Expand Down Expand Up @@ -237,7 +237,7 @@ def test_affine_regression():

def test_get_voxel_size_deprecated():
hdr = PARRECHeader(HDR_INFO, HDR_DEFS)
with catch_warn_reset(modules=[parrec], record=True) as wlist:
with clear_and_catch_warnings(modules=[parrec], record=True) as wlist:
simplefilter('always')
hdr.get_voxel_size()
assert_equal(wlist[0].category, DeprecationWarning)
Expand All @@ -255,7 +255,7 @@ def test_get_sorted_slice_indices():
17, 16, 15, 14, 13, 12, 11, 10, 9,
26, 25, 24, 23, 22, 21, 20, 19, 18])
# Omit last slice, only two volumes
with catch_warn_reset(modules=[parrec], record=True):
with clear_and_catch_warnings(modules=[parrec], record=True):
hdr = PARRECHeader(HDR_INFO, HDR_DEFS[:-1], permit_truncated=True)
assert_array_equal(hdr.get_sorted_slice_indices(), range(n_slices - 9))

Expand Down Expand Up @@ -300,7 +300,7 @@ def test_truncated_load():
with open(TRUNC_PAR, 'rt') as fobj:
gen_info, slice_info = parse_PAR_header(fobj)
assert_raises(PARRECError, PARRECHeader, gen_info, slice_info)
with catch_warn_reset(record=True) as wlist:
with clear_and_catch_warnings(record=True) as wlist:
hdr = PARRECHeader(gen_info, slice_info, True)
assert_equal(len(wlist), 1)

Expand Down Expand Up @@ -373,7 +373,7 @@ def test_truncations():
# Drop one line, raises error
assert_raises(PARRECError, PARRECHeader, gen_info, slice_info[:-1])
# When we are permissive, we raise a warning, and drop a volume
with catch_warn_reset(modules=[parrec], record=True) as wlist:
with clear_and_catch_warnings(modules=[parrec], record=True) as wlist:
hdr = PARRECHeader(gen_info, slice_info[:-1], permit_truncated=True)
assert_equal(len(wlist), 1)
assert_equal(hdr.get_data_shape(), (80, 80, 10))
Expand Down
Loading