Skip to content

Commit 853f5fb

Browse files
committed
Merge pull request #383 from bcipolli/issue-362
MRG: Fix for issue 362: Python 3.5 fails reading large gzipped files This not a nibabel bug, but works around a Python bug (https://bugs.python.org/issue25626). The workaround is: we wrap gzip.GzipFile with buffering, so that files > 4GB require multiple calls to GzipFile.readinto. I've also added test functionality: if NIPY_EXTRA_TESTS contains 'slow', slowly running tests can be run. I've used this to include a test for creation of a large file.
2 parents cf99743 + 3123456 commit 853f5fb

File tree

6 files changed

+134
-18
lines changed

6 files changed

+134
-18
lines changed

doc/source/devel/advanced_testing.rst

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
.. -*- mode: rst -*-
2+
.. ex: set sts=4 ts=4 sw=4 et tw=79:
3+
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ###
4+
#
5+
# See COPYING file distributed along with the NiBabel package for the
6+
# copyright and license terms.
7+
#
8+
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ###
9+
10+
.. _advanced_testing:
11+
12+
************
13+
Advanced Testing
14+
************
15+
16+
Setup
17+
-----
18+
19+
Before running advanced tests, please update all submodules of nibabel, by running ``git submodule update --init``
20+
21+
22+
Long-running tests
23+
------------------
24+
25+
Long-running tests are not enabled by default, and can be resource-intensive. To run these tests:
26+
27+
* Set environment variable ``NIPY_EXTRA_TESTS=slow``
28+
* Run ``nosetests``.
29+
30+
Note that some tests may require a machine with >4GB of RAM.
31+
32+
.. include:: ../links_names.txt

doc/source/devel/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ Developer documentation page
1414
add_image_format
1515
devdiscuss
1616
make_release
17+
advanced_testing

doc/source/installation.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,11 @@ Just install the modules by invoking::
114114
If sudo is not configured (or even installed) you might have to use
115115
``su`` instead.
116116

117-
Now fire up Python and try importing the module to see if everything is fine.
117+
118+
Validating your install
119+
-----------------------
120+
121+
For a basic test of your installation, fire up Python and try importing the module to see if everything is fine.
118122
It should look something like this::
119123

120124
Python 2.7.8 (v2.7.8:ee879c0ffa11, Jun 29 2014, 21:07:35)
@@ -123,4 +127,9 @@ It should look something like this::
123127
>>> import nibabel
124128
>>>
125129

130+
131+
To run the nibabel test suite, from the terminal run ``nosetests nibabel`` or ``python -c "import nibabel; nibabel.test()``.
132+
133+
To run an extended test suite that validates ``nibabel`` for long-running and resource-intensive cases, please see :ref:`advanced_testing`.
134+
126135
.. include:: links_names.txt

nibabel/openers.py

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,65 @@
99
""" Context manager openers for various fileobject types
1010
"""
1111

12-
from os.path import splitext
13-
import gzip
1412
import bz2
13+
import gzip
14+
import sys
15+
from os.path import splitext
16+
1517

1618
# The largest memory chunk that gzip can use for reads
1719
GZIP_MAX_READ_CHUNK = 100 * 1024 * 1024 # 100Mb
1820

1921

22+
class BufferedGzipFile(gzip.GzipFile):
23+
"""GzipFile able to readinto buffer >= 2**32 bytes.
24+
25+
This class only differs from gzip.GzipFile
26+
in Python 3.5.0.
27+
28+
This works around a known issue in Python 3.5.
29+
See https://bugs.python.org/issue25626"""
30+
31+
# This helps avoid defining readinto in Python 2.6,
32+
# where it is undefined on gzip.GzipFile.
33+
# It also helps limit the exposure to this code.
34+
if sys.version_info[:3] == (3, 5, 0):
35+
def __init__(self, fileish, mode='rb', compresslevel=9,
36+
buffer_size=2**32-1):
37+
super(BufferedGzipFile, self).__init__(fileish, mode=mode,
38+
compresslevel=compresslevel)
39+
self.buffer_size = buffer_size
40+
41+
def readinto(self, buf):
42+
"""Uses self.buffer_size to do a buffered read."""
43+
n_bytes = len(buf)
44+
if n_bytes < 2 ** 32:
45+
return super(BufferedGzipFile, self).readinto(buf)
46+
47+
# This works around a known issue in Python 3.5.
48+
# See https://bugs.python.org/issue25626
49+
mv = memoryview(buf)
50+
n_read = 0
51+
max_read = 2 ** 32 - 1 # Max for unsigned 32-bit integer
52+
while (n_read < n_bytes):
53+
n_wanted = min(n_bytes - n_read, max_read)
54+
n_got = super(BufferedGzipFile, self).readinto(
55+
mv[n_read:n_read + n_wanted])
56+
n_read += n_got
57+
if n_got != n_wanted:
58+
break
59+
return n_read
60+
61+
2062
def _gzip_open(fileish, *args, **kwargs):
21-
# open gzip files with faster reads on large files using larger chunks
63+
gzip_file = BufferedGzipFile(fileish, *args, **kwargs)
64+
65+
# Speedup for #209; attribute not present in in Python 3.5
66+
# open gzip files with faster reads on large files using larger
2267
# See https://github.com/nipy/nibabel/pull/210 for discussion
23-
gzip_file = gzip.open(fileish, *args, **kwargs)
24-
gzip_file.max_read_chunk = GZIP_MAX_READ_CHUNK
68+
if hasattr(gzip_file, 'max_chunk_read'):
69+
gzip_file.max_read_chunk = GZIP_MAX_READ_CHUNK
70+
2571
return gzip_file
2672

2773

nibabel/testing/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
''' Utilities for testing '''
1010
from __future__ import division, print_function
1111

12+
import os
1213
import sys
1314
import warnings
1415
from os.path import dirname, abspath, join as pjoin
1516

1617
import numpy as np
1718

19+
from numpy.testing.decorators import skipif
1820
# Allow failed import of nose if not now running tests
1921
try:
2022
from nose.tools import (assert_equal, assert_not_equal,
@@ -164,3 +166,12 @@ def __init__(self, *args, **kwargs):
164166
warnings.warn('catch_warn_reset is deprecated and will be removed in '
165167
'nibabel v3.0; use nibabel.testing.clear_and_catch_warnings.',
166168
FutureWarning)
169+
170+
171+
EXTRA_SET = os.environ.get('NIPY_EXTRA_TESTS', '').split(',')
172+
173+
174+
def runif_extra_has(test_str):
175+
"""Decorator checks to see if NIPY_EXTRA_TESTS env var contains test_str"""
176+
return skipif(test_str not in EXTRA_SET,
177+
"Skip {0} tests.".format(test_str))

nibabel/tests/test_nifti1.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,18 @@
1313

1414
import numpy as np
1515

16-
from ..externals.six import BytesIO
17-
from ..casting import type_info, have_binary128
18-
from ..tmpdirs import InTemporaryDirectory
19-
from ..spatialimages import HeaderDataError
20-
from ..eulerangles import euler2mat
21-
from ..affines import from_matvec
22-
from .. import nifti1 as nifti1
23-
from ..nifti1 import (load, Nifti1Header, Nifti1PairHeader, Nifti1Image,
24-
Nifti1Pair, Nifti1Extension, Nifti1Extensions,
25-
data_type_codes, extension_codes, slice_order_codes)
26-
16+
from nibabel import nifti1 as nifti1
17+
from nibabel.affines import from_matvec
18+
from nibabel.casting import type_info, have_binary128
19+
from nibabel.eulerangles import euler2mat
20+
from nibabel.externals.six import BytesIO
21+
from nibabel.nifti1 import (load, Nifti1Header, Nifti1PairHeader, Nifti1Image,
22+
Nifti1Pair, Nifti1Extension, Nifti1Extensions,
23+
data_type_codes, extension_codes,
24+
slice_order_codes)
25+
from nibabel.openers import ImageOpener
26+
from nibabel.spatialimages import HeaderDataError
27+
from nibabel.tmpdirs import InTemporaryDirectory
2728
from ..freesurfer import load as mghload
2829

2930
from .test_arraywriters import rt_err_estimate, IUINT_TYPES
@@ -35,7 +36,7 @@
3536
from nose.tools import (assert_true, assert_false, assert_equal,
3637
assert_raises)
3738

38-
from ..testing import data_path, suppress_warnings
39+
from ..testing import data_path, suppress_warnings, runif_extra_has
3940

4041
from . import test_analyze as tana
4142
from . import test_spm99analyze as tspm
@@ -1242,3 +1243,19 @@ def test_rt_bias(self):
12421243
# Hokey use of max_miss as a std estimate
12431244
bias_thresh = np.max([max_miss / np.sqrt(count), eps])
12441245
assert_true(np.abs(bias) < bias_thresh)
1246+
1247+
1248+
@runif_extra_has('slow')
1249+
def test_large_nifti1():
1250+
image_shape = (91, 109, 91, 1200)
1251+
img = Nifti1Image(np.ones(image_shape, dtype=np.float32),
1252+
affine=np.eye(4))
1253+
# Dump and load the large image.
1254+
with InTemporaryDirectory():
1255+
img.to_filename('test.nii.gz')
1256+
del img
1257+
data = load('test.nii.gz').get_data()
1258+
# Check that the data are all ones
1259+
assert_equal(image_shape, data.shape)
1260+
n_ones = np.sum((data == 1.))
1261+
assert_equal(np.prod(image_shape), n_ones)

0 commit comments

Comments
 (0)