diff --git a/nibabel/filebasedimages.py b/nibabel/filebasedimages.py index 86ce837942..90bbd8e652 100644 --- a/nibabel/filebasedimages.py +++ b/nibabel/filebasedimages.py @@ -246,7 +246,7 @@ def set_filename(self, filename): Parameters ---------- - filename : str + filename : str or os.PathLike If the image format only has one file associated with it, this will be the only filename set into the image ``.file_map`` attribute. Otherwise, the image instance will @@ -279,7 +279,7 @@ def filespec_to_file_map(klass, filespec): Parameters ---------- - filespec : str + filespec : str or os.PathLike Filename that might be for this image file type. Returns @@ -321,7 +321,7 @@ def to_filename(self, filename): Parameters ---------- - filename : str + filename : str or os.PathLike filename to which to save image. We will parse `filename` with ``filespec_to_file_map`` to work out names for image, header etc. @@ -419,7 +419,7 @@ def _sniff_meta_for(klass, filename, sniff_nbytes, sniff=None): Parameters ---------- - filename : str + filename : str or os.PathLike Filename for an image, or an image header (metadata) file. If `filename` points to an image data file, and the image type has a separate "header" file, we work out the name of the header file, @@ -466,7 +466,7 @@ def path_maybe_image(klass, filename, sniff=None, sniff_max=1024): Parameters ---------- - filename : str + filename : str or os.PathLike Filename for an image, or an image header (metadata) file. If `filename` points to an image data file, and the image type has a separate "header" file, we work out the name of the header file, diff --git a/nibabel/filename_parser.py b/nibabel/filename_parser.py index db6e073018..ed04610fdd 100644 --- a/nibabel/filename_parser.py +++ b/nibabel/filename_parser.py @@ -9,16 +9,43 @@ ''' Create filename pairs, triplets etc, with expected extensions ''' import os -try: - basestring -except NameError: - basestring = str +import pathlib class TypesFilenamesError(Exception): pass +def _stringify_path(filepath_or_buffer): + """Attempt to convert a path-like object to a string. + + Parameters + ---------- + filepath_or_buffer : str or os.PathLike + + Returns + ------- + str_filepath_or_buffer : str + + Notes + ----- + Objects supporting the fspath protocol (python 3.6+) are coerced + according to its __fspath__ method. + For backwards compatibility with older pythons, pathlib.Path objects + are specially coerced. + Any other object is passed through unchanged, which includes bytes, + strings, buffers, or anything else that's not even path-like. + + Copied from: + https://github.com/pandas-dev/pandas/blob/325dd686de1589c17731cf93b649ed5ccb5a99b4/pandas/io/common.py#L131-L160 + """ + if hasattr(filepath_or_buffer, "__fspath__"): + return filepath_or_buffer.__fspath__() + elif isinstance(filepath_or_buffer, pathlib.Path): + return str(filepath_or_buffer) + return filepath_or_buffer + + def types_filenames(template_fname, types_exts, trailing_suffixes=('.gz', '.bz2'), enforce_extensions=True, @@ -31,7 +58,7 @@ def types_filenames(template_fname, types_exts, Parameters ---------- - template_fname : str + template_fname : str or os.PathLike template filename from which to construct output dict of filenames, with given `types_exts` type to extension mapping. If ``self.enforce_extensions`` is True, then filename must have one @@ -82,7 +109,8 @@ def types_filenames(template_fname, types_exts, >>> tfns == {'t1': '/path/test.funny', 't2': '/path/test.ext2'} True ''' - if not isinstance(template_fname, basestring): + template_fname = _stringify_path(template_fname) + if not isinstance(template_fname, str): raise TypesFilenamesError('Need file name as input ' 'to set_filenames') if template_fname.endswith('.'): @@ -151,7 +179,7 @@ def parse_filename(filename, Parameters ---------- - filename : str + filename : str or os.PathLike filename in which to search for type extensions types_exts : sequence of sequences sequence of (name, extension) str sequences defining type to @@ -190,6 +218,8 @@ def parse_filename(filename, >>> parse_filename('/path/fnameext2.gz', types_exts, ('.gz',)) ('/path/fname', 'ext2', '.gz', 't2') ''' + filename = _stringify_path(filename) + ignored = None if match_case: endswith = _endswith @@ -232,7 +262,7 @@ def splitext_addext(filename, Parameters ---------- - filename : str + filename : str or os.PathLike filename that may end in any or none of `addexts` match_case : bool, optional If True, match case of `addexts` and `filename`, otherwise do @@ -257,6 +287,8 @@ def splitext_addext(filename, >>> splitext_addext('fname.ext.foo', ('.foo', '.bar')) ('fname', '.ext', '.foo') ''' + filename = _stringify_path(filename) + if match_case: endswith = _endswith else: diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index ddb30cb796..6eb0f156e9 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -17,6 +17,7 @@ from ..volumeutils import (array_to_file, array_from_file, endian_codes, Recoder) from ..filebasedimages import SerializableImage +from ..filename_parser import _stringify_path from ..spatialimages import HeaderDataError, SpatialImage from ..fileholders import FileHolder from ..arrayproxy import ArrayProxy, reshape_dataobj @@ -529,6 +530,7 @@ def __init__(self, dataobj, affine, header=None, @classmethod def filespec_to_file_map(klass, filespec): + filespec = _stringify_path(filespec) """ Check for compressed .mgz format, then .mgh format """ if splitext(filespec)[1].lower() == '.mgz': return dict(image=FileHolder(filename=filespec)) diff --git a/nibabel/loadsave.py b/nibabel/loadsave.py index d603dd619c..f8c3e3be0b 100644 --- a/nibabel/loadsave.py +++ b/nibabel/loadsave.py @@ -12,7 +12,7 @@ import os import numpy as np -from .filename_parser import splitext_addext +from .filename_parser import splitext_addext, _stringify_path from .openers import ImageOpener from .filebasedimages import ImageFileError from .imageclasses import all_image_classes @@ -25,7 +25,7 @@ def load(filename, **kwargs): Parameters ---------- - filename : string + filename : str or os.PathLike specification of file to load \*\*kwargs : keyword arguments Keyword arguments to format-specific load @@ -35,12 +35,16 @@ def load(filename, **kwargs): img : ``SpatialImage`` Image of guessed type ''' + filename = _stringify_path(filename) + + # Check file exists and is not empty try: stat_result = os.stat(filename) except OSError: raise FileNotFoundError("No such file or no access: '%s'" % filename) if stat_result.st_size <= 0: raise ImageFileError("Empty file: '%s'" % filename) + sniff = None for image_klass in all_image_classes: is_valid, sniff = image_klass.path_maybe_image(filename, sniff) @@ -85,13 +89,14 @@ def save(img, filename): ---------- img : ``SpatialImage`` image to save - filename : str + filename : str or os.PathLike filename (often implying filenames) to which to save `img`. Returns ------- None ''' + filename = _stringify_path(filename) # Save the type as expected try: diff --git a/nibabel/tests/test_image_api.py b/nibabel/tests/test_image_api.py index 748f9c2472..3b921b9fb9 100644 --- a/nibabel/tests/test_image_api.py +++ b/nibabel/tests/test_image_api.py @@ -26,6 +26,7 @@ import warnings from functools import partial from itertools import product +import pathlib import numpy as np @@ -141,21 +142,23 @@ def validate_filenames(self, imaker, params): assert_almost_equal(np.asanyarray(img.dataobj), np.asanyarray(rt_img.dataobj)) # get_ / set_ filename fname = 'an_image' + self.standard_extension - img.set_filename(fname) - assert_equal(img.get_filename(), fname) - assert_equal(img.file_map['image'].filename, fname) + for path in (fname, pathlib.Path(fname)): + img.set_filename(path) + assert_equal(img.get_filename(), str(path)) + assert_equal(img.file_map['image'].filename, str(path)) # to_ / from_ filename fname = 'another_image' + self.standard_extension - with InTemporaryDirectory(): - # Validate that saving or loading a file doesn't use deprecated methods internally - with clear_and_catch_warnings() as w: - warnings.simplefilter('error', DeprecationWarning) - img.to_filename(fname) - rt_img = img.__class__.from_filename(fname) - assert_array_equal(img.shape, rt_img.shape) - assert_almost_equal(img.get_fdata(), rt_img.get_fdata()) - assert_almost_equal(np.asanyarray(img.dataobj), np.asanyarray(rt_img.dataobj)) - del rt_img # to allow windows to delete the directory + for path in (fname, pathlib.Path(fname)): + with InTemporaryDirectory(): + # Validate that saving or loading a file doesn't use deprecated methods internally + with clear_and_catch_warnings() as w: + warnings.simplefilter('error', DeprecationWarning) + img.to_filename(path) + rt_img = img.__class__.from_filename(path) + assert_array_equal(img.shape, rt_img.shape) + assert_almost_equal(img.get_fdata(), rt_img.get_fdata()) + assert_almost_equal(np.asanyarray(img.dataobj), np.asanyarray(rt_img.dataobj)) + del rt_img # to allow windows to delete the directory def validate_no_slicing(self, imaker, params): img = imaker() diff --git a/nibabel/tests/test_image_load_save.py b/nibabel/tests/test_image_load_save.py index 6031d4e851..9d58a3ed60 100644 --- a/nibabel/tests/test_image_load_save.py +++ b/nibabel/tests/test_image_load_save.py @@ -12,6 +12,7 @@ import shutil from os.path import dirname, join as pjoin from tempfile import mkdtemp +import pathlib import numpy as np @@ -255,13 +256,14 @@ def test_filename_save(): try: pth = mkdtemp() fname = pjoin(pth, 'image' + out_ext) - nils.save(img, fname) - rt_img = nils.load(fname) - assert_array_almost_equal(rt_img.get_fdata(), data) - assert_true(type(rt_img) is loadklass) - # delete image to allow file close. Otherwise windows - # raises an error when trying to delete the directory - del rt_img + for path in (fname, pathlib.Path(fname)): + nils.save(img, path) + rt_img = nils.load(path) + assert_array_almost_equal(rt_img.get_fdata(), data) + assert_true(type(rt_img) is loadklass) + # delete image to allow file close. Otherwise windows + # raises an error when trying to delete the directory + del rt_img finally: shutil.rmtree(pth) diff --git a/nibabel/tests/test_loadsave.py b/nibabel/tests/test_loadsave.py index 491cb07b76..3d7101b6d3 100644 --- a/nibabel/tests/test_loadsave.py +++ b/nibabel/tests/test_loadsave.py @@ -3,6 +3,7 @@ from os.path import dirname, join as pjoin import shutil +import pathlib import numpy as np @@ -26,14 +27,20 @@ def test_read_img_data(): - for fname in ('example4d.nii.gz', - 'example_nifti2.nii.gz', - 'minc1_1_scale.mnc', - 'minc1_4d.mnc', - 'test.mgz', - 'tiny.mnc' - ): - fpath = pjoin(data_path, fname) + fnames_test = [ + 'example4d.nii.gz', + 'example_nifti2.nii.gz', + 'minc1_1_scale.mnc', + 'minc1_4d.mnc', + 'test.mgz', + 'tiny.mnc' + ] + fnames_test += [pathlib.Path(p) for p in fnames_test] + for fname in fnames_test: + # os.path.join doesnt work between str / os.PathLike in py3.5 + fpath = pjoin(data_path, str(fname)) + if isinstance(fname, pathlib.Path): + fpath = pathlib.Path(fpath) img = load(fpath) data = img.get_fdata() data2 = read_img_data(img) @@ -45,8 +52,11 @@ def test_read_img_data(): assert_array_equal(read_img_data(img, prefer='unscaled'), data) # Assert all caps filename works as well with TemporaryDirectory() as tmpdir: - up_fpath = pjoin(tmpdir, fname.upper()) - shutil.copyfile(fpath, up_fpath) + up_fpath = pjoin(tmpdir, str(fname).upper()) + if isinstance(fname, pathlib.Path): + up_fpath = pathlib.Path(up_fpath) + # shutil doesnt work with os.PathLike in py3.5 + shutil.copyfile(str(fpath), str(up_fpath)) img = load(up_fpath) assert_array_equal(img.dataobj, data) del img