25
25
26
26
See :mod:`nibabel.tests.test_proxy_api` for proxy API conformance checks.
27
27
"""
28
+ from contextlib import contextmanager
29
+ from threading import RLock
30
+
28
31
import numpy as np
29
32
30
33
from .deprecated import deprecate_with_version
31
34
from .volumeutils import array_from_file , apply_read_scaling
32
35
from .fileslice import fileslice
33
36
from .keywordonly import kw_only_meth
34
- from .openers import ImageOpener
37
+ from .openers import ImageOpener , HAVE_INDEXED_GZIP
38
+
39
+
40
+ """This flag controls whether a new file handle is created every time an image
41
+ is accessed through an ``ArrayProxy``, or a single file handle is created and
42
+ used for the lifetime of the ``ArrayProxy``. It should be set to one of
43
+ ``True``, ``False``, or ``'auto'``.
44
+
45
+ If ``True``, a single file handle is created and used. If ``False``, a new
46
+ file handle is created every time the image is accessed. If ``'auto'``, and
47
+ the optional ``indexed_gzip`` dependency is present, a single file handle is
48
+ created and persisted. If ``indexed_gzip`` is not available, behaviour is the
49
+ same as if ``keep_file_open is False``.
50
+
51
+ If this is set to any other value, attempts to create an ``ArrayProxy`` without
52
+ specifying the ``keep_file_open`` flag will result in a ``ValueError`` being
53
+ raised.
54
+ """
55
+ KEEP_FILE_OPEN_DEFAULT = False
35
56
36
57
37
58
class ArrayProxy (object ):
@@ -69,8 +90,8 @@ class ArrayProxy(object):
69
90
_header = None
70
91
71
92
@kw_only_meth (2 )
72
- def __init__ (self , file_like , spec , mmap = True ):
73
- """ Initialize array proxy instance
93
+ def __init__ (self , file_like , spec , mmap = True , keep_file_open = None ):
94
+ """Initialize array proxy instance
74
95
75
96
Parameters
76
97
----------
@@ -99,8 +120,18 @@ def __init__(self, file_like, spec, mmap=True):
99
120
True gives the same behavior as ``mmap='c'``. If `file_like`
100
121
cannot be memory-mapped, ignore `mmap` value and read array from
101
122
file.
102
- scaling : {'fp', 'dv'}, optional, keyword only
103
- Type of scaling to use - see header ``get_data_scaling`` method.
123
+ keep_file_open : { None, 'auto', True, False }, optional, keyword only
124
+ `keep_file_open` controls whether a new file handle is created
125
+ every time the image is accessed, or a single file handle is
126
+ created and used for the lifetime of this ``ArrayProxy``. If
127
+ ``True``, a single file handle is created and used. If ``False``,
128
+ a new file handle is created every time the image is accessed. If
129
+ ``'auto'``, and the optional ``indexed_gzip`` dependency is
130
+ present, a single file handle is created and persisted. If
131
+ ``indexed_gzip`` is not available, behaviour is the same as if
132
+ ``keep_file_open is False``. If ``file_like`` is an open file
133
+ handle, this setting has no effect. The default value (``None``)
134
+ will result in the value of ``KEEP_FILE_OPEN_DEFAULT`` being used.
104
135
"""
105
136
if mmap not in (True , False , 'c' , 'r' ):
106
137
raise ValueError ("mmap should be one of {True, False, 'c', 'r'}" )
@@ -125,6 +156,70 @@ def __init__(self, file_like, spec, mmap=True):
125
156
# Permit any specifier that can be interpreted as a numpy dtype
126
157
self ._dtype = np .dtype (self ._dtype )
127
158
self ._mmap = mmap
159
+ self ._keep_file_open = self ._should_keep_file_open (file_like ,
160
+ keep_file_open )
161
+ self ._lock = RLock ()
162
+
163
+ def __del__ (self ):
164
+ """If this ``ArrayProxy`` was created with ``keep_file_open=True``,
165
+ the open file object is closed if necessary.
166
+ """
167
+ if hasattr (self , '_opener' ) and not self ._opener .closed :
168
+ self ._opener .close_if_mine ()
169
+ self ._opener = None
170
+
171
+ def __getstate__ (self ):
172
+ """Returns the state of this ``ArrayProxy`` during pickling. """
173
+ state = self .__dict__ .copy ()
174
+ state .pop ('_lock' , None )
175
+ return state
176
+
177
+ def __setstate__ (self , state ):
178
+ """Sets the state of this ``ArrayProxy`` during unpickling. """
179
+ self .__dict__ .update (state )
180
+ self ._lock = RLock ()
181
+
182
+ def _should_keep_file_open (self , file_like , keep_file_open ):
183
+ """Called by ``__init__``, and used to determine the final value of
184
+ ``keep_file_open``.
185
+
186
+ The return value is derived from these rules:
187
+
188
+ - If ``file_like`` is a file(-like) object, ``False`` is returned.
189
+ Otherwise, ``file_like`` is assumed to be a file name.
190
+ - if ``file_like`` ends with ``'gz'``, and the ``indexed_gzip``
191
+ library is available, ``True`` is returned.
192
+ - Otherwise, ``False`` is returned.
193
+
194
+ Parameters
195
+ ----------
196
+
197
+ file_like : object
198
+ File-like object or filename, as passed to ``__init__``.
199
+ keep_file_open : { 'auto', True, False }
200
+ Flag as passed to ``__init__``.
201
+
202
+ Returns
203
+ -------
204
+
205
+ The value of ``keep_file_open`` that will be used by this
206
+ ``ArrayProxy``.
207
+ """
208
+ if keep_file_open is None :
209
+ keep_file_open = KEEP_FILE_OPEN_DEFAULT
210
+ # if keep_file_open is True/False, we do what the user wants us to do
211
+ if isinstance (keep_file_open , bool ):
212
+ return keep_file_open
213
+ if keep_file_open != 'auto' :
214
+ raise ValueError ('keep_file_open should be one of {None, '
215
+ '\' auto\' , True, False}' )
216
+
217
+ # file_like is a handle - keep_file_open is irrelevant
218
+ if hasattr (file_like , 'read' ) and hasattr (file_like , 'seek' ):
219
+ return False
220
+ # Otherwise, if file_like is gzipped, and we have_indexed_gzip, we set
221
+ # keep_file_open to True, else we set it to False
222
+ return HAVE_INDEXED_GZIP and file_like .endswith ('gz' )
128
223
129
224
@property
130
225
@deprecate_with_version ('ArrayProxy.header deprecated' , '2.2' , '3.0' )
@@ -155,12 +250,33 @@ def inter(self):
155
250
def is_proxy (self ):
156
251
return True
157
252
253
+ @contextmanager
254
+ def _get_fileobj (self ):
255
+ """Create and return a new ``ImageOpener``, or return an existing one.
256
+
257
+ The specific behaviour depends on the value of the ``keep_file_open``
258
+ flag that was passed to ``__init__``.
259
+
260
+ Yields
261
+ ------
262
+ ImageOpener
263
+ A newly created ``ImageOpener`` instance, or an existing one,
264
+ which provides access to the file.
265
+ """
266
+ if self ._keep_file_open :
267
+ if not hasattr (self , '_opener' ):
268
+ self ._opener = ImageOpener (self .file_like )
269
+ yield self ._opener
270
+ else :
271
+ with ImageOpener (self .file_like ) as opener :
272
+ yield opener
273
+
158
274
def get_unscaled (self ):
159
- ''' Read of data from file
275
+ """ Read of data from file
160
276
161
277
This is an optional part of the proxy API
162
- '''
163
- with ImageOpener ( self .file_like ) as fileobj :
278
+ """
279
+ with self ._get_fileobj ( ) as fileobj , self . _lock :
164
280
raw_data = array_from_file (self ._shape ,
165
281
self ._dtype ,
166
282
fileobj ,
@@ -175,18 +291,19 @@ def __array__(self):
175
291
return apply_read_scaling (raw_data , self ._slope , self ._inter )
176
292
177
293
def __getitem__ (self , slicer ):
178
- with ImageOpener ( self .file_like ) as fileobj :
294
+ with self ._get_fileobj ( ) as fileobj :
179
295
raw_data = fileslice (fileobj ,
180
296
slicer ,
181
297
self ._shape ,
182
298
self ._dtype ,
183
299
self ._offset ,
184
- order = self .order )
300
+ order = self .order ,
301
+ lock = self ._lock )
185
302
# Upcast as necessary for big slopes, intercepts
186
303
return apply_read_scaling (raw_data , self ._slope , self ._inter )
187
304
188
305
def reshape (self , shape ):
189
- ''' Return an ArrayProxy with a new shape, without modifying data '''
306
+ """ Return an ArrayProxy with a new shape, without modifying data """
190
307
size = np .prod (self ._shape )
191
308
192
309
# Calculate new shape if not fully specified
0 commit comments