Skip to content

Commit ae2fa36

Browse files
authored
Merge pull request #610 from CRiddler/loadsave-pathlib_compat
ENH: Accept pathlib.Path objects where filenames are accepted
2 parents f2c7812 + 60a4624 commit ae2fa36

File tree

7 files changed

+100
-46
lines changed

7 files changed

+100
-46
lines changed

nibabel/filebasedimages.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ def set_filename(self, filename):
246246
247247
Parameters
248248
----------
249-
filename : str
249+
filename : str or os.PathLike
250250
If the image format only has one file associated with it,
251251
this will be the only filename set into the image
252252
``.file_map`` attribute. Otherwise, the image instance will
@@ -279,7 +279,7 @@ def filespec_to_file_map(klass, filespec):
279279
280280
Parameters
281281
----------
282-
filespec : str
282+
filespec : str or os.PathLike
283283
Filename that might be for this image file type.
284284
285285
Returns
@@ -321,7 +321,7 @@ def to_filename(self, filename):
321321
322322
Parameters
323323
----------
324-
filename : str
324+
filename : str or os.PathLike
325325
filename to which to save image. We will parse `filename`
326326
with ``filespec_to_file_map`` to work out names for image,
327327
header etc.
@@ -419,7 +419,7 @@ def _sniff_meta_for(klass, filename, sniff_nbytes, sniff=None):
419419
420420
Parameters
421421
----------
422-
filename : str
422+
filename : str or os.PathLike
423423
Filename for an image, or an image header (metadata) file.
424424
If `filename` points to an image data file, and the image type has
425425
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):
466466
467467
Parameters
468468
----------
469-
filename : str
469+
filename : str or os.PathLike
470470
Filename for an image, or an image header (metadata) file.
471471
If `filename` points to an image data file, and the image type has
472472
a separate "header" file, we work out the name of the header file,

nibabel/filename_parser.py

+40-8
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,43 @@
99
''' Create filename pairs, triplets etc, with expected extensions '''
1010

1111
import os
12-
try:
13-
basestring
14-
except NameError:
15-
basestring = str
12+
import pathlib
1613

1714

1815
class TypesFilenamesError(Exception):
1916
pass
2017

2118

