diff --git a/nibabel/deprecated.py b/nibabel/deprecated.py index aa41675dbd..ab9f31c8cf 100644 --- a/nibabel/deprecated.py +++ b/nibabel/deprecated.py @@ -1,7 +1,9 @@ """Module to help with deprecating objects and classes """ +from __future__ import annotations import warnings +from typing import Type from .deprecator import Deprecator from .pkg_info import cmp_pkg_version @@ -77,3 +79,40 @@ class VisibleDeprecationWarning(UserWarning): deprecate_with_version = Deprecator(cmp_pkg_version) + + +def alert_future_error( + msg: str, + version: str, + *, + warning_class: Type[Warning] = FutureWarning, + error_class: Type[Exception] = RuntimeError, + warning_rec: str = '', + error_rec: str = '', + stacklevel: int = 2, +): + """Warn or error with appropriate messages for changing functionality. + + Parameters + ---------- + msg : str + Description of the condition that led to the alert + version : str + NiBabel version at which the warning will become an error + warning_class : subclass of Warning, optional + Warning class to emit before version + error_class : subclass of Exception, optional + Error class to emit after version + warning_rec : str, optional + Guidance for suppressing the warning and avoiding the future error + error_rec: str, optional + Guidance for resolving the error + stacklevel: int, optional + Warnings stacklevel to provide; note that this will be incremented by + 1, so provide the stacklevel you would provide directly to warnings.warn() + """ + if cmp_pkg_version(version) >= 0: + msg = f'{msg} This will error in NiBabel {version}. {warning_rec}' + warnings.warn(msg.strip(), warning_class, stacklevel=stacklevel + 1) + else: + raise error_class(f'{msg} {error_rec}'.strip()) diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index a10686145b..61a6da3660 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -21,6 +21,7 @@ from .arrayproxy import get_obj_dtype from .batteryrunners import Report from .casting import have_binary128 +from .deprecated import alert_future_error from .filebasedimages import SerializableImage from .optpkg import optional_package from .quaternions import fillpositive, mat2quat, quat2mat @@ -1831,13 +1832,16 @@ def __init__(self, dataobj, affine, header=None, extra=None, file_map=None, dtyp # already fail. danger_dts = (np.dtype('int64'), np.dtype('uint64')) if header is None and dtype is None and get_obj_dtype(dataobj) in danger_dts: - msg = ( + alert_future_error( f'Image data has type {dataobj.dtype}, which may cause ' - 'incompatibilities with other tools. This will error in ' - 'NiBabel 5.0. This warning can be silenced ' - f'by passing the dtype argument to {self.__class__.__name__}().' + 'incompatibilities with other tools.', + '5.0', + warning_rec='This warning can be silenced by passing the dtype argument' + f' to {self.__class__.__name__}().', + error_rec='To use this type, pass an explicit header or dtype argument' + f' to {self.__class__.__name__}().', + error_class=ValueError, ) - warnings.warn(msg, FutureWarning, stacklevel=2) super().__init__(dataobj, affine, header, extra, file_map, dtype) # Force set of s/q form when header is None unless affine is also None if header is None and affine is not None: diff --git a/nibabel/tests/test_deprecated.py b/nibabel/tests/test_deprecated.py index 962f9c0827..8b9f6c360f 100644 --- a/nibabel/tests/test_deprecated.py +++ b/nibabel/tests/test_deprecated.py @@ -6,7 +6,12 @@ import pytest from nibabel import pkg_info -from nibabel.deprecated import FutureWarningMixin, ModuleProxy, deprecate_with_version +from nibabel.deprecated import ( + FutureWarningMixin, + ModuleProxy, + alert_future_error, + deprecate_with_version, +) from nibabel.tests.test_deprecator import TestDeprecatorFunc as _TestDF @@ -79,3 +84,28 @@ def func(): assert func() == 99 finally: pkg_info.cmp_pkg_version.__defaults__ = ('2.0',) + + +def test_alert_future_error(): + with pytest.warns(FutureWarning): + alert_future_error( + 'Message', + '9999.9.9', + warning_rec='Silence this warning by doing XYZ.', + error_rec='Fix this issue by doing XYZ.', + ) + with pytest.raises(RuntimeError): + alert_future_error( + 'Message', + '1.0.0', + warning_rec='Silence this warning by doing XYZ.', + error_rec='Fix this issue by doing XYZ.', + ) + with pytest.raises(ValueError): + alert_future_error( + 'Message', + '1.0.0', + warning_rec='Silence this warning by doing XYZ.', + error_rec='Fix this issue by doing XYZ.', + error_class=ValueError, + ) diff --git a/nibabel/tests/test_nifti1.py b/nibabel/tests/test_nifti1.py index 2cbbfc1f5d..808d06c15a 100644 --- a/nibabel/tests/test_nifti1.py +++ b/nibabel/tests/test_nifti1.py @@ -35,6 +35,7 @@ slice_order_codes, ) from nibabel.optpkg import optional_package +from nibabel.pkg_info import cmp_pkg_version from nibabel.spatialimages import HeaderDataError from nibabel.tmpdirs import InTemporaryDirectory @@ -766,16 +767,21 @@ class TestNifti1Pair(tana.TestAnalyzeImage, tspm.ImageScalingMixin): image_class = Nifti1Pair supported_np_types = TestNifti1PairHeader.supported_np_types - def test_int64_warning(self): + def test_int64_warning_or_error(self): # Verify that initializing with (u)int64 data and no - # header/dtype info produces a warning + # header/dtype info produces a warning/error img_klass = self.image_class hdr_klass = img_klass.header_class for dtype in (np.int64, np.uint64): data = np.arange(24, dtype=dtype).reshape((2, 3, 4)) - with pytest.warns(FutureWarning): + # Starts as a warning, transitions to error at 5.0 + if cmp_pkg_version('5.0') < 0: + cm = pytest.raises(ValueError) + else: + cm = pytest.warns(FutureWarning) + with cm: img_klass(data, np.eye(4)) - # No warnings if we're explicit, though + # No problems if we're explicit, though with clear_and_catch_warnings(): warnings.simplefilter('error') img_klass(data, np.eye(4), dtype=dtype)