diff --git a/nibabel/deprecator.py b/nibabel/deprecator.py index 81e93d868e..031a05e601 100644 --- a/nibabel/deprecator.py +++ b/nibabel/deprecator.py @@ -182,8 +182,18 @@ def deprecated_func(*args, **kwargs): warnings.warn(message, warn_class, stacklevel=2) return func(*args, **kwargs) - deprecated_func.__doc__ = _add_dep_doc(deprecated_func.__doc__, - message, TESTSETUP, TESTCLEANUP) + keep_doc = deprecated_func.__doc__ + setup = TESTSETUP + cleanup = TESTCLEANUP + # After expiration, remove all but the first paragraph. + # The details are no longer relevant, but any code will likely + # raise exceptions we don't need. + if keep_doc and until and self.is_bad_version(until): + lines = '\n'.join(line.rstrip() for line in keep_doc.splitlines()) + keep_doc = lines.split('\n\n', 1)[0] + setup = '' + cleanup = '' + deprecated_func.__doc__ = _add_dep_doc(keep_doc, message, setup, cleanup) return deprecated_func return deprecator diff --git a/nibabel/testing/__init__.py b/nibabel/testing/__init__.py index b54c138bf0..8c9411ec91 100644 --- a/nibabel/testing/__init__.py +++ b/nibabel/testing/__init__.py @@ -16,6 +16,7 @@ import unittest +import pytest import numpy as np from numpy.testing import assert_array_equal @@ -223,3 +224,15 @@ def setUp(self): if self.__class__.__name__.startswith('_'): raise unittest.SkipTest("Base test case - subclass to run") super().setUp() + + +def expires(version): + "Decorator to mark a test as xfail with ExpiredDeprecationError after version" + from packaging.version import Version + from nibabel import __version__ as nbver + from nibabel.deprecator import ExpiredDeprecationError + + if Version(nbver) < Version(version): + return lambda x: x + + return pytest.mark.xfail(raises=ExpiredDeprecationError) diff --git a/nibabel/tests/test_analyze.py b/nibabel/tests/test_analyze.py index d91769bc73..7f32e2d8a7 100644 --- a/nibabel/tests/test_analyze.py +++ b/nibabel/tests/test_analyze.py @@ -716,7 +716,7 @@ def test_default_header(self): def test_data_hdr_cache(self): # test the API for loaded images, such that the data returned - # from np.asanyarray(img.dataobj) and img,get_fdata() are not + # from np.asanyarray(img.dataobj) and img.get_fdata() are not # affected by subsequent changes to the header. IC = self.image_class # save an image to a file map @@ -740,14 +740,6 @@ def test_data_hdr_cache(self): assert hdr.get_data_dtype() == np.dtype(np.uint8) assert_array_equal(img2.get_fdata(), data) assert_array_equal(np.asanyarray(img2.dataobj), data) - # now check read_img_data function - here we do see the changed - # header - with pytest.deprecated_call(match="from version: 3.2"): - sc_data = read_img_data(img2) - assert sc_data.shape == (3, 2, 2) - with pytest.deprecated_call(match="from version: 3.2"): - us_data = read_img_data(img2, prefer='unscaled') - assert us_data.shape == (3, 2, 2) def test_affine_44(self): IC = self.image_class diff --git a/nibabel/tests/test_api_validators.py b/nibabel/tests/test_api_validators.py index f1c592ce13..54c1c0fd95 100644 --- a/nibabel/tests/test_api_validators.py +++ b/nibabel/tests/test_api_validators.py @@ -20,6 +20,8 @@ def meth(self): validator(self, imaker, params) meth.__name__ = 'test_' + name[len('validate_'):] meth.__doc__ = f'autogenerated test from {klass.__name__}.{name}' + if hasattr(validator, 'pytestmark'): + meth.pytestmark = validator.pytestmark return meth for name in dir(klass): if not name.startswith('validate_'): diff --git a/nibabel/tests/test_deprecator.py b/nibabel/tests/test_deprecator.py index 2e7a0b9ba9..0280692299 100644 --- a/nibabel/tests/test_deprecator.py +++ b/nibabel/tests/test_deprecator.py @@ -111,11 +111,14 @@ def test_dep_func(self): 'foo\n\n* deprecated from version: 1.2\n* Raises ' f'{ExpiredDeprecationError} as of version: 1.8\n') func = dec('foo', '1.2', '1.8')(func_doc_long) - assert (func.__doc__ == - 'A docstring\n \n foo\n \n * deprecated from version: 1.2\n ' - f'* Raises {ExpiredDeprecationError} as of version: 1.8\n \n' - f'{indent(TESTSETUP, " ", lambda x: True)}' - f' Some text\n{indent(TESTCLEANUP, " ", lambda x: True)}') + assert func.__doc__ == f"""\ +A docstring + +foo + +* deprecated from version: 1.2 +* Raises {ExpiredDeprecationError} as of version: 1.8 +""" with pytest.raises(ExpiredDeprecationError): func() diff --git a/nibabel/tests/test_image_api.py b/nibabel/tests/test_image_api.py index 16003fd79c..a12227a894 100644 --- a/nibabel/tests/test_image_api.py +++ b/nibabel/tests/test_image_api.py @@ -48,7 +48,7 @@ from numpy.testing import assert_almost_equal, assert_array_equal, assert_warns, assert_allclose from nibabel.testing import (bytesio_round_trip, bytesio_filemap, assert_data_similar, - clear_and_catch_warnings, nullcontext) + clear_and_catch_warnings, nullcontext, expires) from ..tmpdirs import InTemporaryDirectory from .test_api_validators import ValidateAPI @@ -170,8 +170,8 @@ def validate_no_slicing(self, imaker, params): with pytest.raises(TypeError): img[:] + @expires("5.0.0") def validate_get_data_deprecated(self, imaker, params): - # Check deprecated header API img = imaker() with pytest.deprecated_call(): data = img.get_data() @@ -209,7 +209,7 @@ class DataInterfaceMixin(GetSetDtypeMixin): Use this mixin if your image has a ``dataobj`` property that contains an array or an array-like thing. """ - meth_names = ('get_fdata', 'get_data') + meth_names = ('get_fdata',) def validate_data_interface(self, imaker, params): # Check get data returns array, and caches @@ -304,27 +304,6 @@ def _check_proxy_interface(self, imaker, meth_name): with maybe_deprecated(meth_name): data_again = method() assert data is data_again - # Check the interaction of caching with get_data, get_fdata. - # Caching for `get_data` should have no effect on caching for - # get_fdata, and vice versa. - # Modify the cached data - data[:] = 43 - # Load using the other data fetch method - other_name = set(self.meth_names).difference({meth_name}).pop() - other_method = getattr(img, other_name) - with maybe_deprecated(other_name): - other_data = other_method() - # We get the original data, not the modified cache - assert_array_equal(proxy_data, other_data) - assert not np.all(data == other_data) - # We can modify the other cache, without affecting the first - other_data[:] = 44 - with maybe_deprecated(other_name): - assert_array_equal(other_method(), 44) - with pytest.deprecated_call(): - assert not np.all(method() == other_method()) - if meth_name != 'get_fdata': - return # Check that caching refreshes for new floating point type. img.uncache() fdata = img.get_fdata() @@ -558,7 +537,7 @@ def validate_to_from_bytes(self, imaker, params): del img_b @pytest.fixture(autouse=True) - def setup(self, httpserver, tmp_path): + def setup_method(self, httpserver, tmp_path): """Make pytest fixtures available to validate functions""" self.httpserver = httpserver self.tmp_path = tmp_path @@ -788,7 +767,7 @@ class TestMinc1API(ImageHeaderAPI): class TestMinc2API(TestMinc1API): - def setup(self): + def setup_method(self): if not have_h5py: raise unittest.SkipTest('Need h5py for these tests') diff --git a/nibabel/tests/test_image_load_save.py b/nibabel/tests/test_image_load_save.py index 12a49ecd7d..c23d145a36 100644 --- a/nibabel/tests/test_image_load_save.py +++ b/nibabel/tests/test_image_load_save.py @@ -29,6 +29,7 @@ from ..volumeutils import native_code, swapped_code from ..optpkg import optional_package from ..spatialimages import SpatialImage +from ..testing import expires from numpy.testing import assert_array_equal, assert_array_almost_equal import pytest @@ -270,6 +271,7 @@ def test_filename_save(): shutil.rmtree(pth) +@expires('5.0.0') def test_guessed_image_type(): # Test whether we can guess the image type from example files with pytest.deprecated_call(): diff --git a/nibabel/tests/test_loadsave.py b/nibabel/tests/test_loadsave.py index c58b95d8e8..799952b57d 100644 --- a/nibabel/tests/test_loadsave.py +++ b/nibabel/tests/test_loadsave.py @@ -14,6 +14,7 @@ from ..filebasedimages import ImageFileError from ..tmpdirs import InTemporaryDirectory, TemporaryDirectory from ..openers import Opener +from ..testing import expires from ..optpkg import optional_package _, have_scipy, _ = optional_package('scipy') @@ -27,6 +28,7 @@ data_path = pjoin(dirname(__file__), 'data') +@expires("5.0.0") def test_read_img_data(): fnames_test = [ 'example4d.nii.gz', @@ -120,6 +122,7 @@ def test_signature_matches_extension(tmp_path): assert msg == "" +@expires("5.0.0") def test_read_img_data_nifti(): shape = (2, 3, 4) data = np.random.normal(size=shape) diff --git a/nibabel/tests/test_onetime.py b/nibabel/tests/test_onetime.py index 3f0c25a7d3..c1609980a3 100644 --- a/nibabel/tests/test_onetime.py +++ b/nibabel/tests/test_onetime.py @@ -1,7 +1,9 @@ import pytest from nibabel.onetime import auto_attr, setattr_on_read +from nibabel.testing import expires +@expires('5.0.0') def test_setattr_on_read(): with pytest.deprecated_call(): class MagicProp: @@ -15,3 +17,17 @@ def a(self): assert 'a' in x.__dict__ # Each call to object() produces a unique object. Verify we get the same one every time. assert x.a is obj + + +def test_auto_attr(): + class MagicProp: + @auto_attr + def a(self): + return object() + + x = MagicProp() + assert 'a' not in x.__dict__ + obj = x.a + assert 'a' in x.__dict__ + # Each call to object() produces a unique object. Verify we get the same one every time. + assert x.a is obj diff --git a/nibabel/tests/test_orientations.py b/nibabel/tests/test_orientations.py index 77b892acbc..0b3b8081d0 100644 --- a/nibabel/tests/test_orientations.py +++ b/nibabel/tests/test_orientations.py @@ -20,6 +20,7 @@ ornt2axcodes, axcodes2ornt, aff2axcodes) from ..affines import from_matvec, to_matvec +from ..testing import expires IN_ARRS = [np.eye(4), @@ -353,6 +354,7 @@ def test_inv_ornt_aff(): inv_ornt_aff([[0, 1], [1, -1], [np.nan, np.nan]], (3, 4, 5)) +@expires('5.0.0') def test_flip_axis_deprecation(): a = np.arange(24).reshape((2, 3, 4)) axis = 1 diff --git a/nibabel/tests/test_spatialimages.py b/nibabel/tests/test_spatialimages.py index fc11452151..52eff4be72 100644 --- a/nibabel/tests/test_spatialimages.py +++ b/nibabel/tests/test_spatialimages.py @@ -26,7 +26,8 @@ bytesio_round_trip, clear_and_catch_warnings, suppress_warnings, - memmap_after_ufunc + memmap_after_ufunc, + expires, ) from ..tmpdirs import InTemporaryDirectory @@ -358,21 +359,13 @@ def test_get_fdata(self): assert rt_img.get_fdata() is not out_data assert (rt_img.get_fdata() == in_data).all() + @expires("5.0.0") def test_get_data(self): # Test array image and proxy image interface img_klass = self.image_class in_data_template = np.arange(24, dtype=np.int16).reshape((2, 3, 4)) in_data = in_data_template.copy() img = img_klass(in_data, None) - # Can't slice into the image object: - with pytest.raises(TypeError) as exception_manager: - img[0, 0, 0] - # Make sure the right message gets raised: - assert (str(exception_manager.value) == - "Cannot slice image objects; consider using " - "`img.slicer[slice]` to generate a sliced image (see " - "documentation for caveats) or slicing image array data " - "with `img.dataobj[slice]` or `img.get_fdata()[slice]`") assert in_data is img.dataobj with pytest.deprecated_call(): out_data = img.get_data() @@ -411,6 +404,16 @@ def test_slicer(self): in_data = in_data_template.copy().reshape(dshape) img = img_klass(in_data, base_affine.copy()) + # Can't slice into the image object: + with pytest.raises(TypeError) as exception_manager: + img[0, 0, 0] + # Make sure the right message gets raised: + assert (str(exception_manager.value) == + "Cannot slice image objects; consider using " + "`img.slicer[slice]` to generate a sliced image (see " + "documentation for caveats) or slicing image array data " + "with `img.dataobj[slice]` or `img.get_fdata()[slice]`") + if not spatial_axes_first(img): with pytest.raises(ValueError): img.slicer @@ -519,13 +522,9 @@ def test_slicer(self): pass else: sliced_data = in_data[sliceobj] - with pytest.deprecated_call(): - assert (sliced_data == sliced_img.get_data()).all() assert (sliced_data == sliced_img.get_fdata()).all() assert (sliced_data == sliced_img.dataobj).all() assert (sliced_data == img.dataobj[sliceobj]).all() - with pytest.deprecated_call(): - assert (sliced_data == img.get_data()[sliceobj]).all() assert (sliced_data == img.get_fdata()[sliceobj]).all()