Skip to content

Commit 0cfd094

Browse files
committed
Merge pull request #276 from matthew-brett/in-memory
MRG: API to help modify image caching Add 'caching='fill'|'unchanged' keyword to get_data method. Add in_memory property to tell user whether image already has an in-memory copy of the data.
2 parents 1995aa3 + ec90b0a commit 0cfd094

File tree

2 files changed

+88
-20
lines changed

2 files changed

+88
-20
lines changed

nibabel/spatialimages.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ def from_header(klass, header=None):
176176
if header is None:
177177
return klass()
178178
# I can't do isinstance here because it is not necessarily true
179-
# that a subclass has exactly the same interface as it's parent
179+
# that a subclass has exactly the same interface as its parent
180180
# - for example Nifti1Images inherit from Analyze, but have
181181
# different field names
182182
if type(header) == klass:
@@ -448,21 +448,53 @@ def __str__(self):
448448
'metadata:',
449449
'%s' % self._header))
450450

451-
def get_data(self):
451+
def get_data(self, caching='fill'):
452452
""" Return image data from image with any necessary scalng applied
453453
454454
If the image data is a array proxy (data not yet read from disk) then
455-
read the data, and store in an internal cache. Future calls to
456-
``get_data`` will return the cached copy.
455+
the default behavior (`caching` == "fill") is to read the data, and
456+
store in an internal cache. Future calls to ``get_data`` will return
457+
the cached copy.
458+
459+
Once the data has been cached and returned from a proxy array, the
460+
cached array can be modified by modifying the returned array, because
461+
the returned array is a reference to the array in the cache. Regardless
462+
of the `caching` flag, this is always true of an in-memory image (where
463+
the image data is an array rather than an array proxy).
464+
465+
Parameters
466+
----------
467+
caching : {'fill', 'unchanged'}, optional
468+
This argument has no effect in the case where the image data is an
469+
array, or the image data has already been cached. If the image data
470+
is an array proxy, and the image data has not yet been cached, then
471+
'fill' (the default) will read the data from the array proxy, and
472+
store in an internal cache, so that future calls to ``get_data``
473+
will return the cached copy. If 'unchanged' then leave the current
474+
state of caching unchanged; return the cached copy if it exists, if
475+
not, load the data from disk and return that, but without filling
476+
the cache.
457477
458478
Returns
459479
-------
460480
data : array
461481
array of image data
462482
"""
463-
if self._data_cache is None:
464-
self._data_cache = np.asanyarray(self._dataobj)
465-
return self._data_cache
483+
if caching not in ('fill', 'unchanged'):
484+
raise ValueError('caching value should be "fill" or "unchanged"')
485+
if self._data_cache is not None:
486+
return self._data_cache
487+
data = np.asanyarray(self._dataobj)
488+
if caching == 'fill':
489+
self._data_cache = data
490+
return data
491+
492+
@property
493+
def in_memory(self):
494+
""" True when array data is in memory
495+
"""
496+
return (isinstance(self._dataobj, np.ndarray)
497+
or self._data_cache is not None)
466498

467499
def uncache(self):
468500
""" Delete any cached read of data from proxied data

nibabel/tests/test_image_api.py

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717
array creation. If it does, this call empties that cache. Implement this
1818
as a no-op if ``get_data()`` does not cache.
1919
* ``img[something]`` generates an informative TypeError
20+
* ``img.in_memory`` is True for an array image, and for a proxy image that is
21+
cached, but False otherwise.
2022
"""
2123
from __future__ import division, print_function, absolute_import
2224

2325
import warnings
26+
from functools import partial
2427

2528
import numpy as np
2629

@@ -172,8 +175,23 @@ def validate_data(self, imaker, params):
172175
assert_false(isinstance(img.dataobj, np.ndarray))
173176
proxy_data = np.asarray(img.dataobj)
174177
proxy_copy = proxy_data.copy()
178+
# Not yet cached, proxy image: in_memory is False
179+
assert_false(img.in_memory)
180+
# Load with caching='unchanged'
181+
data = img.get_data(caching='unchanged')
182+
# Still not cached
183+
assert_false(img.in_memory)
184+
# Default load, does caching
175185
data = img.get_data()
186+
# Data now cached
187+
assert_true(img.in_memory)
176188
assert_false(proxy_data is data)
189+
# Now caching='unchanged' does nothing, returns cached version
190+
data_again = img.get_data(caching='unchanged')
191+
assert_true(data is data_again)
192+
# caching='fill' does nothing because the cache is already full
193+
data_yet_again = img.get_data(caching='fill')
194+
assert_true(data is data_yet_again)
177195
# changing array data does not change proxy data, or reloaded data
178196
data[:] = 42
179197
assert_array_equal(proxy_data, proxy_copy)
@@ -182,23 +200,41 @@ def validate_data(self, imaker, params):
182200
assert_array_equal(img.get_data(), 42)
183201
# until we uncache
184202
img.uncache()
203+
# Which unsets in_memory
204+
assert_false(img.in_memory)
185205
assert_array_equal(img.get_data(), proxy_copy)
206+
# Check caching='fill' does cache data
207+
img = imaker()
208+
assert_false(img.in_memory)
209+
data = img.get_data(caching='fill')
210+
assert_true(img.in_memory)
211+
data_again = img.get_data()
212+
assert_true(data is data_again)
186213
else: # not proxy
187-
assert_true(isinstance(img.dataobj, np.ndarray))
188-
non_proxy_data = np.asarray(img.dataobj)
189-
data = img.get_data()
190-
assert_true(non_proxy_data is data)
191-
# changing array data does change proxy data, and reloaded data
192-
data[:] = 42
193-
assert_array_equal(np.asarray(img.dataobj), 42)
194-
# It does change the result of get_data
195-
assert_array_equal(img.get_data(), 42)
196-
# Unache has no effect
197-
img.uncache()
198-
assert_array_equal(img.get_data(), 42)
199-
# Read only
214+
for caching in (None, 'fill', 'unchanged'):
215+
img = imaker()
216+
get_data_func = (img.get_data if caching is None else
217+
partial(img.get_data, caching=caching))
218+
assert_true(isinstance(img.dataobj, np.ndarray))
219+
assert_true(img.in_memory)
220+
data = get_data_func()
221+
assert_true(data is img.dataobj)
222+
# changing array data does change proxy data, and reloaded data
223+
data[:] = 42
224+
assert_array_equal(np.asarray(img.dataobj), 42)
225+
# It does change the result of get_data
226+
assert_array_equal(get_data_func(), 42)
227+
# Unache has no effect
228+
img.uncache()
229+
assert_array_equal(get_data_func(), 42)
230+
assert_true(img.in_memory)
231+
# dataobj is read only
200232
fake_data = np.zeros(img.shape).astype(img.get_data_dtype())
201233
assert_raises(AttributeError, setattr, img, 'dataobj', fake_data)
234+
# So is in_memory
235+
assert_raises(AttributeError, setattr, img, 'in_memory', False)
236+
# Values to get_data caching parameter must be 'fill' or 'unchanged'
237+
assert_raises(ValueError, img.get_data, caching='something')
202238

203239
def validate_data_deprecated(self, imaker, params):
204240
# Check _data property still exists, but raises warning

0 commit comments

Comments
 (0)