Skip to content

Commit

Permalink
Allow file names to be given as pathlib Paths
Browse files Browse the repository at this point in the history
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
  • Loading branch information
colinpalmer committed Jul 14, 2022
1 parent 28290ea commit c9e036e
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 18 deletions.
19 changes: 11 additions & 8 deletions mrcfile/load_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def new(name, data=None, compression=None, overwrite=False):
"""Create a new MRC file.
Args:
name: The file name to use.
name: The file name to use, as a string or :class:`~pathlib.Path`.
data: Data to put in the file, as a :class:`numpy array
<numpy.ndarray>`. The default is :data:`None`, to create an empty
file.
Expand Down Expand Up @@ -87,7 +87,7 @@ def open(name, mode='r', permissive=False, header_only=False): # @ReservedAssig
:doc:`usage guide <../usage_guide>` for more information.
Args:
name: The file name to open.
name: The file name to open, as a string or :class:`~pathlib.Path`.
mode: The file mode to use. This should be one of the following: ``r``
for read-only, ``r+`` for read and write, or ``w+`` for a new empty
file. The default is ``r``.
Expand Down Expand Up @@ -121,6 +121,7 @@ def open(name, mode='r', permissive=False, header_only=False): # @ReservedAssig
number of bytes in the corresponding dtype.
"""
NewMrc = MrcFile
name = str(name) # in case name is a pathlib Path
if os.path.exists(name):
with io.open(name, 'rb') as f:
start = f.read(MAP_ID_OFFSET_BYTES + len(MAP_ID))
Expand Down Expand Up @@ -148,7 +149,7 @@ def read(name):
:func:`mrcfile.open` instead.
Args:
name: The file name to read.
name: The file name to read, as a string or :class:`~pathlib.Path`.
Returns:
A :class:`numpy array<numpy.ndarray>` containing the data from the file.
Expand All @@ -168,8 +169,9 @@ def write(name, data=None, overwrite=False, voxel_size=None):
representing the new file, use :func:`mrcfile.new` instead.
Args:
name: The file name to use. If the name ends with ``.gz`` or ``.bz2``, the file
will be compressed using gzip or bzip2 respectively.
name: The file name to use, as a string or :class:`~pathlib.Path`. If the name
ends with ``.gz`` or ``.bz2``, the file will be compressed using gzip or
bzip2 respectively.
data: Data to put in the file, as a :class:`numpy array
<numpy.ndarray>`. The default is :data:`None`, to create an empty
file.
Expand All @@ -186,6 +188,7 @@ def write(name, data=None, overwrite=False, voxel_size=None):
Warns:
RuntimeWarning: If the data array contains Inf or NaN values.
"""
name = str(name) # in case name is a pathlib Path
compression = None
if name.endswith('.gz'):
compression = 'gzip'
Expand Down Expand Up @@ -226,7 +229,7 @@ def open_async(name, mode='r', permissive=False):
:meth:`~mrcfile.future_mrcfile.FutureMrcFile.done`.
Args:
name: The file name to open.
name: The file name to open, as a string or :class:`~pathlib.Path`.
mode: The file mode (one of ``r``, ``r+`` or ``w+``).
permissive: Read the file in permissive mode. The default is
:data:`False`.
Expand Down Expand Up @@ -254,7 +257,7 @@ def mmap(name, mode='r', permissive=False):
:class:`~mrcfile.mrcfile.MrcFile` object.
Args:
name: The file name to open.
name: The file name to open, as a string or :class:`~pathlib.Path`.
mode: The file mode (one of ``r``, ``r+`` or ``w+``).
permissive: Read the file in permissive mode. The default is
:data:`False`.
Expand All @@ -281,7 +284,7 @@ def new_mmap(name, shape, mrc_mode=0, fill=None, overwrite=False, extended_heade
with a reasonable default value).
Args:
name: The file name to use.
name: The file name to use, as a string or :class:`~pathlib.Path`.
shape: The shape of the data array to open, as a 2-, 3- or 4-tuple of
ints. For example, ``(nz, ny, nx)`` for a new 3D volume, or
``(ny, nx)`` for a new 2D image.
Expand Down
5 changes: 3 additions & 2 deletions mrcfile/mrcfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def __init__(self, name, mode='r', overwrite=False, permissive=False,
extended header and data arrays.
Args:
name: The file name to open.
name: The file name to open, as a string or pathlib Path.
mode: The file mode to use. This should be one of the following:
``r`` for read-only, ``r+`` for read and write, or ``w+`` for a
new empty file. The default is ``r``.
Expand Down Expand Up @@ -97,7 +97,8 @@ def __init__(self, name, mode='r', overwrite=False, permissive=False,

if mode not in ['r', 'r+', 'w+']:
raise ValueError("Mode '{0}' not supported".format(mode))


name = str(name) # in case name is a pathlib Path
if ('w' in mode and os.path.exists(name) and not overwrite):
raise ValueError("File '{0}' already exists; set overwrite=True "
"to overwrite it".format(name))
Expand Down
50 changes: 42 additions & 8 deletions tests/test_load_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@
from mrcfile.gzipmrcfile import GzipMrcFile
from . import helpers

# Try to import pathlib if we can
pathlib_unavailable = False
try:
from pathlib import Path
except ImportError:
try:
from pathlib2 import Path
except ImportError:
pathlib_unavailable = True


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

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

def test_read_function(self):
@unittest.skipIf(pathlib_unavailable, "pathlib not available")
def test_normal_opening_pathlib(self):
"""Single test to ensure pathlib functionality is tested even if there's
a problem with the LoadFunctionTestWithPathlib class"""
path = Path(self.example_mrc_name)
with mrcfile.open(path) as mrc:
assert repr(mrc) == ("MrcFile('{0}', mode='r')"
.format(self.example_mrc_name))

def test_read(self):
volume = mrcfile.read(self.example_mrc_name)
assert isinstance(volume, np.ndarray)
assert volume.shape, volume.dtype == ((20, 20, 20), np.float32)
Expand All @@ -75,7 +95,7 @@ def test_new_empty_file(self):
with mrcfile.new(self.temp_mrc_name) as mrc:
assert repr(mrc) == ("MrcFile('{0}', mode='w+')"
.format(self.temp_mrc_name))

def test_new_empty_file_with_open_function(self):
with mrcfile.open(self.temp_mrc_name, mode='w+') as mrc:
assert repr(mrc) == ("MrcFile('{0}', mode='w+')"
Expand Down Expand Up @@ -115,9 +135,9 @@ def test_unknown_compression_type(self):
mrcfile.new(self.temp_mrc_name, compression='other')

def test_overwriting_flag(self):
assert not os.path.exists(self.temp_mrc_name)
open(self.temp_mrc_name, 'w+').close()
assert os.path.exists(self.temp_mrc_name)
assert not os.path.exists(str(self.temp_mrc_name))
open(str(self.temp_mrc_name), 'w+').close()
assert os.path.exists(str(self.temp_mrc_name))
with self.assertRaisesRegex(ValueError, "already exists"):
mrcfile.new(self.temp_mrc_name)
with self.assertRaisesRegex(ValueError, "already exists"):
Expand Down Expand Up @@ -217,11 +237,25 @@ def test_write(self):

def test_write_with_auto_compression(self):
data_in = np.random.random((10, 10)).astype(np.float16)
filename = self.temp_mrc_name + '.gz'
mrcfile.write(filename, data_in)
with mrcfile.open(filename) as mrc:
mrcfile.write(self.temp_gz_mrc_name, data_in)
with mrcfile.open(self.temp_gz_mrc_name) as mrc:
assert isinstance(mrc, GzipMrcFile)


@unittest.skipIf(pathlib_unavailable, "pathlib not available")
class LoadFunctionTestWithPathlib(LoadFunctionTest):

"""Class to run the load function tests using pathlib paths instead of strings."""

def setUp(self):
super(LoadFunctionTestWithPathlib, self).setUp()
self.temp_mrc_name = Path(self.temp_mrc_name)
self.temp_gz_mrc_name = Path(self.temp_gz_mrc_name)
self.example_mrc_name = Path(self.example_mrc_name)
self.gzip_mrc_name = Path(self.gzip_mrc_name)
self.bzip2_mrc_name = Path(self.bzip2_mrc_name)
self.slow_mrc_name = Path(self.slow_mrc_name)


if __name__ == '__main__':
unittest.main()
16 changes: 16 additions & 0 deletions tests/test_mrcfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@
VOLUME_STACK_SPACEGROUP)
import mrcfile.utils as utils

# Try to import pathlib if we can
pathlib_unavailable = False
try:
from pathlib import Path
except ImportError:
try:
from pathlib2 import Path
except ImportError:
pathlib_unavailable = True


# Doctest stuff commented out for now - would be nice to get it working!
# import doctest
Expand Down Expand Up @@ -226,6 +236,12 @@ def test_stream_can_be_read_again(self):
orig_data = mrc.data.copy()
mrc._read()
np.testing.assert_array_equal(orig_data, mrc.data)

@unittest.skipIf(pathlib_unavailable, "pathlib not available")
def test_opening_with_pathlib(self):
path = Path(self.example_mrc_name)
with self.newmrc(path) as mrc:
assert self.example_mrc_name in repr(mrc)

############################################################################
#
Expand Down
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ envlist =
# matrix of test environments
[testenv]
deps =
py27: pathlib2
numpy1.16: numpy >= 1.16.0, < 1.17.0
numpy1.17: numpy >= 1.17.0, < 1.18.0
numpy1.18: numpy >= 1.18.0, < 1.19.0
Expand Down

0 comments on commit c9e036e

Please sign in to comment.