Skip to content

Commit fd5c84b

Browse files
committed
Merge remote-tracking branch 'upstream/maint/4.0.x'
2 parents 5719019 + e3f55df commit fd5c84b

File tree

7 files changed

+206
-32
lines changed

7 files changed

+206
-32
lines changed

Changelog

+16
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,22 @@ Eric Larson (EL), Demian Wassermann, Stephan Gerhard and Ross Markello (RM).
2525

2626
References like "pr/298" refer to github pull request numbers.
2727

28+
4.0.2 (Wednesday 31 August 2022)
29+
================================
30+
31+
Bug-fix release in the 4.0.x series.
32+
33+
Bug fixes
34+
---------
35+
* Make ``GiftiMetaData.data`` a list proxy, deprecate (pr/1127) (CM, reviewed
36+
by Hao-Ting Wang)
37+
38+
Maintenance
39+
-----------
40+
* Finalize deprecation of ``ArrayWriter.to_fileobj(nan2zero=...)`` argument
41+
(pr/1126) (CM)
42+
43+
2844
4.0.1 (Saturday 18 June 2022)
2945
=============================
3046

nibabel/arraywriters.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,12 @@ def __init__(self, array, out_dtype=None)
2929
larger ints and smaller.
3030
"""
3131

32-
import warnings
33-
3432
import numpy as np
3533

3634
from .casting import (int_to_float, as_int, int_abs, type_info, floor_exact,
3735
best_float, shared_range)
3836
from .volumeutils import finite_range, array_to_file
37+
from .deprecator import ExpiredDeprecationError
3938

4039

4140
class WriterError(Exception):
@@ -192,11 +191,12 @@ def _check_nan2zero(self, nan2zero):
192191
if nan2zero != self._nan2zero:
193192
raise WriterError('Deprecated `nan2zero` argument to `to_fileobj` '
194193
'must be same as class value set in __init__')
195-
warnings.warn('Please remove `nan2zero` from call to ' '`to_fileobj` '
196-
'and use in instance __init__ instead.\n'
197-
'* deprecated in version: 2.0\n'
198-
'* will raise error in version: 4.0\n',
199-
DeprecationWarning, stacklevel=3)
194+
raise ExpiredDeprecationError(
195+
'Please remove `nan2zero` from call to `to_fileobj` '
196+
'and use in instance __init__ instead.\n'
197+
'* deprecated in version: 2.0\n'
198+
'* Raises ExpiredDeprecationError as of version: 4.0\n'
199+
)
200200

201201
def _needs_nan2zero(self):
202202
""" True if nan2zero check needed for writing array """

nibabel/gifti/gifti.py

+83-7
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,42 @@
2626
from ..deprecated import deprecate_with_version
2727

2828

29+
class _GiftiMDList(list):
30+
"""List view of GiftiMetaData object that will translate most operations"""
31+
def __init__(self, metadata):
32+
self._md = metadata
33+
super().__init__(
34+
GiftiNVPairs._private_init(k, v, metadata)
35+
for k, v in metadata.items()
36+
)
37+
38+
def append(self, nvpair):
39+
self._md[nvpair.name] = nvpair.value
40+
super().append(nvpair)
41+
42+
def clear(self):
43+
super().clear()
44+
self._md.clear()
45+
46+
def extend(self, iterable):
47+
for nvpair in iterable:
48+
self.append(nvpair)
49+
50+
def insert(self, index, nvpair):
51+
self._md[nvpair.name] = nvpair.value
52+
super().insert(index, nvpair)
53+
54+
def pop(self, index=-1):
55+
nvpair = super().pop(index)
56+
nvpair._container = None
57+
del self._md[nvpair.name]
58+
return nvpair
59+
60+
def remove(self, nvpair):
61+
super().remove(nvpair)
62+
del self._md[nvpair.name]
63+
64+
2965
class GiftiMetaData(CaretMetaData):
3066
""" A sequence of GiftiNVPairs containing metadata for a gifti data array
3167
"""
@@ -72,11 +108,12 @@ def _sanitize(args, kwargs):
72108
return (), {pair.name: pair.value}
73109

74110
@property
111+
@deprecate_with_version(
112+
'The data attribute is deprecated. Use GiftiMetaData object '
113+
'directly as a dict.',
114+
'4.0', '6.0')
75115
def data(self):
76-
warnings.warn(
77-
"GiftiMetaData.data will be a dict in NiBabel 6.0.",
78-
FutureWarning, stacklevel=2)
79-
return [GiftiNVPairs(k, v) for k, v in self._data.items()]
116+
return _GiftiMDList(self)
80117

81118
@classmethod
82119
@deprecate_with_version(
@@ -94,7 +131,7 @@ def get_metadata(self):
94131

95132
@property
96133
@deprecate_with_version(
97-
'metadata property deprecated. Use GiftiMetadata object '
134+
'metadata property deprecated. Use GiftiMetaData object '
98135
'as dict or pass to dict() for a standard dictionary.',
99136
'4.0', '6.0')
100137
def metadata(self):
@@ -113,9 +150,48 @@ class GiftiNVPairs:
113150
name : str
114151
value : str
115152
"""
153+
@deprecate_with_version(
154+
'GiftiNVPairs objects are deprecated. Use the GiftiMetaData object '
155+
'as a dict, instead.',
156+
'4.0', '6.0')
116157
def __init__(self, name=u'', value=u''):
117-
self.name = name
118-
self.value = value
158+
self._name = name
159+
self._value = value
160+
self._container = None
161+
162+
@classmethod
163+
def _private_init(cls, name, value, md):
164+
"""Private init method to provide warning-free experience"""
165+
with warnings.catch_warnings():
166+
warnings.simplefilter('ignore', DeprecationWarning)
167+
self = cls(name, value)
168+
self._container = md
169+
return self
170+
171+
def __eq__(self, other):
172+
if not isinstance(other, GiftiNVPairs):
173+
return NotImplemented
174+
return self.name == other.name and self.value == other.value
175+
176+
@property
177+
def name(self):
178+
return self._name
179+
180+
@name.setter
181+
def name(self, key):
182+
if self._container:
183+
self._container[key] = self._container.pop(self._name)
184+
self._name = key
185+
186+
@property
187+
def value(self):
188+
return self._value
189+
190+
@value.setter
191+
def value(self, val):
192+
if self._container:
193+
self._container[self._name] = val
194+
self._value = val
119195