19+
def _stringify_path(filepath_or_buffer):
20+
"""Attempt to convert a path-like object to a string.
21+
22+
Parameters
23+
----------
24+
filepath_or_buffer : str or os.PathLike
25+
26+
Returns
27+
-------
28+
str_filepath_or_buffer : str
29+
30+
Notes
31+
-----
32+
Objects supporting the fspath protocol (python 3.6+) are coerced
33+
according to its __fspath__ method.
34+
For backwards compatibility with older pythons, pathlib.Path objects
35+
are specially coerced.
36+
Any other object is passed through unchanged, which includes bytes,
37+
strings, buffers, or anything else that's not even path-like.
38+
39+
Copied from:
40+
https://github.com/pandas-dev/pandas/blob/325dd686de1589c17731cf93b649ed5ccb5a99b4/pandas/io/common.py#L131-L160
41+
"""
42+
if hasattr(filepath_or_buffer, "__fspath__"):
43+
return filepath_or_buffer.__fspath__()
44+
elif isinstance(filepath_or_buffer, pathlib.Path):
45+
return str(filepath_or_buffer)
46+
return filepath_or_buffer
47+
48+
2249
def types_filenames(template_fname, types_exts,
2350
trailing_suffixes=('.gz', '.bz2'),
2451
enforce_extensions=True,
@@ -31,7 +58,7 @@ def types_filenames(template_fname, types_exts,
3158
3259
Parameters
3360
----------
34-
template_fname : str
61+
template_fname : str or os.PathLike
3562
template filename from which to construct output dict of
3663
filenames, with given `types_exts` type to extension mapping. If
3764
``self.enforce_extensions`` is True, then filename must have one
@@ -82,7 +109,8 @@ def types_filenames(template_fname, types_exts,
82109
>>> tfns == {'t1': '/path/test.funny', 't2': '/path/test.ext2'}
83110
True
84111
'''
85-
if not isinstance(template_fname, basestring):
112+
template_fname = _stringify_path(template_fname)
113+
if not isinstance(template_fname, str):
86114
raise TypesFilenamesError('Need file name as input '
87115
'to set_filenames')
88116
if template_fname.endswith('.'):
@@ -151,7 +179,7 @@ def parse_filename(filename,
151179
152180
Parameters
153181
----------
154-
filename : str
182+
filename : str or os.PathLike
155183
filename in which to search for type extensions
156184
types_exts : sequence of sequences
157185
sequence of (name, extension) str sequences defining type to
@@ -190,6 +218,8 @@ def parse_filename(filename,
190218
>>> parse_filename('/path/fnameext2.gz', types_exts, ('.gz',))
191219
('/path/fname', 'ext2', '.gz', 't2')
192220
'''
221+
filename = _stringify_path(filename)
222+
193223
ignored = None
194224
if match_case:
195225
endswith = _endswith
@@ -232,7 +262,7 @@ def splitext_addext(filename,
232262
233263
Parameters
234264
----------
235-
filename : str
265+
filename : str or os.PathLike
236266
filename that may end in any or none of `addexts`
237267
match_case : bool, optional
238268
If True, match case of `addexts` and `filename`, otherwise do
@@ -257,6 +287,8 @@ def splitext_addext(filename,
257287
>>> splitext_addext('fname.ext.foo', ('.foo', '.bar'))
258288
('fname', '.ext', '.foo')
259289
'''
290+
filename = _stringify_path(filename)
291+
260292
if match_case:
261293
endswith = _endswith
262294
else:

nibabel/freesurfer/mghformat.py

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from ..volumeutils import (array_to_file, array_from_file, endian_codes,
1818
Recoder)
1919
from ..filebasedimages import SerializableImage
20+
from ..filename_parser import _stringify_path
2021
from ..spatialimages import HeaderDataError, SpatialImage
2122
from ..fileholders import FileHolder
2223
from ..arrayproxy import ArrayProxy, reshape_dataobj
@@ -529,6 +530,7 @@ def __init__(self, dataobj, affine, header=None,
529530

530531
@classmethod
531532
def filespec_to_file_map(klass, filespec):
533+
filespec = _stringify_path(filespec)
532534
""" Check for compressed .mgz format, then .mgh format """
533535
if splitext(filespec)[1].lower() == '.mgz':
534536
return dict(image=FileHolder(filename=filespec))

nibabel/loadsave.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import os
1313
import numpy as np
1414

15-
from .filename_parser import splitext_addext
15+
from .filename_parser import splitext_addext, _stringify_path
1616
from .openers import ImageOpener
1717
from .filebasedimages import ImageFileError
1818
from .imageclasses import all_image_classes
@@ -25,7 +25,7 @@ def load(filename, **kwargs):
2525
2626
Parameters
2727
----------
28-
filename : string
28+
filename : str or os.PathLike
2929
specification of file to load
3030
\*\*kwargs : keyword arguments
3131
Keyword arguments to format-specific load
@@ -35,12 +35,16 @@ def load(filename, **kwargs):
3535
img : ``SpatialImage``
3636
Image of guessed type
3737
'''
38+
filename = _stringify_path(filename)
39+
40+
# Check file exists and is not empty
3841
try:
3942
stat_result = os.stat(filename)
4043
except OSError:
4144
raise FileNotFoundError("No such file or no access: '%s'" % filename)
4245
if stat_result.st_size <= 0:
4346
raise ImageFileError("Empty file: '%s'" % filename)
47+
4448
sniff = None
4549
for image_klass in all_image_classes:
4650
is_valid, sniff = image_klass.path_maybe_image(filename, sniff)
@@ -85,13 +89,14 @@ def save(img, filename):
8589
----------
8690
img : ``SpatialImage``
8791
image to save
88-
filename : str
92+
filename : str or os.PathLike
8993
filename (often implying filenames) to which to save `img`.
9094
9195
Returns
9296
-------
9397
None
9498
'''
99+
filename = _stringify_path(filename)
95100

96101
# Save the type as expected
97102
try:

nibabel/tests/test_image_api.py

+16-13
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import warnings
2727
from functools import partial
2828
from itertools import product
29+
import pathlib
2930

3031
import numpy as np
3132

@@ -141,21 +142,23 @@ def validate_filenames(self, imaker, params):
141142
assert_almost_equal(np.asanyarray(img.dataobj), np.asanyarray(rt_img.dataobj))
142143
# get_ / set_ filename
143144
fname = 'an_image' + self.standard_extension
144-
img.set_filename(fname)
145-
assert_equal(img.get_filename(), fname)
146-
assert_equal(img.file_map['image'].filename, fname)
145+
for path in (fname, pathlib.Path(fname)):
146+
img.set_filename(path)
147+
assert_equal(img.get_filename(), str(path))
148+
assert_equal(img.file_map['image'].filename, str(path))
147149
# to_ / from_ filename
148150
fname = 'another_image' + self.standard_extension
149-
with InTemporaryDirectory():
150-
# Validate that saving or loading a file doesn't use deprecated methods internally
151-
with clear_and_catch_warnings() as w:
152-
warnings.simplefilter('error', DeprecationWarning)
153-
img.to_filename(fname)
154-
rt_img = img.__class__.from_filename(fname)
155-
assert_array_equal(img.shape, rt_img.shape)
156-
assert_almost_equal(img.get_fdata(), rt_img.get_fdata())
157-
assert_almost_equal(np.asanyarray(img.dataobj), np.asanyarray(rt_img.dataobj))
158-
del rt_img # to allow windows to delete the directory
151+
for path in (fname, pathlib.Path(fname)):
152+
with InTemporaryDirectory():
153+
# Validate that saving or loading a file doesn't use deprecated methods internally
154+
with clear_and_catch_warnings() as w:
155+
warnings.simplefilter('error', DeprecationWarning)
156+
img.to_filename(path)
157+
rt_img = img.__class__.from_filename(path)
158+
assert_array_equal(img.shape, rt_img.shape)
159+
assert_almost_equal(img.get_fdata(), rt_img.get_fdata())
160+
assert_almost_equal(np.asanyarray(img.dataobj), np.asanyarray(rt_img.dataobj))
161+
del rt_img # to allow windows to delete the directory
159162

160163
def validate_no_slicing(self, imaker, params):
161164
img = imaker()

nibabel/tests/test_image_load_save.py

+9-7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import shutil
1313
from os.path import dirname, join as pjoin
1414
from tempfile import mkdtemp
15+
import pathlib
1516

1617
import numpy as np
1718

@@ -255,13 +256,14 @@ def test_filename_save():
255256
try:
256257
pth = mkdtemp()
257258
fname = pjoin(pth, 'image' + out_ext)
258-
nils.save(img, fname)
259-
rt_img = nils.load(fname)
260-
assert_array_almost_equal(rt_img.get_fdata(), data)
261-
assert_true(type(rt_img) is loadklass)
262-
# delete image to allow file close. Otherwise windows
263-
# raises an error when trying to delete the directory
264-
del rt_img
259+
for path in (fname, pathlib.Path(fname)):
260+
nils.save(img, path)
261+
rt_img = nils.load(path)
262+
assert_array_almost_equal(rt_img.get_fdata(), data)
263+
assert_true(type(rt_img) is loadklass)
264+
# delete image to allow file close. Otherwise windows
265+
# raises an error when trying to delete the directory
266+
del rt_img
265267
finally:
266268
shutil.rmtree(pth)
267269

nibabel/tests/test_loadsave.py

+20-10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from os.path import dirname, join as pjoin
55
import shutil
6+
import pathlib
67

78
import numpy as np
89

@@ -26,14 +27,20 @@
2627

2728

2829
def test_read_img_data():
29-
for fname in ('example4d.nii.gz',
30-
'example_nifti2.nii.gz',
31-
'minc1_1_scale.mnc',
32-
'minc1_4d.mnc',
33-
'test.mgz',
34-
'tiny.mnc'
35-
):
36-
fpath = pjoin(data_path, fname)
30+
fnames_test = [
31+
'example4d.nii.gz',
32+
'example_nifti2.nii.gz',
33+
'minc1_1_scale.mnc',
34+
'minc1_4d.mnc',
35+
'test.mgz',
36+
'tiny.mnc'
37+
]
38+
fnames_test += [pathlib.Path(p) for p in fnames_test]
39+
for fname in fnames_test:
40+
# os.path.join doesnt work between str / os.PathLike in py3.5
41+
fpath = pjoin(data_path, str(fname))
42+
if isinstance(fname, pathlib.Path):
43+
fpath = pathlib.Path(fpath)
3744
img = load(fpath)
3845
data = img.get_fdata()
3946
data2 = read_img_data(img)
@@ -45,8 +52,11 @@ def test_read_img_data():
4552
assert_array_equal(read_img_data(img, prefer='unscaled'), data)
4653
# Assert all caps filename works as well
4754
with TemporaryDirectory() as tmpdir:
48-
up_fpath = pjoin(tmpdir, fname.upper())
49-
shutil.copyfile(fpath, up_fpath)
55+
up_fpath = pjoin(tmpdir, str(fname).upper())
56+
if isinstance(fname, pathlib.Path):
57+
up_fpath = pathlib.Path(up_fpath)
58+
# shutil doesnt work with os.PathLike in py3.5
59+
shutil.copyfile(str(fpath), str(up_fpath))
5060
img = load(up_fpath)
5161
assert_array_equal(img.dataobj, data)
5262
del img

0 commit comments

Comments
 (0)