-
Notifications
You must be signed in to change notification settings - Fork 264
ENH: Add "element" containers and make dicom wrappers compatible #416
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
Changes from all commits
8990212
55768cc
109c65e
46be012
b1ad6eb
be4129f
86ce9bf
5d86f6f
bd21464
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- | ||
# vi: set ft=python sts=4 ts=4 sw=4 et: | ||
'''Containers that provide easy access to the values of nested elements | ||
|
||
These containers are for storing "elements" which have both a core data value | ||
as well as some additional meta data. When indexing into these containers it is | ||
this core value that is returned, which allows for much cleaner and more | ||
readable access to nested structures. | ||
|
||
Each object stored in these containers must have an attribute `value` which | ||
provides the core data value for the element. To get the element object itself | ||
the `get_elem` method must be used. | ||
''' | ||
from collections import MutableMapping, MutableSequence | ||
|
||
from .externals import OrderedDict | ||
|
||
|
||
class MetaElem(object): | ||
'''Basic element type has a `value` and a `meta` attribute.''' | ||
def __init__(self, value, meta=None): | ||
self.value = value | ||
self.meta = {} if meta is None else meta | ||
|
||
|
||
class InvalidElemError(Exception): | ||
'''The object being added to the container doesn't have a `value` attribute | ||
''' | ||
def __init__(self, invalid_val): | ||
message = ("Provided value '%s' of type %s does not have a 'value' " | ||
"attribute" % (invalid_val, type(invalid_val))) | ||
super(InvalidElemError, self).__init__(message) | ||
|
||
|
||
class ElemDict(MutableMapping): | ||
'''Ordered dict-like providing easy access to nested elements | ||
|
||
Each value added to the dict must in turn have a `value` attribute, which | ||
is what is returned by subsequent calls to `__getitem__`. To get the | ||
element itself use the `get_elem` method. | ||
''' | ||
|
||
def __init__(self, *args, **kwargs): | ||
if len(args) > 1: | ||
raise TypeError("At most one arg expected, got %d" % len(args)) | ||
self._elems = OrderedDict() | ||
if len(args) == 1: | ||
arg = args[0] | ||
if hasattr(arg, 'get_elem'): | ||
it = ((k, arg.get_elem(k)) for k in arg) | ||
elif hasattr(arg, 'items'): | ||
it = arg.items() | ||
else: | ||
it = arg | ||
for key, val in it: | ||
self[key] = val | ||
for key, val in kwargs.items(): | ||
self[key] = val | ||
|
||
def __getitem__(self, key): | ||
return self._elems[key].value | ||
|
||
def __setitem__(self, key, val): | ||
if not hasattr(val, 'value'): | ||
raise InvalidElemError(val) | ||
self._elems[key] = val | ||
|
||
def __delitem__(self, key): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could avoid these three methods by subclassing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe that subclassing MutableMapping is preferred for more "custom" dict-like classes, rather than just extending the API of a standard There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At least this:
gives I guess |
||
del self._elems[key] | ||
|
||
def __iter__(self): | ||
return iter(self._elems) | ||
|
||
def __len__(self): | ||
return len(self._elems) | ||
|
||
def __repr__(self): | ||
return ('ElemDict(%s)' % | ||
', '.join(['%r=%r' % x for x in self.items()])) | ||
|
||
def update(self, other): | ||
if hasattr(other, 'get_elem'): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe add |
||
for key in other: | ||
self[key] = other.get_elem(key) | ||
else: | ||
for key, elem in other.items(): | ||
self[key] = elem | ||
|
||
def get_elem(self, key): | ||
return self._elems[key] | ||
|
||
|
||
class ElemList(MutableSequence): | ||
'''A list-like container providing easy access to nested elements | ||
|
||
Each value added to the list must in turn have a `value` attribute, which | ||
is what is returned by subsequent calls to `__getitem__`. To get the | ||
element itself use the `get_elem` method. | ||
''' | ||
def __init__(self, data=None): | ||
self._elems = list() | ||
if data is None: | ||
return | ||
if isinstance(data, self.__class__): | ||
for idx in range(len(data)): | ||
self.append(data.get_elem(idx)) | ||
else: | ||
for elem in data: | ||
self.append(elem) | ||
|
||
def _tuple_from_slice(self, slc): | ||
'''Get (start, end, step) tuple from slice object. | ||
''' | ||
(start, end, step) = slc.indices(len(self)) | ||
# Replace (0, -1, 1) with (0, 0, 1) (misfeature in .indices()). | ||
if step == 1: | ||
if end < start: | ||
end = start | ||
step = None | ||
if slc.step is None: | ||
step = None | ||
return (start, end, step) | ||
|
||
def __getitem__(self, idx): | ||
if isinstance(idx, slice): | ||
return ElemList(self._elems[idx]) | ||
else: | ||
return self._elems[idx].value | ||
|
||
def __setitem__(self, idx, val): | ||
if isinstance(idx, slice): | ||
(start, end, step) = self._tuple_from_slice(idx) | ||
if step is None: | ||
# Normal slice | ||
for j, assign_val in enumerate(val): | ||
self.insert(start + j, assign_val) | ||
return | ||
# Extended slice | ||
indices = range(start, end, step) | ||
if len(val) != len(indices): | ||
raise ValueError(('attempt to assign sequence of size %d' + | ||
' to extended slice of size %d') % | ||
(len(value), len(indices))) | ||
for j, assign_val in enumerate(val): | ||
self.insert(indices[j], assign_val) | ||
else: | ||
self.insert(idx, val) | ||
|
||
def __delitem__(self, idx): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe subclass There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above, I think MutableSequence is preferred in this situation. |
||
del self._elems[idx] | ||
|
||
def __len__(self): | ||
return len(self._elems) | ||
|
||
def __repr__(self): | ||
return ('ElemList([%s])' % ', '.join(['%r' % x for x in self])) | ||
|
||
def __add__(self, other): | ||
result = self.__class__(self) | ||
if isinstance(other, self.__class__): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree Edit: Oops, that would produce values not elems... Some kind of method for this sounds good, maybe Edit2: Doh! I just realized you meant to just produce elements not (idx, element) tuples... Maybe give it different name from the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about |
||
for idx in range(len(other)): | ||
result.append(other.get_elem(idx)) | ||
else: | ||
for e in other: | ||
result.append(e) | ||
return result | ||
|
||
def __radd__(self, other): | ||
result = self.__class__(other) | ||
for idx in range(len(self)): | ||
result.append(self.get_elem(idx)) | ||
return result | ||
|
||
def __iadd__(self, other): | ||
if isinstance(other, self.__class__): | ||
for idx in range(len(other)): | ||
self.append(other.get_elem(idx)) | ||
else: | ||
for e in other: | ||
self.append(e) | ||
return self | ||
|
||
def insert(self, idx, val): | ||
if not hasattr(val, 'value'): | ||
raise InvalidElemError(val) | ||
self._elems.insert(idx, val) | ||
|
||
def get_elem(self, idx): | ||
return self._elems[idx] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
""" Testing element containers | ||
""" | ||
from __future__ import print_function | ||
|
||
from ..elemcont import MetaElem, ElemDict, ElemList, InvalidElemError | ||
|
||
import pytest | ||
|
||
|
||
def test_elemdict(): | ||
# Test ElemDict class | ||
e = ElemDict() | ||
with pytest.raises(InvalidElemError): | ||
e['some'] = 'thing' | ||
assert list(e.keys()) == [] | ||
elem = MetaElem('thing') | ||
e['some'] = elem | ||
assert list(e.keys()) == ['some'] | ||
assert e['some'] == 'thing' | ||
assert e.get_elem('some') == elem | ||
|
||
# Test constructor | ||
with pytest.raises(InvalidElemError): | ||
ElemDict(dict(some='thing')) | ||
e = ElemDict(dict(some=MetaElem('thing'))) | ||
assert list(e.keys()) == ['some'] | ||
assert e['some'] == 'thing' | ||
e = ElemDict(some=MetaElem('thing')) | ||
assert list(e.keys()) == ['some'] | ||
assert e['some'] == 'thing' | ||
e2 = ElemDict(e) | ||
assert list(e2.keys()) == ['some'] | ||
assert e2['some'] == 'thing' | ||
|
||
|
||
def test_elemdict_update(): | ||
e1 = ElemDict(dict(some=MetaElem('thing'))) | ||
e1.update(dict(hello=MetaElem('world'))) | ||
assert list(e1.items()) == [('some', 'thing'), ('hello', 'world')] | ||
e1 = ElemDict(dict(some=MetaElem('thing'))) | ||
e2 = ElemDict(dict(hello=MetaElem('world'))) | ||
e1.update(e2) | ||
assert list(e1.items()) == [('some', 'thing'), ('hello', 'world')] | ||
|
||
|
||
def test_elemlist(): | ||
# Test ElemList class | ||
el = ElemList() | ||
assert len(el) == 0 | ||
with pytest.raises(InvalidElemError): | ||
el.append('something') | ||
elem = MetaElem('something') | ||
el.append(elem) | ||
assert len(el) == 1 | ||
assert el[0] == 'something' | ||
assert el.get_elem(0) == elem | ||
assert [x for x in el] == ['something'] | ||
|
||
# Test constructor | ||
with pytest.raises(InvalidElemError): | ||
ElemList(['something']) | ||
el = ElemList([elem]) | ||
assert len(el) == 1 | ||
assert el[0] == 'something' | ||
assert el.get_elem(0) == elem | ||
el2 = ElemList(el) | ||
assert len(el2) == 1 | ||
assert el2[0] == 'something' | ||
assert el2.get_elem(0) == elem | ||
|
||
|
||
def test_elemlist_slicing(): | ||
el = ElemList() | ||
el[5:6] = [MetaElem('hello'), MetaElem('there'), MetaElem('world')] | ||
assert [x for x in el] == ['hello', 'there', 'world'] | ||
assert isinstance(el[:2], ElemList) | ||
assert [x for x in el[:2]] == ['hello', 'there'] | ||
|
||
|
||
def test_elemlist_add(): | ||
res = ElemList([MetaElem('hello'), MetaElem('there')]) + ElemList([MetaElem('world')]) | ||
assert isinstance(res, ElemList) | ||
assert [x for x in res] == ['hello', 'there', 'world'] | ||
res = ElemList([MetaElem('hello'), MetaElem('there')]) + [MetaElem('world')] | ||
assert isinstance(res, ElemList) | ||
assert [x for x in res] == ['hello', 'there', 'world'] | ||
res = [MetaElem('hello'), MetaElem('there')] + ElemList([MetaElem('world')]) | ||
assert isinstance(res, ElemList) | ||
assert [x for x in res] == ['hello', 'there', 'world'] | ||
res = ElemList([MetaElem('hello'), MetaElem('there')]) | ||
res += [MetaElem('world')] | ||
assert [x for x in res] == ['hello', 'there', 'world'] | ||
res = ElemList([MetaElem('hello'), MetaElem('there')]) | ||
res += ElemList([MetaElem('world')]) | ||
assert [x for x in res] == ['hello', 'there', 'world'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why
*args
here rather thanval
or something?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do I understand right that
args[0]
can be any ofThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is trying to replicate the behavior of the
dict
constructor. You are correct thatargs[0]
can be dict-like of a list tuples. Either way the elements need to have avalue
attribute.