Skip to content

Commit 9e60eeb

Browse files
committed
NF: Add ability for ArrayProxy to keep its ImageOpener open, instead of
creating a new on on every file access.
1 parent 23539e8 commit 9e60eeb

File tree

2 files changed

+51
-10
lines changed

2 files changed

+51
-10
lines changed

nibabel/analyze.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -934,7 +934,7 @@ def set_data_dtype(self, dtype):
934934

935935
@classmethod
936936
@kw_only_meth(1)
937-
def from_file_map(klass, file_map, mmap=True):
937+
def from_file_map(klass, file_map, mmap=True, keep_file_open=False):
938938
''' class method to create image from mapping in `file_map ``
939939
940940
Parameters
@@ -950,6 +950,11 @@ def from_file_map(klass, file_map, mmap=True):
950950
`mmap` value of True gives the same behavior as ``mmap='c'``. If
951951
image data file cannot be memory-mapped, ignore `mmap` value and
952952
read array from file.
953+
keep_file_open: If ``file_like`` is a file name, the default behaviour
954+
is to open a new file handle every time the data is accessed. If
955+
this flag is set to `True``, the file handle will be opened on the
956+
first access, and kept open until this ``ArrayProxy`` is garbage-
957+
collected.
953958
954959
Returns
955960
-------
@@ -964,7 +969,8 @@ def from_file_map(klass, file_map, mmap=True):
964969
imgf = img_fh.fileobj
965970
if imgf is None:
966971
imgf = img_fh.filename
967-
data = klass.ImageArrayProxy(imgf, hdr_copy, mmap=mmap)
972+
data = klass.ImageArrayProxy(imgf, hdr_copy, mmap=mmap,
973+
keep_file_open=keep_file_open)
968974
# Initialize without affine to allow header to pass through unmodified
969975
img = klass(data, None, header, file_map=file_map)
970976
# set affine from header though
@@ -976,7 +982,7 @@ def from_file_map(klass, file_map, mmap=True):
976982

977983
@classmethod
978984
@kw_only_meth(1)
979-
def from_filename(klass, filename, mmap=True):
985+
def from_filename(klass, filename, mmap=True, keep_file_open=False):
980986
''' class method to create image from filename `filename`
981987
982988
Parameters
@@ -990,6 +996,11 @@ def from_filename(klass, filename, mmap=True):
990996
`mmap` value of True gives the same behavior as ``mmap='c'``. If
991997
image data file cannot be memory-mapped, ignore `mmap` value and
992998
read array from file.
999+
keep_file_open: If ``file_like`` is a file name, the default behaviour
1000+
is to open a new file handle every time the data is accessed. If
1001+
this flag is set to `True``, the file handle will be opened on the
1002+
first access, and kept open until this ``ArrayProxy`` is garbage-
1003+
collected.
9931004
9941005
Returns
9951006
-------
@@ -998,7 +1009,8 @@ def from_filename(klass, filename, mmap=True):
9981009
if mmap not in (True, False, 'c', 'r'):
9991010
raise ValueError("mmap should be one of {True, False, 'c', 'r'}")
10001011
file_map = klass.filespec_to_file_map(filename)
1001-
return klass.from_file_map(file_map, mmap=mmap)
1012+
return klass.from_file_map(file_map, mmap=mmap,
1013+
keep_file_open=keep_file_open)
10021014

10031015
load = from_filename
10041016

nibabel/arrayproxy.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
2626
See :mod:`nibabel.tests.test_proxy_api` for proxy API conformance checks.
2727
"""
28+
from contextlib import contextmanager
29+
2830
import numpy as np
2931

3032
from .deprecated import deprecate_with_version
@@ -69,8 +71,8 @@ class ArrayProxy(object):
6971
_header = None
7072

7173
@kw_only_meth(2)
72-
def __init__(self, file_like, spec, mmap=True):
73-
""" Initialize array proxy instance
74+
def __init__(self, file_like, spec, mmap=True, keep_file_open=False):
75+
"""Initialize array proxy instance
7476
7577
Parameters
7678
----------
@@ -99,12 +101,16 @@ def __init__(self, file_like, spec, mmap=True):
99101
True gives the same behavior as ``mmap='c'``. If `file_like`
100102
cannot be memory-mapped, ignore `mmap` value and read array from
101103
file.
102-
scaling : {'fp', 'dv'}, optional, keyword only
103-
Type of scaling to use - see header ``get_data_scaling`` method.
104+
keep_file_open: If ``file_like`` is a file name, the default behaviour
105+
is to open a new file handle every time the data is accessed. If
106+
this flag is set to `True``, the file handle will be opened on the
107+
first access, and kept open until this ``ArrayProxy`` is garbage-
108+
collected.
104109
"""
105110
if mmap not in (True, False, 'c', 'r'):
106111
raise ValueError("mmap should be one of {True, False, 'c', 'r'}")
107112
self.file_like = file_like
113+
self._keep_file_open = keep_file_open
108114
if hasattr(spec, 'get_data_shape'):
109115
slope, inter = spec.get_slope_inter()
110116
par = (spec.get_data_shape(),
@@ -126,6 +132,15 @@ def __init__(self, file_like, spec, mmap=True):
126132
self._dtype = np.dtype(self._dtype)
127133
self._mmap = mmap
128134

135+
def __del__(self):
136+
'''If this ``ArrayProxy`` was created with ``keep_file_open=True``,
137+
the open file object is closed if necessary.
138+
'''
139+
if self._keep_file_open and hasattr(self, '_opener'):
140+
if not self._opener.closed:
141+
self._opener.close()
142+
self._opener = None
143+
129144
@property
130145
@deprecate_with_version('ArrayProxy.header deprecated', '2.2', '3.0')
131146
def header(self):
@@ -155,12 +170,26 @@ def inter(self):
155170
def is_proxy(self):
156171
return True
157172

173+
@contextmanager
174+
def _get_fileobj(self):
175+
'''Create and return a new ``ImageOpener``, or return an existing one.
176+
one. The specific behaviour depends on the value of the
177+
``keep_file_open`` flag that was passed to ``__init__``.
178+
'''
179+
if self._keep_file_open:
180+
if not hasattr(self, '_opener'):
181+
self._opener = ImageOpener(self.file_like)
182+
yield self._opener
183+
else:
184+
with ImageOpener(self.file_like) as opener:
185+
yield opener
186+
158187
def get_unscaled(self):
159188
''' Read of data from file
160189
161190
This is an optional part of the proxy API
162191
'''
163-
with ImageOpener(self.file_like) as fileobj:
192+
with self._get_fileobj() as fileobj:
164193
raw_data = array_from_file(self._shape,
165194
self._dtype,
166195
fileobj,
@@ -175,7 +204,7 @@ def __array__(self):
175204
return apply_read_scaling(raw_data, self._slope, self._inter)
176205

177206
def __getitem__(self, slicer):
178-
with ImageOpener(self.file_like) as fileobj:
207+
with self._get_fileobj() as fileobj:
179208
raw_data = fileslice(fileobj,
180209
slicer,
181210
self._shape,

0 commit comments

Comments
 (0)