Skip to content

ENH: Schedule int64 warning to convert to error at 5.0 #1173

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 2 commits into from
Jan 6, 2023
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
39 changes: 39 additions & 0 deletions nibabel/deprecated.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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())
14 changes: 9 additions & 5 deletions nibabel/nifti1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
32 changes: 31 additions & 1 deletion nibabel/tests/test_deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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,
)
14 changes: 10 additions & 4 deletions nibabel/tests/test_nifti1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down