Skip to content

Commit 75c2b85

Browse files
authored
Merge pull request #1091 from effigies/enh/unify_xml_metadata
RF: Unify Caret-XML-style metadata structure as dict-like
2 parents e7e1d46 + 6bc86df commit 75c2b85

8 files changed

+294
-94
lines changed

nibabel/caret.py

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
4+
#
5+
# See COPYING file distributed along with the NiBabel package for the
6+
# copyright and license terms.
7+
#
8+
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
9+
from collections.abc import MutableMapping
10+
11+
from . import xmlutils as xml
12+
13+
14+
class CaretMetaData(xml.XmlSerializable, MutableMapping):
15+
""" A list of name-value pairs used in various Caret-based XML formats
16+
17+
* Description - Provides a simple method for user-supplied metadata that
18+
associates names with values.
19+
* Attributes: [NA]
20+
* Child Elements
21+
22+
* MD (0...N)
23+
24+
* Text Content: [NA]
25+
26+
MD elements are a single metadata entry consisting of a name and a value.
27+
28+
Attributes
29+
----------
30+
data : mapping of {name: value} pairs
31+
32+
>>> md = CaretMetaData()
33+
>>> md['key'] = 'val'
34+
>>> md
35+
<CaretMetaData {'key': 'val'}>
36+
>>> dict(md)
37+
{'key': 'val'}
38+
>>> md.to_xml()
39+
b'<MetaData><MD><Name>key</Name><Value>val</Value></MD></MetaData>'
40+
41+
Objects may be constructed like any ``dict``:
42+
43+
>>> md = CaretMetaData(key='val')
44+
>>> md.to_xml()
45+
b'<MetaData><MD><Name>key</Name><Value>val</Value></MD></MetaData>'
46+
"""
47+
def __init__(self, *args, **kwargs):
48+
args, kwargs = self._sanitize(args, kwargs)
49+
self._data = dict(*args, **kwargs)
50+
51+
@staticmethod
52+
def _sanitize(args, kwargs):
53+
""" Override in subclasses to accept and warn on previous invocations
54+
"""
55+
return args, kwargs
56+
57+
def __getitem__(self, key):
58+
""" Get metadata entry by name
59+
60+
>>> md = CaretMetaData({'key': 'val'})
61+
>>> md['key']
62+
'val'
63+
"""
64+
return self._data[key]
65+
66+
def __setitem__(self, key, value):
67+
""" Set metadata entry by name
68+
69+
>>> md = CaretMetaData({'key': 'val'})
70+
>>> dict(md)
71+
{'key': 'val'}
72+
>>> md['newkey'] = 'newval'
73+
>>> dict(md)
74+
{'key': 'val', 'newkey': 'newval'}
75+
>>> md['key'] = 'otherval'
76+
>>> dict(md)
77+
{'key': 'otherval', 'newkey': 'newval'}
78+
"""
79+
self._data[key] = value
80+
81+
def __delitem__(self, key):
82+
""" Delete metadata entry by name
83+
84+
>>> md = CaretMetaData({'key': 'val'})
85+
>>> dict(md)
86+
{'key': 'val'}
87+
>>> del md['key']
88+
>>> dict(md)
89+
{}
90+
"""
91+
del self._data[key]
92+
93+
def __len__(self):
94+
""" Get length of metadata list
95+
96+
>>> md = CaretMetaData({'key': 'val'})
97+
>>> len(md)
98+
1
99+
"""
100+
return len(self._data)
101+
102+
def __iter__(self):
103+
""" Iterate over metadata entries
104+
105+
>>> md = CaretMetaData({'key': 'val'})
106+
>>> for key in md:
107+
... print(key)
108+
key
109+
"""
110+
return iter(self._data)
111+
112+
def __repr__(self):
113+
return f"<{self.__class__.__name__} {self._data!r}>"
114+
115+
def _to_xml_element(self):
116+
metadata = xml.Element('MetaData')
117+
118+
for name_text, value_text in self._data.items():
119+
md = xml.SubElement(metadata, 'MD')
120+
name = xml.SubElement(md, 'Name')
121+
name.text = str(name_text)
122+
value = xml.SubElement(md, 'Value')
123+
value.text = str(value_text)
124+
return metadata

nibabel/cifti2/cifti2.py

+50-30
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from ..nifti1 import Nifti1Extensions
2626
from ..nifti2 import Nifti2Image, Nifti2Header
2727
from ..arrayproxy import reshape_dataobj
28+
from ..caret import CaretMetaData
2829
from warnings import warn
2930

3031

@@ -102,7 +103,7 @@ def _underscore(string):
102103
return re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', string).lower()
103104

104105

