Skip to content

Commit e795be6

Browse files
authored
Merge pull request #1127 from effigies/fix/gifti_md_list
FIX: Make GiftiMetaData.data a list proxy, deprecate
2 parents c90b75d + 301e024 commit e795be6

File tree

4 files changed

+170
-11
lines changed

4 files changed

+170
-11
lines changed

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_removalschedule.py

+3
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"),

0 commit comments

Comments
 (0)