Skip to content

Commit 8e5aa81

Browse files
committed
Merge pull request #328 from bcipolli/deprecate_binopener
MRG: More graceful handling of compressed file Opening This PR deprecates BinOpener, instead exposing a decorator for creating arbitrary file extension / opener methods (similar to that proposed in #317 and created in #319).
2 parents 6dc6bae + f4c20f3 commit 8e5aa81

File tree

15 files changed

+134
-61
lines changed

15 files changed

+134
-61
lines changed

bin/nib-nifti-dx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ def main():
2525
(opts, files) = parser.parse_args()
2626

2727
for fname in files:
28-
with nib.volumeutils.BinOpener(fname) as fobj:
29-
hdr = fobj.read(nib.nifti1.header_dtype.itemsize)
28+
with nib.openers.ImageOpener(fname) as fobj:
29+
hdr = fobj.read(nib.nifti1.header_dtype.itemsize)
3030
result = nib.Nifti1Header.diagnose_binaryblock(hdr)
3131
if len(result):
3232
print('Picky header check output for "%s"\n' % fname)

nibabel/arrayproxy.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@
2727
"""
2828
import warnings
2929

30-
from .volumeutils import BinOpener, array_from_file, apply_read_scaling
30+
from .volumeutils import array_from_file, apply_read_scaling
3131
from .fileslice import fileslice
3232
from .keywordonly import kw_only_meth
33+
from .openers import ImageOpener
3334

3435

3536
class ArrayProxy(object):
@@ -130,7 +131,7 @@ def get_unscaled(self):
130131
131132
This is an optional part of the proxy API
132133
'''
133-
with BinOpener(self.file_like) as fileobj:
134+
with ImageOpener(self.file_like) as fileobj:
134135
raw_data = array_from_file(self._shape,
135136
self._dtype,
136137
fileobj,
@@ -145,7 +146,7 @@ def __array__(self):
145146
return apply_read_scaling(raw_data, self._slope, self._inter)
146147

147148
def __getitem__(self, slicer):
148-
with BinOpener(self.file_like) as fileobj:
149+
with ImageOpener(self.file_like) as fileobj:
149150
raw_data = fileslice(fileobj,
150151
slicer,
151152
self._shape,

nibabel/benchmarks/bench_fileslice.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import numpy as np
2020

2121
from io import BytesIO
22-
from ..openers import Opener
22+
from ..openers import ImageOpener
2323
from ..fileslice import fileslice
2424
from ..rstutils import rst_table
2525
from ..tmpdirs import InTemporaryDirectory
@@ -47,10 +47,10 @@ def run_slices(file_like, repeat=3, offset=0, order='F'):
4747
n_dim = len(SHAPE)
4848
n_slicers = len(_slices_for_len(1))
4949
times_arr = np.zeros((n_dim, n_slicers))
50-
with Opener(file_like, 'wb') as fobj:
50+
with ImageOpener(file_like, 'wb') as fobj:
5151
fobj.write(b'\0' * offset)
5252
fobj.write(arr.tostring(order=order))
53-
with Opener(file_like, 'rb') as fobj:
53+
with ImageOpener(file_like, 'rb') as fobj:
5454
for i, L in enumerate(SHAPE):
5555
for j, slicer in enumerate(_slices_for_len(L)):
5656
sliceobj = [slice(None)] * n_dim

nibabel/fileholders.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from copy import copy
1212

13-
from .volumeutils import BinOpener
13+
from .openers import ImageOpener
1414

1515

1616
class FileHolderError(Exception):
@@ -63,10 +63,10 @@ def get_prepare_fileobj(self, *args, **kwargs):
6363
``self.pos``
6464
'''
6565
if self.fileobj is not None:
66-
obj = BinOpener(self.fileobj) # for context manager
66+
obj = ImageOpener(self.fileobj) # for context manager
6767
obj.seek(self.pos)
6868
elif self.filename is not None:
69-
obj = BinOpener(self.filename, *args, **kwargs)
69+
obj = ImageOpener(self.filename, *args, **kwargs)
7070
if self.pos != 0:
7171
obj.seek(self.pos)
7272
else:

nibabel/freesurfer/mghformat.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from ..fileholders import FileHolder, copy_file_map
1919
from ..arrayproxy import ArrayProxy
2020
from ..keywordonly import kw_only_meth
21+
from ..openers import ImageOpener
2122

2223
# mgh header
2324
# See https://surfer.nmr.mgh.harvard.edu/fswiki/FsTutorial/MghFormat
@@ -453,6 +454,7 @@ def writeftr_to(self, fileobj):
453454
fileobj.write(ftr_nd.tostring())
454455

455456

457+
@ImageOpener.register_ext_from_image('.mgz', ImageOpener.gz_def)
456458
class MGHImage(SpatialImage):
457459
""" Class for MGH format image
458460
"""

nibabel/loadsave.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import numpy as np
1313

1414
from .filename_parser import types_filenames, splitext_addext
15-
from .volumeutils import BinOpener, Opener
15+
from .openers import ImageOpener
1616
from .analyze import AnalyzeImage
1717
from .spm2analyze import Spm2AnalyzeImage
1818
from .nifti1 import Nifti1Image, Nifti1Pair, header_dtype as ni1_hdr_dtype
@@ -68,18 +68,18 @@ def guessed_image_type(filename):
6868
elif lext == '.mnc':
6969
# Look for HDF5 signature for MINC2
7070
# https://www.hdfgroup.org/HDF5/doc/H5.format.html
71-
with Opener(filename) as fobj:
71+
with ImageOpener(filename) as fobj:
7272
signature = fobj.read(4)
7373
klass = Minc2Image if signature == b'\211HDF' else Minc1Image
7474
elif lext == '.nii':
75-
with BinOpener(filename) as fobj:
75+
with ImageOpener(filename) as fobj:
7676
binaryblock = fobj.read(348)
7777
ft = which_analyze_type(binaryblock)
7878
klass = Nifti2Image if ft == 'nifti2' else Nifti1Image
7979
else: # might be nifti 1 or 2 pair or analyze of some sort
8080
files_types = (('image', '.img'), ('header', '.hdr'))
8181
filenames = types_filenames(filename, files_types)
82-
with BinOpener(filenames['header']) as fobj:
82+
with ImageOpener(filenames['header']) as fobj:
8383
binaryblock = fobj.read(348)
8484
ft = which_analyze_type(binaryblock)
8585
if ft == 'nifti2':
@@ -208,7 +208,7 @@ def read_img_data(img, prefer='scaled'):
208208
hdr.set_data_offset(dao.offset)
209209
if default_scaling and (dao.slope, dao.inter) != (1, 0):
210210
hdr.set_slope_inter(dao.slope, dao.inter)
211-
with BinOpener(img_file_like) as fileobj:
211+
with ImageOpener(img_file_like) as fileobj:
212212
if prefer == 'scaled':
213213
return hdr.data_from_fileobj(fileobj)
214214
return hdr.raw_data_from_fileobj(fileobj)

nibabel/nicom/dicomwrappers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
from . import csareader as csar
2020
from .dwiparams import B2q, nearest_pos_semi_def, q2bg
21-
from ..volumeutils import BinOpener
21+
from ..openers import ImageOpener
2222
from ..onetime import setattr_on_read as one_time
2323

2424

@@ -51,7 +51,7 @@ def wrapper_from_file(file_like, *args, **kwargs):
5151
"""
5252
import dicom
5353

54-
with BinOpener(file_like) as fobj:
54+
with ImageOpener(file_like) as fobj:
5555
dcm_data = dicom.read_file(fobj, *args, **kwargs)
5656
return wrapper_from_data(dcm_data)
5757

nibabel/openers.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,42 @@ def __enter__(self):
146146

147147
def __exit__(self, exc_type, exc_val, exc_tb):
148148
self.close_if_mine()
149+
150+
151+
class ImageOpener(Opener):
152+
""" Opener-type class passed to image classes to collect compressed extensions
153+
154+
This class allows itself to have image extensions added to its class
155+
attributes, via the `register_ex_from_images`. The class can therefore
156+
change state when image classes are defined.
157+
"""
158+
159+
@classmethod
160+
def register_ext_from_image(opener_klass, ext, func_def):
161+
"""Decorator for adding extension / opener_function associations.
162+
163+
Should be used to decorate classes.
164+
165+
Parameters
166+
----------
167+
opener_klass : decorated class
168+
ext : file extension to associate `func_def` with.
169+
should start with '.'
170+
func_def : opener function/parameter tuple
171+
Should be a `(function, (args,))` tuple, where `function` accepts
172+
a filename as the first parameter, and `args` defines the
173+
other arguments that `function` accepts. These arguments must
174+
be any (unordered) subset of `mode`, `compresslevel`,
175+
and `buffering`.
176+
177+
Returns
178+
-------
179+
opener_klass, with a side-effect of updating the ImageOpener class
180+
with the desired extension / opener association.
181+
"""
182+
def decorate(klass):
183+
assert ext not in opener_klass.compress_ext_map, \
184+
"Cannot redefine extension-function mappings."
185+
opener_klass.compress_ext_map[ext] = func_def
186+
return klass
187+
return decorate

nibabel/parrec.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,11 @@
9999
from .keywordonly import kw_only_meth
100100
from .spatialimages import SpatialImage, Header
101101
from .eulerangles import euler2mat
102-
from .volumeutils import Recoder, array_from_file, BinOpener
102+
from .volumeutils import Recoder, array_from_file
103103
from .affines import from_matvec, dot_reduce, apply_affine
104104
from .nifti1 import unit_codes
105105
from .fileslice import fileslice, strided_scalar
106+
from .openers import ImageOpener
106107

107108
# PSL to RAS affine
108109
PSL_TO_RAS = np.array([[0, 0, -1, 0], # L -> R
@@ -581,13 +582,13 @@ def is_proxy(self):
581582
return True
582583

583584
def get_unscaled(self):
584-
with BinOpener(self.file_like) as fileobj:
585+
with ImageOpener(self.file_like) as fileobj:
585586
return _data_from_rec(fileobj, self._rec_shape, self._dtype,
586587
self._slice_indices, self._shape,
587588
mmap=self._mmap)
588589

589590
def __array__(self):
590-
with BinOpener(self.file_like) as fileobj:
591+
with ImageOpener(self.file_like) as fileobj:
591592
return _data_from_rec(fileobj,
592593
self._rec_shape,
593594
self._dtype,
@@ -603,7 +604,7 @@ def __getitem__(self, slicer):
603604
return np.asanyarray(self)[slicer]
604605
# Slices all sequential from zero, can use fileslice
605606
# This gives more efficient volume by volume loading, for example
606-
with BinOpener(self.file_like) as fileobj:
607+
with ImageOpener(self.file_like) as fileobj:
607608
raw_data = fileslice(fileobj, slicer, self._shape, self._dtype, 0,
608609
'F')
609610
# Broadcast scaling to shape of original data

nibabel/tests/test_helpers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import numpy as np
66

7-
from ..openers import Opener
7+
from ..openers import ImageOpener
88
from ..tmpdirs import InTemporaryDirectory
99
from ..optpkg import optional_package
1010
_, have_scipy, _ = optional_package('scipy.io')
@@ -49,7 +49,7 @@ def bz2_mio_error():
4949
import scipy.io
5050

5151
with InTemporaryDirectory():
52-
with Opener('test.mat.bz2', 'wb') as fobj:
52+
with ImageOpener('test.mat.bz2', 'wb') as fobj:
5353
try:
5454
scipy.io.savemat(fobj, {'a': 1}, format='4')
5555
except ValueError:

0 commit comments

Comments
 (0)