Skip to content

Commit 2c02fd3

Browse files
committed
Merge branch 'enh/arrayproxy_order' into rebase/biap0009
2 parents 295c25e + b7c179c commit 2c02fd3

File tree

2 files changed

+104
-23
lines changed

2 files changed

+104
-23
lines changed

nibabel/arrayproxy.py

Lines changed: 24 additions & 4 deletions
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,10 @@ 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'. The default order is 'F'.
119123
keep_file_open : { None, True, False }, optional, keyword only
120124
`keep_file_open` controls whether a new file handle is created
121125
every time the image is accessed, or a single file handle is
@@ -128,6 +132,8 @@ def __init__(self, file_like, spec, *, mmap=True, keep_file_open=None):
128132
"""
129133
if mmap not in (True, False, 'c', 'r'):
130134
raise ValueError("mmap should be one of {True, False, 'c', 'r'}")
135+
if order not in (None, 'C', 'F'):
136+
raise ValueError("order should be one of {'C', 'F'}")
131137
self.file_like = file_like
132138
if hasattr(spec, 'get_data_shape'):
133139
slope, inter = spec.get_slope_inter()
@@ -142,11 +148,25 @@ def __init__(self, file_like, spec, *, mmap=True, keep_file_open=None):
142148
else:
143149
raise TypeError('spec must be tuple of length 2-5 or header object')
144150

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

nibabel/tests/test_arrayproxy.py

Lines changed: 80 additions & 19 deletions
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,10 @@ def copy(self):
5760

5861
class CArrayProxy(ArrayProxy):
5962
# C array memory layout
63+
_default_order = 'C'
64+
65+
66+
class DeprecatedCArrayProxy(ArrayProxy):
6067
order = 'C'
6168

6269

@@ -154,33 +161,87 @@ def test_nifti1_init():
154161
assert_array_equal(np.asarray(ap), arr * 2.0 + 10)
155162

156163

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
164+
@pytest.mark.parametrize("n_dim", (1, 2, 3))
165+
@pytest.mark.parametrize("offset", (0, 20))
166+
def test_proxy_slicing(n_dim, offset):
167+
shape = (15, 16, 17)[:n_dim]
168+
arr = np.arange(np.prod(shape)).reshape(shape)
169+
hdr = Nifti1Header()
170+
hdr.set_data_offset(offset)
171+
hdr.set_data_dtype(arr.dtype)
172+
hdr.set_data_shape(shape)
173+
for order, klass in ('F', ArrayProxy), ('C', CArrayProxy):
174+
fobj = BytesIO()
175+
fobj.write(b'\0' * offset)
176+
fobj.write(arr.tobytes(order=order))
177+
prox = klass(fobj, hdr)
178+
assert prox.order == order
179+
for sliceobj in slicer_samples(shape):
180+
assert_array_equal(arr[sliceobj], prox[sliceobj])
181+
182+
183+
def test_proxy_slicing_with_scaling():
184+
shape = (15, 16, 17)
185+
offset = 20
186+
arr = np.arange(np.prod(shape)).reshape(shape)
187+
hdr = Nifti1Header()
188+
hdr.set_data_offset(offset)
189+
hdr.set_data_dtype(arr.dtype)
190+
hdr.set_data_shape(shape)
175191
hdr.set_slope_inter(2.0, 1.0)
176192
fobj = BytesIO()
177-
fobj.write(b'\0' * offset)
193+
fobj.write(bytes(offset))
178194
fobj.write(arr.tobytes(order='F'))
179195
prox = ArrayProxy(fobj, hdr)
180196
sliceobj = (None, slice(None), 1, -1)
181197
assert_array_equal(arr[sliceobj] * 2.0 + 1.0, prox[sliceobj])
182198

183199

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

0 commit comments

Comments
 (0)