diff --git a/nibabel/spatialimages.py b/nibabel/spatialimages.py index 73104e4ca5..4736072a79 100644 --- a/nibabel/spatialimages.py +++ b/nibabel/spatialimages.py @@ -176,7 +176,7 @@ def from_header(klass, header=None): if header is None: return klass() # I can't do isinstance here because it is not necessarily true - # that a subclass has exactly the same interface as it's parent + # that a subclass has exactly the same interface as its parent # - for example Nifti1Images inherit from Analyze, but have # different field names if type(header) == klass: @@ -448,21 +448,53 @@ def __str__(self): 'metadata:', '%s' % self._header)) - def get_data(self): + def get_data(self, caching='fill'): """ Return image data from image with any necessary scalng applied If the image data is a array proxy (data not yet read from disk) then - read the data, and store in an internal cache. Future calls to - ``get_data`` will return the cached copy. + the default behavior (`caching` == "fill") is to read the data, and + store in an internal cache. Future calls to ``get_data`` will return + the cached copy. + + Once the data has been cached and returned from a proxy array, the + cached array can be modified by modifying the returned array, because + the returned array is a reference to the array in the cache. Regardless + of the `caching` flag, this is always true of an in-memory image (where + the image data is an array rather than an array proxy). + + Parameters + ---------- + caching : {'fill', 'unchanged'}, optional + This argument has no effect in the case where the image data is an + array, or the image data has already been cached. If the image data + is an array proxy, and the image data has not yet been cached, then + 'fill' (the default) will read the data from the array proxy, and + store in an internal cache, so that future calls to ``get_data`` + will return the cached copy. If 'unchanged' then leave the current + state of caching unchanged; return the cached copy if it exists, if + not, load the data from disk and return that, but without filling + the cache. Returns ------- data : array array of image data """ - if self._data_cache is None: - self._data_cache = np.asanyarray(self._dataobj) - return self._data_cache + if caching not in ('fill', 'unchanged'): + raise ValueError('caching value should be "fill" or "unchanged"') + if self._data_cache is not None: + return self._data_cache + data = np.asanyarray(self._dataobj) + if caching == 'fill': + self._data_cache = data + return data + + @property + def in_memory(self): + """ True when array data is in memory + """ + return (isinstance(self._dataobj, np.ndarray) + or self._data_cache is not None) def uncache(self): """ Delete any cached read of data from proxied data diff --git a/nibabel/tests/test_image_api.py b/nibabel/tests/test_image_api.py index 1565d165a2..fed9f85d5b 100644 --- a/nibabel/tests/test_image_api.py +++ b/nibabel/tests/test_image_api.py @@ -17,10 +17,13 @@ array creation. If it does, this call empties that cache. Implement this as a no-op if ``get_data()`` does not cache. * ``img[something]`` generates an informative TypeError +* ``img.in_memory`` is True for an array image, and for a proxy image that is + cached, but False otherwise. """ from __future__ import division, print_function, absolute_import import warnings +from functools import partial import numpy as np @@ -172,8 +175,23 @@ def validate_data(self, imaker, params): assert_false(isinstance(img.dataobj, np.ndarray)) proxy_data = np.asarray(img.dataobj) proxy_copy = proxy_data.copy() + # Not yet cached, proxy image: in_memory is False + assert_false(img.in_memory) + # Load with caching='unchanged' + data = img.get_data(caching='unchanged') + # Still not cached + assert_false(img.in_memory) + # Default load, does caching data = img.get_data() + # Data now cached + assert_true(img.in_memory) assert_false(proxy_data is data) + # Now caching='unchanged' does nothing, returns cached version + data_again = img.get_data(caching='unchanged') + assert_true(data is data_again) + # caching='fill' does nothing because the cache is already full + data_yet_again = img.get_data(caching='fill') + assert_true(data is data_yet_again) # changing array data does not change proxy data, or reloaded data data[:] = 42 assert_array_equal(proxy_data, proxy_copy) @@ -182,23 +200,41 @@ def validate_data(self, imaker, params): assert_array_equal(img.get_data(), 42) # until we uncache img.uncache() + # Which unsets in_memory + assert_false(img.in_memory) assert_array_equal(img.get_data(), proxy_copy) + # Check caching='fill' does cache data + img = imaker() + assert_false(img.in_memory) + data = img.get_data(caching='fill') + assert_true(img.in_memory) + data_again = img.get_data() + assert_true(data is data_again) else: # not proxy - assert_true(isinstance(img.dataobj, np.ndarray)) - non_proxy_data = np.asarray(img.dataobj) - data = img.get_data() - assert_true(non_proxy_data is data) - # changing array data does change proxy data, and reloaded data - data[:] = 42 - assert_array_equal(np.asarray(img.dataobj), 42) - # It does change the result of get_data - assert_array_equal(img.get_data(), 42) - # Unache has no effect - img.uncache() - assert_array_equal(img.get_data(), 42) - # Read only + for caching in (None, 'fill', 'unchanged'): + img = imaker() + get_data_func = (img.get_data if caching is None else + partial(img.get_data, caching=caching)) + assert_true(isinstance(img.dataobj, np.ndarray)) + assert_true(img.in_memory) + data = get_data_func() + assert_true(data is img.dataobj) + # changing array data does change proxy data, and reloaded data + data[:] = 42 + assert_array_equal(np.asarray(img.dataobj), 42) + # It does change the result of get_data + assert_array_equal(get_data_func(), 42) + # Unache has no effect + img.uncache() + assert_array_equal(get_data_func(), 42) + assert_true(img.in_memory) + # dataobj is read only fake_data = np.zeros(img.shape).astype(img.get_data_dtype()) assert_raises(AttributeError, setattr, img, 'dataobj', fake_data) + # So is in_memory + assert_raises(AttributeError, setattr, img, 'in_memory', False) + # Values to get_data caching parameter must be 'fill' or 'unchanged' + assert_raises(ValueError, img.get_data, caching='something') def validate_data_deprecated(self, imaker, params): # Check _data property still exists, but raises warning