105-
class Cifti2MetaData(xml.XmlSerializable, MutableMapping):
106+
class Cifti2MetaData(CaretMetaData):
106107
""" A list of name-value pairs
107108
108109
* Description - Provides a simple method for user-supplied metadata that
@@ -121,25 +122,55 @@ class Cifti2MetaData(xml.XmlSerializable, MutableMapping):
121122
----------
122123
data : list of (name, value) tuples
123124
"""
124-
def __init__(self, metadata=None):
125-
self.data = OrderedDict()
126-
if metadata is not None:
127-
self.update(metadata)
128-
129-
def __getitem__(self, key):
130-
return self.data[key]
131-
132-
def __setitem__(self, key, value):
133-
self.data[key] = value
134-
135-
def __delitem__(self, key):
136-
del self.data[key]
137-
138-
def __len__(self):
139-
return len(self.data)
125+
@staticmethod
126+
def _sanitize(args, kwargs):
127+
""" Sanitize and warn on deprecated arguments
128+
129+
Accept metadata positional/keyword argument that can take
130+
``None`` to indicate no initialization.
131+
132+
>>> import pytest
133+
>>> Cifti2MetaData()
134+
<Cifti2MetaData {}>
135+
>>> Cifti2MetaData([("key", "val")])
136+
<Cifti2MetaData {'key': 'val'}>
137+
>>> Cifti2MetaData(key="val")
138+
<Cifti2MetaData {'key': 'val'}>
139+
>>> with pytest.warns(FutureWarning):
140+
... Cifti2MetaData(None)
141+
<Cifti2MetaData {}>
142+
>>> with pytest.warns(FutureWarning):
143+
... Cifti2MetaData(metadata=None)
144+
<Cifti2MetaData {}>
145+
>>> with pytest.warns(FutureWarning):
146+
... Cifti2MetaData(metadata={'key': 'val'})
147+
<Cifti2MetaData {'key': 'val'}>
148+
149+
Note that "metadata" could be a valid key:
150+
151+
>>> Cifti2MetaData(metadata='val')
152+
<Cifti2MetaData {'metadata': 'val'}>
153+
"""
154+
if not args and list(kwargs) == ["metadata"]:
155+
if not isinstance(kwargs["metadata"], str):
156+
warn("Cifti2MetaData now has a dict-like interface and will "
157+
"no longer accept the ``metadata`` keyword argument in "
158+
"NiBabel 6.0. See ``pydoc dict`` for initialization options.",
159+
FutureWarning, stacklevel=3)
160+
md = kwargs.pop("metadata")
161+
if md is not None:
162+
args = (md,)
163+
if args == (None,):
164+
warn("Cifti2MetaData now has a dict-like interface and will no longer "
165+
"accept the positional argument ``None`` in NiBabel 6.0. "
166+
"See ``pydoc dict`` for initialization options.",
167+
FutureWarning, stacklevel=3)
168+
args = ()
169+
return args, kwargs
140170

141-
def __iter__(self):
142-
return iter(self.data)
171+
@property
172+
def data(self):
173+
return self._data
143174

144175
def difference_update(self, metadata):
145176
"""Remove metadata key-value pairs
@@ -159,17 +190,6 @@ def difference_update(self, metadata):
159190
for k in pairs:
160191
del self.data[k]
161192

162-
def _to_xml_element(self):
163-
metadata = xml.Element('MetaData')
164-
165-
for name_text, value_text in self.data.items():
166-
md = xml.SubElement(metadata, 'MD')
167-
name = xml.SubElement(md, 'Name')
168-
name.text = str(name_text)
169-
value = xml.SubElement(md, 'Value')
170-
value.text = str(value_text)
171-
return metadata
172-
173193

174194
class Cifti2LabelTable(xml.XmlSerializable, MutableMapping):
175195
r""" CIFTI-2 label table: a sequence of ``Cifti2Label``\s

nibabel/cifti2/cifti2_axes.py

-3
Original file line numberDiff line numberDiff line change
@@ -1075,7 +1075,6 @@ def to_mapping(self, dim):
10751075
"""
10761076
mim = cifti2.Cifti2MatrixIndicesMap([dim], 'CIFTI_INDEX_TYPE_SCALARS')
10771077
for name, meta in zip(self.name, self.meta):
1078-
meta = None if len(meta) == 0 else meta
10791078
named_map = cifti2.Cifti2NamedMap(name, cifti2.Cifti2MetaData(meta))
10801079
mim.append(named_map)
10811080
return mim
@@ -1213,8 +1212,6 @@ def to_mapping(self, dim):
12131212
label_table = cifti2.Cifti2LabelTable()
12141213
for key, value in label.items():
12151214
label_table[key] = (value[0],) + tuple(value[1])
1216-
if len(meta) == 0:
1217-
meta = None
12181215
named_map = cifti2.Cifti2NamedMap(name, cifti2.Cifti2MetaData(meta),
12191216
label_table)
12201217
mim.append(named_map)

nibabel/cifti2/tests/test_cifti2.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,20 @@ def test_value_if_klass():
3535

3636

3737
def test_cifti2_metadata():
38-
md = ci.Cifti2MetaData(metadata={'a': 'aval'})
38+
md = ci.Cifti2MetaData({'a': 'aval'})
3939
assert len(md) == 1
4040
assert list(iter(md)) == ['a']
4141
assert md['a'] == 'aval'
4242
assert md.data == dict([('a', 'aval')])
4343

44+
with pytest.warns(FutureWarning):
45+
md = ci.Cifti2MetaData(metadata={'a': 'aval'})
46+
assert md == {'a': 'aval'}
47+
48+
with pytest.warns(FutureWarning):
49+
md = ci.Cifti2MetaData(None)
50+
assert md == {}
51+
4452
md = ci.Cifti2MetaData()
4553
assert len(md) == 0
4654
assert list(iter(md)) == []

0 commit comments

Comments
 (0)