Skip to content

Commit 4a541ca

Browse files
committed
Merge pull request #203 from matthew-brett/proxy-validators
MRG: array proxy API validation; proxy refactor Add ArrayProxy API validation. Extends tests a little for array proxies, prepares for extending the API for array proxies. Refactor ArrayProxy to copy out fields it needs from the header, but deprecate the header copy (it can be large), so we can just keep the fields from the header that we need.
2 parents ff2b545 + 28026b9 commit 4a541ca

File tree

11 files changed

+458
-31
lines changed

11 files changed

+458
-31
lines changed

nibabel/analyze.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
.get/set_data_shape
2626
.get/set_data_dtype
2727
.get/set_zooms
28+
.get/set_data_offset
2829
.get_base_affine()
2930
.get_best_affine()
3031
.data_to_fileobj
@@ -682,6 +683,11 @@ def set_zooms(self, zooms):
682683
def as_analyze_map(self):
683684
return self
684685

686+
def set_data_offset(self, offset):
687+
""" Set offset into data file to read data
688+
"""
689+
self._structarr['vox_offset'] = offset
690+
685691
def get_data_offset(self):
686692
''' Return offset into data file to read data
687693

nibabel/arrayproxy.py

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@
1010
1111
The API is - at minimum:
1212
13-
* The object has an attribute ``shape``
14-
* that the object returns the data array from ``np.asarray(obj)``
13+
* The object has a read-only attribute ``shape``
14+
* read only ``is_proxy`` attribute / property
15+
* the object returns the data array from ``np.asarray(obj)``
1516
* that modifying no object outside ``obj`` will affect the result of
16-
``np.asarray(obj)``. Specifically, if you pass a header into the the
17-
__init__, then modifying the original header will not affect the result of the
18-
array return.
17+
``np.asarray(obj)``. Specifically:
18+
* Changes in position (``obj.tell()``) of passed file-like objects will
19+
not affect the output of from ``np.asarray(proxy)``.
20+
* if you pass a header into the __init__, then modifying the original
21+
header will not affect the result of the array return.
1922
"""
23+
import warnings
2024

21-
from .volumeutils import BinOpener
25+
from .volumeutils import BinOpener, array_from_file, apply_read_scaling
2226

2327

2428
class ArrayProxy(object):
@@ -30,24 +34,62 @@ class ArrayProxy(object):
3034
variants, including Nifti1, and with the MGH format, apparently.
3135
3236
It requires a ``header`` object with methods:
33-
* copy
3437
* get_data_shape
35-
* data_from_fileobj
38+
* get_data_dtype
39+
* get_data_offset
40+
* get_slope_inter
41+
42+
The header should also have a 'copy' method. This requirement will go away
43+
when the deprecated 'header' propoerty goes away.
3644
3745
Other image types might need to implement their own implementation of this
3846
API. See :mod:`minc` for an example.
3947
"""
4048
def __init__(self, file_like, header):
4149
self.file_like = file_like
42-
self.header = header.copy()
50+
# Copies of values needed to read array
4351
self._shape = header.get_data_shape()
52+
self._dtype = header.get_data_dtype()
53+
self._offset = header.get_data_offset()
54+
self._slope, self._inter = header.get_slope_inter()
55+
self._slope = 1.0 if self._slope is None else self._slope
56+
self._inter = 0.0 if self._inter is None else self._inter
57+
# Reference to original header; we will remove this soon
58+
self._header = header.copy()
59+
60+
@property
61+
def header(self):
62+
warnings.warn('We will remove the header property from proxies soon',
63+
FutureWarning,
64+
stacklevel=2)
65+
return self._header
4466

4567
@property
4668
def shape(self):
4769
return self._shape
4870

71+
@property
72+
def is_proxy(self):
73+
return True
74+
75+
@property
76+
def slope(self):
77+
return self._slope
78+
79+
@property
80+
def inter(self):
81+
return self._inter
82+
83+
@property
84+
def offset(self):
85+
return self._offset
86+
4987
def __array__(self):
5088
''' Read of data from file '''
5189
with BinOpener(self.file_like) as fileobj:
52-
data = self.header.data_from_fileobj(fileobj)
53-
return data
90+
raw_data = array_from_file(self._shape,
91+
self._dtype,
92+
fileobj,
93+
self._offset)
94+
# Upcast as necessary for big slopes, intercepts
95+
return apply_read_scaling(raw_data, self._slope, self._inter)

nibabel/ecat.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,15 @@ def __init__(self, subheader):
660660
self._data = None
661661
x, y, z = subheader.get_shape()
662662
nframes = subheader.get_nframes()
663-
self.shape = (x, y, z, nframes)
663+
self._shape = (x, y, z, nframes)
664+
665+
@property
666+
def shape(self):
667+
return self._shape
668+
669+
@property
670+
def is_proxy(self):
671+
return True
664672

665673
def __array__(self):
666674
''' Read of data from file

