Skip to content

Commit c9e036e

Browse files
committed
Allow file names to be given as pathlib Paths
This should work automatically on any supported version of Python 3. For Python 2.7, if pathlib2 is not installed, the tests that check the pathlib functionality will be skipped. #42
1 parent 28290ea commit c9e036e

File tree

5 files changed

+73
-18
lines changed

5 files changed

+73
-18
lines changed

mrcfile/load_functions.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def new(name, data=None, compression=None, overwrite=False):
3030
"""Create a new MRC file.
3131
3232
Args:
33-
name: The file name to use.
33+
name: The file name to use, as a string or :class:`~pathlib.Path`.
3434
data: Data to put in the file, as a :class:`numpy array
3535
<numpy.ndarray>`. The default is :data:`None`, to create an empty
3636
file.
@@ -87,7 +87,7 @@ def open(name, mode='r', permissive=False, header_only=False): # @ReservedAssig
8787
:doc:`usage guide <../usage_guide>` for more information.
8888
8989
Args:
90-
name: The file name to open.
90+
name: The file name to open, as a string or :class:`~pathlib.Path`.
9191
mode: The file mode to use. This should be one of the following: ``r``
9292
for read-only, ``r+`` for read and write, or ``w+`` for a new empty
9393
file. The default is ``r``.
@@ -121,6 +121,7 @@ def open(name, mode='r', permissive=False, header_only=False): # @ReservedAssig
121121
number of bytes in the corresponding dtype.
122122
"""
123123
NewMrc = MrcFile
124+
name = str(name) # in case name is a pathlib Path
124125
if os.path.exists(name):
125126
with io.open(name, 'rb') as f:
126127
start = f.read(MAP_ID_OFFSET_BYTES + len(MAP_ID))
@@ -148,7 +149,7 @@ def read(name):
148149
:func:`mrcfile.open` instead.
149150
150151
Args:
151-
name: The file name to read.
152+
name: The file name to read, as a string or :class:`~pathlib.Path`.
152153
153154
Returns:
154155
A :class:`numpy array<numpy.ndarray>` containing the data from the file.
@@ -168,8 +169,9 @@ def write(name, data=None, overwrite=False, voxel_size=None):
168169
representing the new file, use :func:`mrcfile.new` instead.
169170
170171
Args:
171-
name: The file name to use. If the name ends with ``.gz`` or ``.bz2``, the file
172-
will be compressed using gzip or bzip2 respectively.
172+
name: The file name to use, as a string or :class:`~pathlib.Path`. If the name
173+
ends with ``.gz`` or ``.bz2``, the file will be compressed using gzip or
174+
bzip2 respectively.
173175
data: Data to put in the file, as a :class:`numpy array
174176
<numpy.ndarray>`. The default is :data:`None`, to create an empty
175177
file.
@@ -186,6 +188,7 @@ def write(name, data=None, overwrite=False, voxel_size=None):
186188
Warns:
187189
RuntimeWarning: If the data array contains Inf or NaN values.
188190
"""
191+
name = str(name) # in case name is a pathlib Path
189192
compression = None
190193
if name.endswith('.gz'):
191194
compression = 'gzip'
@@ -226,7 +229,7 @@ def open_async(name, mode='r', permissive=False):
226229
:meth:`~mrcfile.future_mrcfile.FutureMrcFile.done`.
227230
228231
Args:
229-
name: The file name to open.
232+
name: The file name to open, as a string or :class:`~pathlib.Path`.
230233
mode: The file mode (one of ``r``, ``r+`` or ``w+``).
231234
permissive: Read the file in permissive mode. The default is
232235
:data:`False`.
@@ -254,7 +257,7 @@ def mmap(name, mode='r', permissive=False):
254257
:class:`~mrcfile.mrcfile.MrcFile` object.
255258
256259
Args:
257-
name: The file name to open.
260+
name: The file name to open, as a string or :class:`~pathlib.Path`.
258261
mode: The file mode (one of ``r``, ``r+`` or ``w+``).
259262
permissive: Read the file in permissive mode. The default is
260263
:data:`False`.
@@ -281,7 +284,7 @@ def new_mmap(name, shape, mrc_mode=0, fill=None, overwrite=False, extended_heade
281284
with a reasonable default value).
282285
283286
Args:
284-
name: The file name to use.
287+
name: The file name to use, as a string or :class:`~pathlib.Path`.
285288
shape: The shape of the data array to open, as a 2-, 3- or 4-tuple of
286289
ints. For example, ``(nz, ny, nx)`` for a new 3D volume, or
287290
``(ny, nx)`` for a new 2D image.

mrcfile/mrcfile.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def __init__(self, name, mode='r', overwrite=False, permissive=False,
5959
extended header and data arrays.
6060
6161
Args:
62-
name: The file name to open.
62+
name: The file name to open, as a string or pathlib Path.
6363
mode: The file mode to use. This should be one of the following:
6464
``r`` for read-only, ``r+`` for read and write, or ``w+`` for a
6565
new empty file. The default is ``r``.
@@ -97,7 +97,8 @@ def __init__(self, name, mode='r', overwrite=False, permissive=False,
9797

9898
if mode not in ['r', 'r+', 'w+']:
9999
raise ValueError("Mode '{0}' not supported".format(mode))
100-
100+
101+
name = str(name) # in case name is a pathlib Path
101102
if ('w' in mode and os.path.exists(name) and not overwrite):
102103
raise ValueError("File '{0}' already exists; set overwrite=True "
103104
"to overwrite it".format(name))

tests/test_load_functions.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@
2121
from mrcfile.gzipmrcfile import GzipMrcFile
2222
from . import helpers
2323

24+
# Try to import pathlib if we can
25+
pathlib_unavailable = False
26+
try:
27+
from pathlib import Path
28+
except ImportError:
29+
try:
30+
from pathlib2 import Path
31+
except ImportError:
32+
pathlib_unavailable = True
33+
2434

2535
class LoadFunctionTest(helpers.AssertRaisesRegexMixin, unittest.TestCase):
2636

@@ -35,6 +45,7 @@ def setUp(self):
3545
self.test_data = helpers.get_test_data_path()
3646
self.test_output = tempfile.mkdtemp()
3747
self.temp_mrc_name = os.path.join(self.test_output, 'test_mrcfile.mrc')
48+
self.temp_gz_mrc_name = self.temp_mrc_name + '.gz'
3849
self.example_mrc_name = os.path.join(self.test_data, 'EMD-3197.map')
3950
self.gzip_mrc_name = os.path.join(self.test_data, 'emd_3197.map.gz')
4051
self.bzip2_mrc_name = os.path.join(self.test_data, 'EMD-3197.map.bz2')
@@ -50,7 +61,16 @@ def test_normal_opening(self):
5061
assert repr(mrc) == ("MrcFile('{0}', mode='r')"
5162
.format(self.example_mrc_name))
5263

53-
def test_read_function(self):
64+
@unittest.skipIf(pathlib_unavailable, "pathlib not available")
65+
def test_normal_opening_pathlib(self):
66+
"""Single test to ensure pathlib functionality is tested even if there's
67+
a problem with the LoadFunctionTestWithPathlib class"""
68+
path = Path(self.example_mrc_name)
69+
with mrcfile.open(path) as mrc:
70+
assert repr(mrc) == ("MrcFile('{0}', mode='r')"
71+
.format(self.example_mrc_name))
72+
73+
def test_read(self):
5474
volume = mrcfile.read(self.example_mrc_name)
5575
assert isinstance(volume, np.ndarray)
5676
assert volume.shape, volume.dtype == ((20, 20, 20), np.float32)
@@ -75,7 +95,7 @@ def test_new_empty_file(self):
7595
with mrcfile.new(self.temp_mrc_name) as mrc:
7696
assert repr(mrc) == ("MrcFile('{0}', mode='w+')"
7797
.format(self.temp_mrc_name))
78-
98+
7999
def test_new_empty_file_with_open_function(self):
80100
with mrcfile.open(self.temp_mrc_name, mode='w+') as mrc:
81101
assert repr(mrc) == ("MrcFile('{0}', mode='w+')"
@@ -115,9 +135,9 @@ def test_unknown_compression_type(self):
115135
mrcfile.new(self.temp_mrc_name, compression='other')
116136

117137
def test_overwriting_flag(self):
118-
assert not os.path.exists(self.temp_mrc_name)
119-
open(self.temp_mrc_name, 'w+').close()
120-
assert os.path.exists(self.temp_mrc_name)
138+
assert not os.path.exists(str(self.temp_mrc_name))
139+
open(str(self.temp_mrc_name), 'w+').close()
140+
assert os.path.exists(str(self.temp_mrc_name))
121141
with self.assertRaisesRegex(ValueError, "already exists"):
122142
mrcfile.new(self.temp_mrc_name)
123143
with self.assertRaisesRegex(ValueError, "already exists"):
@@ -217,11 +237,25 @@ def test_write(self):
217237

218238
def test_write_with_auto_compression(self):
219239
data_in = np.random.random((10, 10)).astype(np.float16)
220-
filename = self.temp_mrc_name + '.gz'
221-
mrcfile.write(filename, data_in)
222-
with mrcfile.open(filename) as mrc:
240+
mrcfile.write(self.temp_gz_mrc_name, data_in)
241+
with mrcfile.open(self.temp_gz_mrc_name) as mrc:
223242
assert isinstance(mrc, GzipMrcFile)
224243

225244

245+
@unittest.skipIf(pathlib_unavailable, "pathlib not available")
246+
class LoadFunctionTestWithPathlib(LoadFunctionTest):
247+
248+
"""Class to run the load function tests using pathlib paths instead of strings."""
249+
250+
def setUp(self):
251+
super(LoadFunctionTestWithPathlib, self).setUp()
252+
self.temp_mrc_name = Path(self.temp_mrc_name)
253+
self.temp_gz_mrc_name = Path(self.temp_gz_mrc_name)
254+
self.example_mrc_name = Path(self.example_mrc_name)
255+
self.gzip_mrc_name = Path(self.gzip_mrc_name)
256+
self.bzip2_mrc_name = Path(self.bzip2_mrc_name)
257+
self.slow_mrc_name = Path(self.slow_mrc_name)
258+
259+
226260
if __name__ == '__main__':
227261
unittest.main()

tests/test_mrcfile.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@
2525
VOLUME_STACK_SPACEGROUP)
2626
import mrcfile.utils as utils
2727

28+
# Try to import pathlib if we can
29+
pathlib_unavailable = False
30+
try:
31+
from pathlib import Path
32+
except ImportError:
33+
try:
34+
from pathlib2 import Path
35+
except ImportError:
36+
pathlib_unavailable = True
37+
2838

2939
# Doctest stuff commented out for now - would be nice to get it working!
3040
# import doctest
@@ -226,6 +236,12 @@ def test_stream_can_be_read_again(self):
226236
orig_data = mrc.data.copy()
227237
mrc._read()
228238
np.testing.assert_array_equal(orig_data, mrc.data)
239+
240+
@unittest.skipIf(pathlib_unavailable, "pathlib not available")
241+
def test_opening_with_pathlib(self):
242+
path = Path(self.example_mrc_name)
243+
with self.newmrc(path) as mrc:
244+
assert self.example_mrc_name in repr(mrc)
229245

230246
############################################################################
231247
#

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ envlist =
1313
# matrix of test environments
1414
[testenv]
1515
deps =
16+
py27: pathlib2
1617
numpy1.16: numpy >= 1.16.0, < 1.17.0
1718
numpy1.17: numpy >= 1.17.0, < 1.18.0
1819
numpy1.18: numpy >= 1.18.0, < 1.19.0

0 commit comments

Comments
 (0)