Skip to content

Commit cc18fb6

Browse files
committed
ENH: Add "element" containers and make dicom wrappers compatible
Add two main types: 'ElemDict' and 'ElemList'. Each can only store objects with an attribute 'value'. Standard lookups with '__getitem__' and 'get' return just this 'value' attribute. To access the underlying object, a 'get_elem' method is provided. Also updated the DICOM wrappers to provide a 'get_elem' method that returns the full DICOM element rather than just the value. Added a __iter__ method that returns the available DICOM keys.
1 parent 5a42cb3 commit cc18fb6

File tree

4 files changed

+306
-0
lines changed

4 files changed

+306
-0
lines changed

nibabel/elemcont.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
'''Containers for storing "elements" which have both a core data value as well
4+
as some additional meta data. When indexing into these containers it is this
5+
core value that is returned, which allows for much cleaner and more readable
6+
access to nested structures.
7+
8+
Each object stored in these containers must have an attribute `value` which
9+
provides the core data value for the element. To get the element object itself
10+
the `get_elem` method must be used.
11+
'''
12+
from collections import MutableMapping, MutableSequence
13+
14+
from .externals import OrderedDict
15+
from .externals.six import iteritems
16+
17+
18+
class Elem(object):
19+
'''Basic element type has a `value` and a `meta` attribute.'''
20+
def __init__(self, value, meta=None):
21+
self.value = value
22+
self.meta = {} if meta is None else meta
23+
24+
25+
class InvalidElemError(Exception):
26+
'''Raised when trying to add an object without a `value` attribute to an
27+
`ElemDict`.'''
28+
def __init__(self, invalid_val):
29+
self.invalid_val = invalid_val
30+
message = ("Provided value '%s' of type %s does not have a 'value' "
31+
"attribute" % (self.invalid_val, type(invalid_val)))
32+
super(InvalidElemError, self).__init__(message)
33+
34+
35+
class ElemDict(MutableMapping):
36+
'''Ordered dict-like where each value is an "element", which is defined as
37+
any object which has a `value` attribute.
38+
39+
When looking up an item in the dict, it is this `value` attribute that
40+
is returned. To get the element itself use the `get_elem` method.
41+
'''
42+
43+
def __init__(self, *args, **kwargs):
44+
if len(args) > 1:
45+
raise TypeError("At most one arg expected, got %d" % len(args))
46+
self._elems = OrderedDict()
47+
if len(args) == 1:
48+
arg = args[0]
49+
if hasattr(arg, 'get_elem'):
50+
it = ((k, arg.get_elem(k)) for k in arg)
51+
elif hasattr(arg, 'items'):
52+
it = iteritems(arg)
53+
else:
54+
it = arg
55+
for key, val in it:
56+
self[key] = val
57+
for key, val in iteritems(kwargs):
58+
self[key] = val
59+
60+
def __getitem__(self, key):
61+
return self._elems[key].value
62+
63+
def __setitem__(self, key, val):
64+
if not hasattr(val, 'value'):
65+
raise InvalidElemError(val)
66+
self._elems[key] = val
67+
68+
def __delitem__(self, key):
69+
del self._elems[key]
70+
71+
def __iter__(self):
72+
return iter(self._elems)
73+
74+
def __len__(self):
75+
return len(self._elems)
76+
77+
def __repr__(self):
78+
return ('ElemDict(%s)' %
79+
', '.join(['%r=%r' % x for x in self.items()]))
80+
81+
def update(self, other):
82+
if hasattr(other, 'get_elem'):
83+
for key in other:
84+
self[key] = other.get_elem(key)
85+
else:
86+
for key, elem in iteritems(other):
87+
self[key] = elem
88+
89+
def get_elem(self, key):
90+
return self._elems[key]
91+
92+
93+
class ElemList(MutableSequence):
94+
'''A list-like container where each value is an "element", which is
95+
defined as any object which has a `value` attribute.
96+
97+
When looking up an item in the list, it is this `value` attribute that
98+
is returned. To get the element itself use the `get_elem` method.
99+
'''
100+
def __init__(self, data=None):
101+
self._elems = list()
102+
if data is not None:
103+
if isinstance(data, self.__class__):
104+
for idx in range(len(data)):
105+
self.append(data.get_elem(idx))
106+
else:
107+
for elem in data:
108+
self.append(elem)
109+
110+
def _tuple_from_slice(self, slc):
111+
'''Get (start, end, step) tuple from slice object.
112+
'''
113+
(start, end, step) = slc.indices(len(self))
114+
# Replace (0, -1, 1) with (0, 0, 1) (misfeature in .indices()).
115+
if step == 1:
116+
if end < start:
117+
end = start
118+
step = None
119+
if slc.step == None:
120+
step = None
121+
return (start, end, step)
122+
123+
def __getitem__(self, idx):
124+
if isinstance(idx, slice):
125+
return ElemList(self._elems[idx])
126+
else:
127+
return self._elems[idx].value
128+
129+
def __setitem__(self, idx, val):
130+
if isinstance(idx, slice):
131+
(start, end, step) = self._tuple_from_slice(idx)
132+
if step != None:
133+
# Extended slice
134+
indices = range(start, end, step)
135+
if len(val) != len(indices):
136+
raise ValueError(('attempt to assign sequence of size %d' +
137+
' to extended slice of size %d') %
138+
(len(value), len(indices)))
139+
for j, assign_val in enumerate(val):
140+
self.insert(indices[j], assign_val)
141+
else:
142+
# Normal slice
143+
for j, assign_val in enumerate(val):
144+
self.insert(start + j, assign_val)
145+
else:
146+
self.insert(idx, val)
147+
148+
def __delitem__(self, idx):
149+
del self._elems[idx]
150+
151+
def __len__(self):
152+
return len(self._elems)
153+
154+
def __repr__(self):
155+
return ('ElemList([%s])' % ', '.join(['%r' % x for x in self]))
156+
157+
def __add__(self, other):
158+
result = self.__class__(self)
159+
if isinstance(other, self.__class__):
160+
for idx in range(len(other)):
161+
result.append(other.get_elem(idx))
162+
else:
163+
for e in other:
164+
result.append(e)
165+
return result
166+
167+
def __radd__(self, other):
168+
result = self.__class__(other)
169+
for idx in range(len(self)):
170+
result.append(self.get_elem(idx))
171+
return result
172+
173+
def __iadd__(self, other):
174+
if isinstance(other, self.__class__):
175+
for idx in range(len(other)):
176+
self.append(other.get_elem(idx))
177+
else:
178+
for e in other:
179+
self.append(e)
180+
return self
181+
182+
def insert(self, idx, val):
183+
if not hasattr(val, 'value'):
184+
raise InvalidElemError(val)
185+
self._elems.insert(idx, val)
186+
187+
def get_elem(self, idx):
188+
return self._elems[idx]