nibabel/minc1.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,15 @@ class MincImageArrayProxy(object):
204204
'''
205205
def __init__(self, minc_file):
206206
self.minc_file = minc_file
207-
self.shape = minc_file.get_data_shape()
207+
self._shape = minc_file.get_data_shape()
208+
209+
@property
210+
def shape(self):
211+
return self._shape
212+
213+
@property
214+
def is_proxy(self):
215+
return True
208216

209217
def __array__(self):
210218
''' Read of data from file '''

nibabel/tests/test_analyze.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,13 @@ def test_datatype(self):
337337
ehdr.set_data_dtype(dt)
338338
assert_true(ehdr['datatype'] == code)
339339

340+
def test_offset(self):
341+
# Test get / set offset
342+
hdr = self.header_class()
343+
offset = hdr.get_data_offset()
344+
hdr.set_data_offset(offset + 16)
345+
assert_equal(hdr.get_data_offset(), offset + 16)
346+
340347
def test_data_shape_zooms_affine(self):
341348
hdr = self.header_class()
342349
for shape in ((1,2,3),(0,),(1,),(1,2),(1,2,3,4)):

nibabel/tests/test_arrayproxy.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
"""
1111
from __future__ import division, print_function, absolute_import
1212

13+
import warnings
14+
1315
from ..externals.six import BytesIO
1416
from ..tmpdirs import InTemporaryDirectory
1517

@@ -27,32 +29,43 @@ class FunkyHeader(object):
2729
def __init__(self, shape):
2830
self.shape = shape
2931

30-
def copy(self):
31-
return self.__class__(self.shape[:])
32-
3332
def get_data_shape(self):
3433
return self.shape[:]
3534

36-
def data_from_fileobj(self, fileobj):
37-
return np.arange(np.prod(self.shape)).reshape(self.shape)
35+
def get_data_dtype(self):
36+
return np.int32
37+
38+
def get_data_offset(self):
39+
return 16
40+
41+
def get_slope_inter(self):
42+
return 1.0, 0.0
43+
44+
def copy(self):
45+
# Not needed when we remove header property
46+
return FunkyHeader(self.shape)
3847

3948

4049
def test_init():
4150
bio = BytesIO()
4251
shape = [2,3,4]
52+
dtype = np.int32
53+
arr = np.arange(24, dtype=dtype).reshape(shape)
54+
bio.seek(16)
55+
bio.write(arr.tostring(order='F'))
4356
hdr = FunkyHeader(shape)
4457
ap = ArrayProxy(bio, hdr)
4558
assert_true(ap.file_like is bio)
4659
assert_equal(ap.shape, shape)
4760
# shape should be read only
4861
assert_raises(AttributeError, setattr, ap, 'shape', shape)
49-
# Check there has been a copy of the header
50-
assert_false(ap.header is hdr)
62+
# Get the data
63+
assert_array_equal(np.asarray(ap), arr)
5164
# Check we can modify the original header without changing the ap version
5265
hdr.shape[0] = 6
5366
assert_not_equal(ap.shape, shape)
54-
# Get the data
55-
assert_array_equal(np.asarray(ap), np.arange(24).reshape((2,3,4)))
67+
# Data stays the same, also
68+
assert_array_equal(np.asarray(ap), arr)
5669

5770

5871
def write_raw_data(arr, hdr, fileobj):
@@ -73,7 +86,9 @@ def test_nifti1_init():
7386
assert_true(ap.file_like == bio)
7487
assert_equal(ap.shape, shape)
7588
# Check there has been a copy of the header
76-
assert_false(ap.header is hdr)
89+
with warnings.catch_warnings():
90+
warnings.simplefilter("ignore")
91+
assert_false(ap.header is hdr)
7792
# Get the data
7893
assert_array_equal(np.asarray(ap), arr * 2.0 + 10)
7994
with InTemporaryDirectory():

nibabel/tests/test_helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def bytesio_filemap(klass):
1414

1515

1616
def bytesio_round_trip(img):
17-
""" Save then load from bytesio
17+
""" Save then load image from bytesio
1818
"""
1919
klass = img.__class__
2020
bytes_map = bytesio_filemap(klass)

nibabel/tests/test_minc2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323

2424
if have_h5py:
25-
class TestMinc1File(tm2._TestMincFile):
25+
class TestMinc2File(tm2._TestMincFile):
2626
module = minc2
2727
file_class = Minc2File
2828
opener = h5py.File

0 commit comments

Comments
 (0)