120196

121197
class GiftiLabelTable(xml.XmlSerializable):

nibabel/gifti/tests/test_gifti.py

+82-2
Original file line numberDiff line numberDiff line change
@@ -228,15 +228,16 @@ def test_labeltable():
228228
def test_metadata():
229229
md = GiftiMetaData(key='value')
230230
# Old initialization methods
231-
nvpair = GiftiNVPairs('key', 'value')
231+
with pytest.warns(DeprecationWarning) as w:
232+
nvpair = GiftiNVPairs('key', 'value')
232233
with pytest.warns(FutureWarning) as w:
233234
md2 = GiftiMetaData(nvpair=nvpair)
234235
assert len(w) == 1
235236
with pytest.warns(DeprecationWarning) as w:
236237
md3 = GiftiMetaData.from_dict({'key': 'value'})
237238
assert md == md2 == md3 == {'key': 'value'}
238239
# .data as a list of NVPairs is going away
239-
with pytest.warns(FutureWarning) as w:
240+
with pytest.warns(DeprecationWarning) as w:
240241
assert md.data[0].name == 'key'
241242
assert md.data[0].value == 'value'
242243
assert len(w) == 2
@@ -245,6 +246,85 @@ def test_metadata():
245246
md.get_metadata()
246247

247248

249+
def test_metadata_list_interface():
250+
md = GiftiMetaData(key='value')
251+
with pytest.warns(DeprecationWarning):
252+
mdlist = md.data
253+
assert len(mdlist) == 1
254+
assert mdlist[0].name == 'key'
255+
assert mdlist[0].value == 'value'
256+
257+
# Modify elements in-place
258+
mdlist[0].name = 'foo'
259+
assert mdlist[0].name == 'foo'
260+
assert 'foo' in md
261+
assert 'key' not in md
262+
assert md['foo'] == 'value'
263+
mdlist[0].value = 'bar'
264+
assert mdlist[0].value == 'bar'
265+
assert md['foo'] == 'bar'
266+
267+
# Append new NVPair
268+
with pytest.warns(DeprecationWarning) as w:
269+
nvpair = GiftiNVPairs('key', 'value')
270+
mdlist.append(nvpair)
271+
assert len(mdlist) == 2
272+
assert mdlist[1].name == 'key'
273+
assert mdlist[1].value == 'value'
274+
assert len(md) == 2
275+
assert md == {'foo': 'bar', 'key': 'value'}
276+
277+
# Clearing empties both
278+
mdlist.clear()
279+
assert len(mdlist) == 0
280+
assert len(md) == 0
281+
282+
# Extension adds multiple keys
283+
with pytest.warns(DeprecationWarning) as w:
284+
foobar = GiftiNVPairs('foo', 'bar')
285+
mdlist.extend([nvpair, foobar])
286+
assert len(mdlist) == 2
287+
assert len(md) == 2
288+
assert md == {'key': 'value', 'foo': 'bar'}
289+
290+
# Insertion updates list order, though we don't attempt to preserve it in the dict
291+
with pytest.warns(DeprecationWarning) as w:
292+
lastone = GiftiNVPairs('last', 'one')
293+
mdlist.insert(1, lastone)
294+
assert len(mdlist) == 3
295+
assert len(md) == 3
296+
assert mdlist[1].name == 'last'
297+
assert mdlist[1].value == 'one'
298+
assert md == {'key': 'value', 'foo': 'bar', 'last': 'one'}
299+
300+
# Popping returns a pair
301+
mypair = mdlist.pop(0)
302+
assert isinstance(mypair, GiftiNVPairs)
303+
assert mypair.name == 'key'
304+
assert mypair.value == 'value'
305+
assert len(mdlist) == 2
306+
assert len(md) == 2
307+
assert 'key' not in md
308+
assert md == {'foo': 'bar', 'last': 'one'}
309+
# Modifying the pair now does not affect md
310+
mypair.name = 'completelynew'
311+
mypair.value = 'strings'
312+
assert 'completelynew' not in md
313+
assert md == {'foo': 'bar', 'last': 'one'}
314+
# Check popping from the end (lastone inserted before foobar)
315+
lastpair = mdlist.pop()
316+
assert len(mdlist) == 1
317+
assert len(md) == 1
318+
assert md == {'last': 'one'}
319+
320+
# And let's remove an old pair with a new object
321+
with pytest.warns(DeprecationWarning) as w:
322+
lastoneagain = GiftiNVPairs('last', 'one')
323+
mdlist.remove(lastoneagain)
324+
assert len(mdlist) == 0
325+
assert len(md) == 0
326+
327+
248328
def test_gifti_label_rgba():
249329
rgba = np.random.rand(4)
250330
kwargs = dict(zip(['red', 'green', 'blue', 'alpha'], rgba))