nibabel/nicom/dicomwrappers.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@
2121
from ..openers import ImageOpener
2222
from ..onetime import setattr_on_read as one_time
2323

24+
try:
25+
from dicom.datadict import tag_for_name
26+
except ImportError:
27+
try:
28+
from pydicom.datadict import tag_for_name
29+
except ImportError:
30+
pass
31+
2432

2533
class WrapperError(Exception):
2634
pass
@@ -286,6 +294,17 @@ def get(self, key, default=None):
286294
""" Get values from underlying dicom data """
287295
return self.dcm_data.get(key, default)
288296

297+
def get_elem(self, key):
298+
""" Get DICOM element instead of just the value """
299+
tag = tag_for_name(key)
300+
if tag is None or tag not in self.dcm_data:
301+
raise KeyError('"%s" not in self.dcm_data' % key)
302+
return self.dcm_data.get(tag)
303+
304+
def __iter__(self):
305+
""" Iterate over available DICOM keywords """
306+
return iter(self.dcm_data.dir())
307+
289308
def get_affine(self):
290309
""" Return mapping between voxel and DICOM coordinate system
291310

nibabel/nicom/tests/test_dicomwrappers.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ def test_wrappers():
7979
assert_equal(dw.get('InstanceNumber'), None)
8080
assert_equal(dw.get('AcquisitionNumber'), None)
8181
assert_raises(KeyError, dw.__getitem__, 'not an item')
82+
assert_raises(KeyError, dw.get_elem, 'not an item')
8283
assert_raises(didw.WrapperError, dw.get_data)
8384
assert_raises(didw.WrapperError, dw.get_affine)
8485
assert_raises(TypeError, maker)
@@ -95,7 +96,15 @@ def test_wrappers():
9596
dw = maker(DATA)
9697
assert_equal(dw.get('InstanceNumber'), 2)
9798
assert_equal(dw.get('AcquisitionNumber'), 2)
99+
elem = dw.get_elem('InstanceNumber')
100+
assert_equal(type(elem), pydicom.dataelem.DataElement)
101+
assert_equal(elem.value, 2)
102+
assert_equal(elem.VR, 'IS')
98103
assert_raises(KeyError, dw.__getitem__, 'not an item')
104+
assert_raises(KeyError, dw.get_elem, 'not an item')
105+
# Test we get a key error when using real DICOM keyword that isn't in
106+
# this dataset
107+
assert_raises(KeyError, dw.get_elem, 'ShutterShape')
99108
for maker in (didw.MosaicWrapper, didw.wrapper_from_data):
100109
dw = maker(DATA)
101110
assert_true(dw.is_mosaic)

nibabel/tests/test_elemcont.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
""" Testing element containers
2+
"""
3+
from __future__ import print_function
4+
5+
from ..elemcont import Elem, ElemDict, ElemList, InvalidElemError
6+
from nose.tools import (assert_true, assert_false, assert_equal, assert_raises)
7+
8+
9+
def test_elemdict():
10+
# Test ElemDict class
11+
e = ElemDict()
12+
assert_raises(InvalidElemError, e.__setitem__, 'some', 'thing')
13+
assert_equal(list(e.keys()), [])
14+
elem = Elem('thing')
15+
e['some'] = elem
16+
assert_equal(list(e.keys()), ['some'])
17+
assert_equal(e['some'], 'thing')
18+
assert_equal(e.get_elem('some'), elem)
19+
20+
# Test constructor
21+
assert_raises(InvalidElemError, ElemDict, dict(some='thing'))
22+
e = ElemDict(dict(some=Elem('thing')))
23+
assert_equal(list(e.keys()), ['some'])
24+
assert_equal(e['some'], 'thing')
25+
e = ElemDict(some=Elem('thing'))
26+
assert_equal(list(e.keys()), ['some'])
27+
assert_equal(e['some'], 'thing')
28+
e2 = ElemDict(e)
29+
assert_equal(list(e2.keys()), ['some'])
30+
assert_equal(e2['some'], 'thing')
31+
32+
33+
def test_elemdict_update():
34+
e1 = ElemDict(dict(some=Elem('thing')))
35+
e1.update(dict(hello=Elem('world')))
36+
assert_equal(list(e1.items()), [('some', 'thing'), ('hello', 'world')])
37+
e1 = ElemDict(dict(some=Elem('thing')))
38+
e2 = ElemDict(dict(hello=Elem('world')))
39+
e1.update(e2)
40+
assert_equal(list(e1.items()), [('some', 'thing'), ('hello', 'world')])
41+
42+
43+
def test_elemlist():
44+
# Test ElemList class
45+
el = ElemList()
46+
assert_equal(len(el), 0)
47+
assert_raises(InvalidElemError, el.append, 'something')
48+
elem = Elem('something')
49+
el.append(elem)
50+
assert_equal(len(el), 1)
51+
assert_equal(el[0], 'something')
52+
assert_equal(el.get_elem(0), elem)
53+
assert_equal([x for x in el], ['something'])
54+
55+
# Test constructor
56+
assert_raises(InvalidElemError, ElemList, ['something'])
57+
el = ElemList([elem])
58+
assert_equal(len(el), 1)
59+
assert_equal(el[0], 'something')
60+
assert_equal(el.get_elem(0), elem)
61+
el2 = ElemList(el)
62+
assert_equal(len(el2), 1)
63+
assert_equal(el2[0], 'something')
64+
assert_equal(el2.get_elem(0), elem)
65+
66+
67+
def test_elemlist_slicing():
68+
el = ElemList()
69+
el[5:6] = [Elem('hello'), Elem('there'), Elem('world')]
70+
assert_equal([x for x in el], ['hello', 'there', 'world'])
71+
assert_true(isinstance(el[:2], ElemList))
72+
assert_equal([x for x in el[:2]], ['hello', 'there'])
73+
74+
75+
def test_elemlist_add():
76+
res = ElemList([Elem('hello'), Elem('there')]) + ElemList([Elem('world')])
77+
assert_true(isinstance(res, ElemList))
78+
assert_equal([x for x in res], ['hello', 'there', 'world'])
79+
res = ElemList([Elem('hello'), Elem('there')]) + [Elem('world')]
80+
assert_true(isinstance(res, ElemList))
81+
assert_equal([x for x in res], ['hello', 'there', 'world'])
82+
res = [Elem('hello'), Elem('there')] + ElemList([Elem('world')])
83+
assert_true(isinstance(res, ElemList))
84+
assert_equal([x for x in res], ['hello', 'there', 'world'])
85+
res = ElemList([Elem('hello'), Elem('there')])
86+
res += [Elem('world')]
87+
assert_equal([x for x in res], ['hello', 'there', 'world'])
88+
res = ElemList([Elem('hello'), Elem('there')])
89+
res += ElemList([Elem('world')])
90+
assert_equal([x for x in res], ['hello', 'there', 'world'])

0 commit comments

Comments
 (0)