Skip to content

Commit 2838c06

Browse files
authored
Merge pull request #1131 from effigies/enh/arrayproxy_order
ENH: Make layout order an initialization parameter of ArrayProxy
2 parents a0bbc97 + cff42d6 commit 2838c06

File tree

2 files changed

+111
-23
lines changed

2 files changed

+111
-23
lines changed

nibabel/arrayproxy.py

+25-4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"""
2828
from contextlib import contextmanager
2929
from threading import RLock
30+
import warnings
3031

3132
import numpy as np
3233

@@ -53,7 +54,7 @@
5354
KEEP_FILE_OPEN_DEFAULT = False
5455

5556

56-
class ArrayProxy(object):
57+
class ArrayProxy:
5758
""" Class to act as proxy for the array that can be read from a file
5859
5960
The array proxy allows us to freeze the passed fileobj and header such that
@@ -83,10 +84,9 @@ class ArrayProxy(object):
8384
See :mod:`nibabel.minc1`, :mod:`nibabel.ecat` and :mod:`nibabel.parrec` for
8485
examples.
8586
"""
86-
# Assume Fortran array memory layout
87-
order = 'F'
87+
_default_order = 'F'
8888

89-
def __init__(self, file_like, spec, *, mmap=True, keep_file_open=None):
89+
def __init__(self, file_like, spec, *, mmap=True, order=None, keep_file_open=None):
9090
"""Initialize array proxy instance
9191
9292
Parameters
@@ -116,6 +116,11 @@ def __init__(self, file_like, spec, *, mmap=True, keep_file_open=None):
116116
True gives the same behavior as ``mmap='c'``. If `file_like`
117117
cannot be memory-mapped, ignore `mmap` value and read array from
118118
file.
119+
order : {None, 'F', 'C'}, optional, keyword only
120+
`order` controls the order of the data array layout. Fortran-style,
121+
column-major order may be indicated with 'F', and C-style, row-major
122+
order may be indicated with 'C'. None gives the default order, that
123+
comes from the `_default_order` class variable.
119124
keep_file_open : { None, True, False }, optional, keyword only
120125
`keep_file_open` controls whether a new file handle is created
121126
every time the image is accessed, or a single file handle is
@@ -128,6 +133,8 @@ def __init__(self, file_like, spec, *, mmap=True, keep_file_open=None):
128133
"""
129134
if mmap not in (True, False, 'c', 'r'):
130135
raise ValueError("mmap should be one of {True, False, 'c', 'r'}")
136+
if order not in (None, 'C', 'F'):
137+
raise ValueError("order should be one of {None, 'C', 'F'}")
131138
self.file_like = file_like
132139
if hasattr(spec, 'get_data_shape'):
133140
slope, inter = spec.get_slope_inter()
@@ -142,11 +149,25 @@ def __init__(self, file_like, spec, *, mmap=True, keep_file_open=None):
142149
else:
143150
raise TypeError('spec must be tuple of length 2-5 or header object')
144151

152+
# Warn downstream users that the class variable order is going away
153+
if hasattr(self.__class__, 'order'):
154+
warnings.warn(f'Class {self.__class__} has an `order` class variable. '
155+
'ArrayProxy subclasses should rename this variable to `_default_order` '
156+
'to avoid conflict with instance variables.\n'
157+
'* deprecated in version: 5.0\n'
158+
'* will raise error in version: 7.0\n',
159+
DeprecationWarning, stacklevel=2)
160+
# Override _default_order with order, to follow intent of subclasser
161+
self._default_order = self.order
162+
145163
# Copies of values needed to read array
146164
self._shape, self._dtype, self._offset, self._slope, self._inter = par
147165
# Permit any specifier that can be interpreted as a numpy dtype
148166
self._dtype = np.dtype(self._dtype)
149167
self._mmap = mmap
168+
if order is None:
169+
order = self._default_order
170+
self.order = order
150171
# Flags to keep track of whether a single ImageOpener is created, and
151172
# whether a single underlying file handle is created.
152173
self._keep_file_open, self._persist_opener = \

nibabel/tests/test_arrayproxy.py

+86-19
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@
1515

1616
import pickle
1717
from io import BytesIO
18+
from packaging.version import Version
1819
from ..tmpdirs import InTemporaryDirectory
1920

2021
import numpy as np
2122

23+
from .. import __version__
2224
from ..arrayproxy import (ArrayProxy, is_proxy, reshape_dataobj, get_obj_dtype)
2325
from ..openers import ImageOpener
2426
from ..nifti1 import Nifti1Header
27+
from ..deprecator import ExpiredDeprecationError
2528

2629
from unittest import mock
2730

@@ -57,6 +60,11 @@ def copy(self):
5760

5861
class CArrayProxy(ArrayProxy):
5962
# C array memory layout
63+
_default_order = 'C'
64+
65+
66+
class DeprecatedCArrayProxy(ArrayProxy):
67+
# Used in test_deprecated_order_classvar. Remove when that test is removed (8.0)
6068
order = 'C'
6169

6270

@@ -81,6 +89,9 @@ def test_init():
8189
assert ap.shape != shape
8290
# Data stays the same, also
8391
assert_array_equal(np.asarray(ap), arr)
92+
# You wouldn't do this, but order=None explicitly requests the default order
93+
ap2 = ArrayProxy(bio, FunkyHeader(arr.shape), order=None)
94+
assert_array_equal(np.asarray(ap2), arr)
8495
# C order also possible
8596
bio = BytesIO()
8697
bio.seek(16)
@@ -90,6 +101,8 @@ def test_init():
90101
# Illegal init
91102
with pytest.raises(TypeError):
92103
ArrayProxy(bio, object())
104+
with pytest.raises(ValueError):
105+
ArrayProxy(bio, hdr, order='badval')
93106

94107

95108
def test_tuplespec():
@@ -154,33 +167,87 @@ def test_nifti1_init():
154167
assert_array_equal(np.asarray(ap), arr * 2.0 + 10)
155168

156169

157-
def test_proxy_slicing():
158-
shapes = (15, 16, 17)
159-
for n_dim in range(1, len(shapes) + 1):
160-
shape = shapes[:n_dim]
161-
arr = np.arange(np.prod(shape)).reshape(shape)
162-
for offset in (0, 20):
163-
hdr = Nifti1Header()
164-
hdr.set_data_offset(offset)
165-
hdr.set_data_dtype(arr.dtype)
166-
hdr.set_data_shape(shape)
167-
for order, klass in ('F', ArrayProxy), ('C', CArrayProxy):
168-
fobj = BytesIO()
169-
fobj.write(b'\0' * offset)
170-
fobj.write(arr.tobytes(order=order))
171-
prox = klass(fobj, hdr)
172-
for sliceobj in slicer_samples(shape):
173-
assert_array_equal(arr[sliceobj], prox[sliceobj])
174-
# Check slicing works with scaling
170+
@pytest.mark.parametrize("n_dim", (1, 2, 3))
171+
@pytest.mark.parametrize("offset", (0, 20))
172+
def test_proxy_slicing(n_dim, offset):
173+
shape = (15, 16, 17)[:n_dim]
174+
arr = np.arange(np.prod(shape)).reshape(shape)
175+
hdr = Nifti1Header()
176+
hdr.set_data_offset(offset)
177+
hdr.set_data_dtype(arr.dtype)
178+
hdr.set_data_shape(shape)
179+
for order, klass in ('F', ArrayProxy), ('C', CArrayProxy):
180+
fobj = BytesIO()
181+
fobj.write(b'\0' * offset)
182+
fobj.write(arr.tobytes(order=order))
183+
prox = klass(fobj, hdr)
184+
assert prox.order == order
185+
for sliceobj in slicer_samples(shape):
186+
assert_array_equal(arr[sliceobj], prox[sliceobj])
187+
188+
189+
def test_proxy_slicing_with_scaling():
190+
shape = (15, 16, 17)
191+
offset = 20
192+
arr = np.arange(np.prod(shape)).reshape(shape)
193+
hdr = Nifti1Header()
194+
hdr.set_data_offset(offset)
195+
hdr.set_data_dtype(arr.dtype)
196+
hdr.set_data_shape(shape)
175197
hdr.set_slope_inter(2.0, 1.0)
176198
fobj = BytesIO()
177-
fobj.write(b'\0' * offset)
199+
fobj.write(bytes(offset))
178200
fobj.write(arr.tobytes(order='F'))
179201
prox = ArrayProxy(fobj, hdr)
180202
sliceobj = (None, slice(None), 1, -1)
181203
assert_array_equal(arr[sliceobj] * 2.0 + 1.0, prox[sliceobj])
182204

183205

206+
@pytest.mark.parametrize("order", ("C", "F"))
207+
def test_order_override(order):
208+
shape = (15, 16, 17)
209+
arr = np.arange(np.prod(shape)).reshape(shape)
210+
fobj = BytesIO()
211+
fobj.write(arr.tobytes(order=order))
212+
for klass in (ArrayProxy, CArrayProxy):
213+
prox = klass(fobj, (shape, arr.dtype), order=order)
214+
assert prox.order == order
215+
sliceobj = (None, slice(None), 1, -1)
216+
assert_array_equal(arr[sliceobj], prox[sliceobj])
217+
218+
219+
def test_deprecated_order_classvar():
220+
shape = (15, 16, 17)
221+
arr = np.arange(np.prod(shape)).reshape(shape)
222+
fobj = BytesIO()
223+
fobj.write(arr.tobytes(order='C'))
224+
sliceobj = (None, slice(None), 1, -1)
225+
226+
# We don't really care about the original order, just that the behavior
227+
# of the deprecated mode matches the new behavior
228+
fprox = ArrayProxy(fobj, (shape, arr.dtype), order='F')
229+
cprox = ArrayProxy(fobj, (shape, arr.dtype), order='C')
230+
231+
# Start raising errors when we crank the dev version
232+
if Version(__version__) >= Version('7.0.0.dev0'):
233+
cm = pytest.raises(ExpiredDeprecationError)
234+
else:
235+
cm = pytest.deprecated_call()
236+
237+
with cm:
238+
prox = DeprecatedCArrayProxy(fobj, (shape, arr.dtype))
239+
assert prox.order == 'C'
240+
assert_array_equal(prox[sliceobj], cprox[sliceobj])
241+
with cm:
242+
prox = DeprecatedCArrayProxy(fobj, (shape, arr.dtype), order='C')
243+
assert prox.order == 'C'
244+
assert_array_equal(prox[sliceobj], cprox[sliceobj])
245+
with cm:
246+
prox = DeprecatedCArrayProxy(fobj, (shape, arr.dtype), order='F')
247+
assert prox.order == 'F'
248+
assert_array_equal(prox[sliceobj], fprox[sliceobj])
249+
250+
184251
def test_is_proxy():
185252
# Test is_proxy function
186253
hdr = FunkyHeader((2, 3, 4))

0 commit comments

Comments
 (0)