nibabel/gifti/tests/test_parse_gifti_fast.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def test_default_types():
147147
# GiftiMetaData
148148
assert_default_types(img.meta)
149149
# GiftiNVPairs - Remove in NIB6
150-
with pytest.warns(FutureWarning):
150+
with pytest.warns(DeprecationWarning):
151151
for nvpair in img.meta.data:
152152
assert_default_types(nvpair)
153153
# GiftiLabelTable
@@ -161,7 +161,7 @@ def test_default_types():
161161
# GiftiMetaData
162162
assert_default_types(darray.meta)
163163
# GiftiNVPairs - Remove in NIB6
164-
with pytest.warns(FutureWarning):
164+
with pytest.warns(DeprecationWarning):
165165
for nvpair in darray.meta.data:
166166
assert_default_types(nvpair)
167167

nibabel/tests/test_arraywriters.py

+12-14
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
make_array_writer, get_slope_inter)
1414
from ..casting import int_abs, type_info, shared_range, on_powerpc
1515
from ..volumeutils import array_from_file, apply_read_scaling, _dt_min_max
16+
from ..deprecator import ExpiredDeprecationError
1617

1718
from numpy.testing import assert_array_almost_equal, assert_array_equal
1819
import pytest
19-
from ..testing import (assert_allclose_safely, suppress_warnings,
20-
error_warnings)
20+
from ..testing import assert_allclose_safely, suppress_warnings
2121

