diff --git a/skimage/_shared/_warnings.py b/skimage/_shared/_warnings.py new file mode 100644 index 00000000..72de07b7 --- /dev/null +++ b/skimage/_shared/_warnings.py @@ -0,0 +1,61 @@ +__all__ = ['all_warnings'] + +from contextlib import contextmanager +import sys +import warnings +import inspect + + +@contextmanager +def all_warnings(): + """ + Context for use in testing to ensure that all warnings are raised. + + Examples + -------- + >>> import warnings + >>> def foo(): + ... warnings.warn(RuntimeWarning("bar")) + + We raise the warning once, while the warning filter is set to "once". + Hereafter, the warning is invisible, even with custom filters: + + >>> with warnings.catch_warnings(): + ... warnings.simplefilter('once') + ... foo() + + We can now run ``foo()`` without a warning being raised: + + >>> from numpy.testing import assert_warns + >>> foo() + + To catch the warning, we call in the help of ``all_warnings``: + + >>> with all_warnings(): + ... assert_warns(RuntimeWarning, foo) + """ + + # Whenever a warning is triggered, Python adds a __warningregistry__ + # member to the *calling* module. The exercize here is to find + # and eradicate all those breadcrumbs that were left lying around. + # + # We proceed by first searching all parent calling frames and explicitly + # clearing their warning registries (necessary for the doctests above to + # pass). Then, we search for all submodules of skimage and clear theirs + # as well (necessary for the skimage test suite to pass). + + frame = inspect.currentframe() + if frame: + for f in inspect.getouterframes(frame): + f[0].f_locals['__warningregistry__'] = {} + del frame + + for mod in list(sys.modules.values()): + try: + mod.__warningregistry__.clear() + except AttributeError: + pass + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + yield w diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index 980ff639..9148e7ff 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -4,8 +4,9 @@ import sys import six +from ._warnings import all_warnings -__all__ = ['deprecated', 'get_bound_method_class'] +__all__ = ['deprecated', 'get_bound_method_class', 'all_warnings'] class skimage_deprecation(Warning): diff --git a/skimage/util/shape.py b/skimage/util/shape.py index 95e0a42e..eed6b952 100644 --- a/skimage/util/shape.py +++ b/skimage/util/shape.py @@ -2,6 +2,7 @@ __all__ = ['view_as_blocks', 'view_as_windows'] import numpy as np from numpy.lib.stride_tricks import as_strided +from warnings import warn def view_as_blocks(arr_in, block_shape): @@ -11,17 +12,17 @@ def view_as_blocks(arr_in, block_shape): Parameters ---------- - arr_in: ndarray - The n-dimensional input array. - - block_shape: tuple + arr_in : ndarray + N-d input array. + block_shape : tuple The shape of the block. Each dimension must divide evenly into the corresponding dimensions of `arr_in`. Returns ------- - arr_out: ndarray - Block view of the input array. + arr_out : ndarray + Block view of the input array. If `arr_in` is non-contiguous, a copy + is made. Examples -------- @@ -70,8 +71,6 @@ def view_as_blocks(arr_in, block_shape): [[[76, 77], [82, 83]]]]) """ - - # -- basic checks on arguments if not isinstance(block_shape, tuple): raise TypeError('block needs to be a tuple') @@ -88,6 +87,11 @@ def view_as_blocks(arr_in, block_shape): raise ValueError("'block_shape' is not compatible with 'arr_in'") # -- restride the array to build the block view + + if not arr_in.flags.contiguous: + warn(RuntimeWarning("Cannot provide views on a non-contiguous input " + "array without copying.")) + arr_in = np.ascontiguousarray(arr_in) new_shape = tuple(arr_shape / block_shape) + tuple(block_shape) @@ -106,9 +110,9 @@ def view_as_windows(arr_in, window_shape, step=1): Parameters ---------- - arr_in: ndarray - The n-dimensional input array. - window_shape: tuple + arr_in : ndarray + N-d input array. + window_shape : tuple Defines the shape of the elementary n-dimensional orthotope (better know as hyperrectangle [1]_) of the rolling window view. step : int, optional @@ -118,8 +122,9 @@ def view_as_windows(arr_in, window_shape, step=1): Returns ------- - arr_out: ndarray - (rolling) window view of the input array. + arr_out : ndarray + (rolling) window view of the input array. If `arr_in` is + non-contiguous, a copy is made. Notes ----- @@ -228,6 +233,10 @@ def view_as_windows(arr_in, window_shape, step=1): raise ValueError("`window_shape` is too small") # -- build rolling window view + if not arr_in.flags.contiguous: + warn(RuntimeWarning("Cannot provide views on a non-contiguous input " + "array without copying.")) + arr_in = np.ascontiguousarray(arr_in) new_shape = tuple((arr_shape - window_shape) // step + 1) + \ diff --git a/skimage/util/tests/test_shape.py b/skimage/util/tests/test_shape.py index b66ac2e2..ee897e7c 100644 --- a/skimage/util/tests/test_shape.py +++ b/skimage/util/tests/test_shape.py @@ -1,7 +1,9 @@ import numpy as np from nose.tools import raises -from numpy.testing import assert_equal +from numpy.testing import assert_equal, assert_warns + from skimage.util.shape import view_as_blocks, view_as_windows +from skimage._shared.utils import all_warnings @raises(TypeError) @@ -131,7 +133,7 @@ def test_view_as_windows_2D(): [17, 18, 19]]]])) -def test_view_as_windows_With_skip(): +def test_view_as_windows_with_skip(): A = np.arange(20).reshape((5, 4)) B = view_as_windows(A, (2, 2), step=2) assert_equal(B, [[[[0, 1], @@ -147,5 +149,14 @@ def test_view_as_windows_With_skip(): assert_equal(C.shape, (1, 1, 2, 2)) +def test_views_non_contiguous(): + A = np.arange(16).reshape((4, 4)) + A = A[::2, :] + + with all_warnings(): + assert_warns(RuntimeWarning, view_as_blocks, A, (2, 2)) + assert_warns(RuntimeWarning, view_as_windows, A, (2, 2)) + + if __name__ == '__main__': np.testing.run_module_suite()