Skip to content

Commit 32e3b05

Browse files
committed
BF: fileslice.fileslice and read_segments functions optionally accept a
threading.Lock to protect seek/read pairs. ArrayProxy creates said lock, and passes it into fileslice calls. Also fixed typo in openers.py from previous commit
1 parent 3d170be commit 32e3b05

File tree

3 files changed

+30
-10
lines changed

3 files changed

+30
-10
lines changed

nibabel/arrayproxy.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
See :mod:`nibabel.tests.test_proxy_api` for proxy API conformance checks.
2727
"""
2828
from contextlib import contextmanager
29+
from threading import Lock
2930

3031
import numpy as np
3132

@@ -110,7 +111,6 @@ def __init__(self, file_like, spec, mmap=True, keep_file_open=False):
110111
if mmap not in (True, False, 'c', 'r'):
111112
raise ValueError("mmap should be one of {True, False, 'c', 'r'}")
112113
self.file_like = file_like
113-
self._keep_file_open = keep_file_open
114114
if hasattr(spec, 'get_data_shape'):
115115
slope, inter = spec.get_slope_inter()
116116
par = (spec.get_data_shape(),
@@ -131,6 +131,8 @@ def __init__(self, file_like, spec, mmap=True, keep_file_open=False):
131131
# Permit any specifier that can be interpreted as a numpy dtype
132132
self._dtype = np.dtype(self._dtype)
133133
self._mmap = mmap
134+
self._keep_file_open = keep_file_open
135+
self._lock = Lock()
134136

135137
def __del__(self):
136138
'''If this ``ArrayProxy`` was created with ``keep_file_open=True``,
@@ -210,7 +212,8 @@ def __getitem__(self, slicer):
210212
self._shape,
211213
self._dtype,
212214
self._offset,
213-
order=self.order)
215+
order=self.order,
216+
lock=self._lock)
214217
# Upcast as necessary for big slopes, intercepts
215218
return apply_read_scaling(raw_data, self._slope, self._inter)
216219

nibabel/fileslice.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
""" Utilities for getting array slices out of file-like objects
22
"""
33
from __future__ import division
4+
from contextlib import contextmanager
45

56
import operator
67
from numbers import Integral
@@ -622,7 +623,7 @@ def slicers2segments(read_slicers, in_shape, offset, itemsize):
622623
return all_segments
623624

624625

625-
def read_segments(fileobj, segments, n_bytes):
626+
def read_segments(fileobj, segments, n_bytes, lock=None):
626627
""" Read `n_bytes` byte data implied by `segments` from `fileobj`
627628
628629
Parameters
@@ -634,29 +635,42 @@ def read_segments(fileobj, segments, n_bytes):
634635
absolute file offset in bytes and number of bytes to read
635636
n_bytes : int
636637
total number of bytes that will be read
638+
lock : threading.Lock
639+
If provided, used to ensure that paired calls to ``seek`` and ``read``
640+
cannot be interrupted by another thread accessing the same ``fileobj``.
641+
637642
638643
Returns
639644
-------
640645
buffer : buffer object
641646
object implementing buffer protocol, such as byte string or ndarray or
642647
mmap or ctypes ``c_char_array``
643648
"""
649+
# Make a dummy lock-like thing to make the code below a bit nicer
650+
if lock is None:
651+
@contextmanager
652+
def dummy_lock():
653+
yield
654+
lock = dummy_lock
655+
644656
if len(segments) == 0:
645657
if n_bytes != 0:
646658
raise ValueError("No segments, but non-zero n_bytes")
647659
return b''
648660
if len(segments) == 1:
649661
offset, length = segments[0]
650-
fileobj.seek(offset)
651-
bytes = fileobj.read(length)
662+
with lock:
663+
fileobj.seek(offset)
664+
bytes = fileobj.read(length)
652665
if len(bytes) != n_bytes:
653666
raise ValueError("Whoops, not enough data in file")
654667
return bytes
655668
# More than one segment
656669
bytes = mmap(-1, n_bytes)
657670
for offset, length in segments:
658-
fileobj.seek(offset)
659-
bytes.write(fileobj.read(length))
671+
with lock:
672+
fileobj.seek(offset)
673+
bytes.write(fileobj.read(length))
660674
if bytes.tell() != n_bytes:
661675
raise ValueError("Oh dear, n_bytes does not look right")
662676
return bytes
@@ -700,7 +714,7 @@ def _simple_fileslice(fileobj, sliceobj, shape, dtype, offset=0, order='C',
700714

701715

702716
def fileslice(fileobj, sliceobj, shape, dtype, offset=0, order='C',
703-
heuristic=threshold_heuristic):
717+
heuristic=threshold_heuristic, lock=None):
704718
""" Slice array in `fileobj` using `sliceobj` slicer and array definitions
705719
706720
`fileobj` contains the contiguous binary data for an array ``A`` of shape,
@@ -737,6 +751,9 @@ def fileslice(fileobj, sliceobj, shape, dtype, offset=0, order='C',
737751
returning one of 'full', 'contiguous', None. See
738752
:func:`optimize_slicer` and see :func:`threshold_heuristic` for an
739753
example.
754+
lock: threading.Lock, optional
755+
If provided, used to ensure that paired calls to ``seek`` and ``read``
756+
cannot be interrupted by another thread accessing the same ``fileobj``.
740757
741758
Returns
742759
-------
@@ -750,7 +767,7 @@ def fileslice(fileobj, sliceobj, shape, dtype, offset=0, order='C',
750767
segments, sliced_shape, post_slicers = calc_slicedefs(
751768
sliceobj, shape, itemsize, offset, order)
752769
n_bytes = reduce(operator.mul, sliced_shape, 1) * itemsize
753-
bytes = read_segments(fileobj, segments, n_bytes)
770+
bytes = read_segments(fileobj, segments, n_bytes, lock)
754771
sliced = np.ndarray(sliced_shape, dtype, buffer=bytes, order=order)
755772
return sliced[post_slicers]
756773

nibabel/openers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def _gzip_open(fileish, mode='rb', compresslevel=9):
7373
is_file = hasattr(fileish, 'read') and hasattr(fileish, 'write') and \
7474
hasattr(fileish, 'mode')
7575
if is_file:
76-
mode = file.mode
76+
mode = fileish.mode
7777

7878
# use indexed_gzip if possible for faster read access
7979
if mode == 'rb' and have_indexed_gzip:

0 commit comments

Comments
 (0)