2222

2323
FLOAT_TYPES = np.sctypes['float']
@@ -506,12 +506,11 @@ def test_nan2zero():
506506
aw = awt(arr, np.float32, **kwargs)
507507
data_back = round_trip(aw)
508508
assert_array_equal(np.isnan(data_back), [True, False])
509-
# Deprecation warning for nan2zero as argument to `to_fileobj`
510-
with error_warnings():
511-
with pytest.deprecated_call():
512-
aw.to_fileobj(BytesIO(), 'F', True)
513-
with pytest.deprecated_call():
514-
aw.to_fileobj(BytesIO(), 'F', nan2zero=True)
509+
# Expired deprecation error for nan2zero as argument to `to_fileobj`
510+
with pytest.raises(ExpiredDeprecationError):
511+
aw.to_fileobj(BytesIO(), 'F', True)
512+
with pytest.raises(ExpiredDeprecationError):
513+
aw.to_fileobj(BytesIO(), 'F', nan2zero=True)
515514
# Error if nan2zero is not the value set at initialization
516515
with pytest.raises(WriterError):
517516
aw.to_fileobj(BytesIO(), 'F', False)
@@ -528,12 +527,11 @@ def test_nan2zero():
528527
data_back = round_trip(aw)
529528
astype_res = np.array(np.nan).astype(np.int32)
530529
assert_array_equal(data_back, [astype_res, 99])
531-
# Deprecation warning for nan2zero as argument to `to_fileobj`
532-
with error_warnings():
533-
with pytest.deprecated_call():
534-
aw.to_fileobj(BytesIO(), 'F', False)
535-
with pytest.deprecated_call():
536-
aw.to_fileobj(BytesIO(), 'F', nan2zero=False)
530+
# Expired deprecation error for nan2zero as argument to `to_fileobj`
531+
with pytest.raises(ExpiredDeprecationError):
532+
aw.to_fileobj(BytesIO(), 'F', False)
533+
with pytest.raises(ExpiredDeprecationError):
534+
aw.to_fileobj(BytesIO(), 'F', nan2zero=False)
537535
# Error if nan2zero is not the value set at initialization
538536
with pytest.raises(WriterError):
539537
aw.to_fileobj(BytesIO(), 'F', True)

nibabel/tests/test_removalschedule.py

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
]
1313

1414
OBJECT_SCHEDULE = [
15+
("7.0.0", [("nibabel.gifti.gifti", "GiftiNVPairs"),
16+
]),
1517
("6.0.0", [("nibabel.loadsave", "guessed_image_type"),
1618
("nibabel.loadsave", "read_img_data"),
1719
("nibabel.orientations", "flip_axis"),
@@ -41,6 +43,7 @@
4143
ATTRIBUTE_SCHEDULE = [
4244
("7.0.0", [("nibabel.gifti.gifti", "GiftiMetaData", "from_dict"),
4345
("nibabel.gifti.gifti", "GiftiMetaData", "metadata"),
46+
("nibabel.gifti.gifti", "GiftiMetaData", "data"),
4447
]),
4548
("5.0.0", [("nibabel.dataobj_images", "DataobjImage", "get_data"),
4649
("nibabel.freesurfer.mghformat", "MGHHeader", "_header_data"),
@@ -62,6 +65,7 @@
6265
("nibabel.ecat", "EcatImage", "from_filespec"),
6366
("nibabel.filebasedimages", "FileBasedImage", "get_header"),
6467
("nibabel.spatialimages", "SpatialImage", "get_affine"),
68+
("nibabel.arraywriters", "ArrayWriter", "_check_nan2zero"),
6569
]),
6670
("4.0.0", [("nibabel.dataobj_images", "DataobjImage", "get_shape"),
6771
("nibabel.filebasedimages", "FileBasedImage", "filespec_to_files"),

0 commit comments

Comments
 (0)