From c9e036ea1eb9950ed54ad6a60b22327c70b05674 Mon Sep 17 00:00:00 2001 From: Colin Palmer Date: Thu, 14 Jul 2022 13:33:23 +0100 Subject: [PATCH] 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. https://github.com/ccpem/mrcfile/issues/42 --- mrcfile/load_functions.py | 19 ++++++++------ mrcfile/mrcfile.py | 5 ++-- tests/test_load_functions.py | 50 ++++++++++++++++++++++++++++++------ tests/test_mrcfile.py | 16 ++++++++++++ tox.ini | 1 + 5 files changed, 73 insertions(+), 18 deletions(-) diff --git a/mrcfile/load_functions.py b/mrcfile/load_functions.py index f597c22..42c957e 100644 --- a/mrcfile/load_functions.py +++ b/mrcfile/load_functions.py @@ -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 `. The default is :data:`None`, to create an empty file. @@ -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``. @@ -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)) @@ -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` containing the data from the file. @@ -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 `. The default is :data:`None`, to create an empty file. @@ -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' @@ -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`. @@ -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`. @@ -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. diff --git a/mrcfile/mrcfile.py b/mrcfile/mrcfile.py index b60c525..4ecdaed 100644 --- a/mrcfile/mrcfile.py +++ b/mrcfile/mrcfile.py @@ -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``. @@ -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)) diff --git a/tests/test_load_functions.py b/tests/test_load_functions.py index a478fe9..581fdea 100644 --- a/tests/test_load_functions.py +++ b/tests/test_load_functions.py @@ -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): @@ -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') @@ -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) @@ -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+')" @@ -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"): @@ -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() diff --git a/tests/test_mrcfile.py b/tests/test_mrcfile.py index 8550d21..660cc1e 100644 --- a/tests/test_mrcfile.py +++ b/tests/test_mrcfile.py @@ -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 @@ -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) ############################################################################ # diff --git a/tox.ini b/tox.ini index f4dabc3..d0025cd 100644 --- a/tox.ini +++ b/tox.ini @@ -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