Skip to content

Commit 489362c

Browse files
committed
ENH: Add support for third-party path-like objects by backporting os.fspath
1 parent 28679be commit 489362c

File tree

3 files changed

+94
-42
lines changed

3 files changed

+94
-42
lines changed

numpy/compat/py3k.py

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
'unicode', 'asunicode', 'asbytes_nested', 'asunicode_nested',
99
'asstr', 'open_latin1', 'long', 'basestring', 'sixu',
1010
'integer_types', 'is_pathlib_path', 'npy_load_module', 'Path',
11-
'contextlib_nullcontext']
11+
'contextlib_nullcontext', 'os_fspath', 'os_PathLike']
1212

1313
import sys
1414
try:
15-
from pathlib import Path
15+
from pathlib import Path, PurePath
1616
except ImportError:
17-
Path = None
17+
Path = PurePath = None
1818

1919
if sys.version_info[0] >= 3:
2020
import io
@@ -95,6 +95,8 @@ def asunicode_nested(x):
9595
def is_pathlib_path(obj):
9696
"""
9797
Check whether obj is a pathlib.Path object.
98+
99+
Prefer using `isinstance(obj, os_PathLike)` instead of this function.
98100
"""
99101
return Path is not None and isinstance(obj, Path)
100102

@@ -177,3 +179,65 @@ def npy_load_module(name, fn, info=None):
177179
finally:
178180
fo.close()
179181
return mod
182+
183+
# backport abc.ABC
184+
import abc
185+
if sys.version_info[:2] >= (3, 4):
186+
abc_ABC = abc.ABC
187+
else:
188+
abc_ABC = abc.ABCMeta('ABC', (object,), {'__slots__': ()})
189+
190+
191+
# Backport os.fs_path, os.PathLike, and PurePath.__fspath__
192+
if sys.version_info[:2] >= (3, 6):
193+
import os
194+
os_fspath = os.fspath
195+
os_PathLike = os.PathLike
196+
else:
197+
def _PurePath__fspath__(self):
198+
return str(self)
199+
200+
class os_PathLike(abc_ABC):
201+
"""Abstract base class for implementing the file system path protocol."""
202+
203+
@abc.abstractmethod
204+
def __fspath__(self):
205+
"""Return the file system path representation of the object."""
206+
raise NotImplementedError
207+
208+
@classmethod
209+
def __subclasshook__(cls, subclass):
210+
if PurePath is not None and issubclass(subclass, PurePath):
211+
return True
212+
return hasattr(subclass, '__fspath__')
213+
214+
215+
def os_fspath(path):
216+
"""Return the path representation of a path-like object.
217+
If str or bytes is passed in, it is returned unchanged. Otherwise the
218+
os.PathLike interface is used to get the path representation. If the
219+
path representation is not str or bytes, TypeError is raised. If the
220+
provided path is not str, bytes, or os.PathLike, TypeError is raised.
221+
"""
222+
if isinstance(path, (str, bytes)):
223+
return path
224+
225+
# Work from the object's type to match method resolution of other magic
226+
# methods.
227+
path_type = type(path)
228+
try:
229+
path_repr = path_type.__fspath__(path)
230+
except AttributeError:
231+
if hasattr(path_type, '__fspath__'):
232+
raise
233+
elif PurePath is not None and issubclass(path_type, PurePath):
234+
return _PurePath__fspath__(path)
235+
else:
236+
raise TypeError("expected str, bytes or os.PathLike object, "
237+
"not " + path_type.__name__)
238+
if isinstance(path_repr, (str, bytes)):
239+
return path_repr
240+
else:
241+
raise TypeError("expected {}.__fspath__() to return str or bytes, "
242+
"not {}".format(path_type.__name__,
243+
type(path_repr).__name__))

numpy/core/memmap.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import numpy as np
44
from .numeric import uint8, ndarray, dtype
55
from numpy.compat import (
6-
long, basestring, is_pathlib_path, contextlib_nullcontext
6+
long, basestring, os_fspath, contextlib_nullcontext, is_pathlib_path
77
)
88

99
__all__ = ['memmap']
@@ -218,10 +218,8 @@ def __new__(subtype, filename, dtype=uint8, mode='r+', offset=0,
218218

219219
if hasattr(filename, 'read'):
220220
f_ctx = contextlib_nullcontext(filename)
221-
elif is_pathlib_path(filename):
222-
f_ctx = filename.open(('r' if mode == 'c' else mode)+'b')
223221
else:
224-
f_ctx = open(filename, ('r' if mode == 'c' else mode)+'b')
222+
f_ctx = open(os_fspath(filename), ('r' if mode == 'c' else mode)+'b')
225223

226224
with f_ctx as fid:
227225
fid.seek(0, 2)
@@ -268,14 +266,13 @@ def __new__(subtype, filename, dtype=uint8, mode='r+', offset=0,
268266
self.offset = offset
269267
self.mode = mode
270268

271-
if isinstance(filename, basestring):
272-
self.filename = os.path.abspath(filename)
273-
elif is_pathlib_path(filename):
269+
if is_pathlib_path(filename):
270+
# special case - if we were constructed with a pathlib.path,
271+
# then filename is a path object, not a string
274272
self.filename = filename.resolve()
275-
# py3 returns int for TemporaryFile().name
276-
elif (hasattr(filename, "name") and
277-
isinstance(filename.name, basestring)):
278-
self.filename = os.path.abspath(filename.name)
273+
elif hasattr(fid, "name") and isinstance(fid.name, basestring):
274+
# py3 returns int for TemporaryFile().name
275+
self.filename = os.path.abspath(fid.name)
279276
# same as memmap copies (e.g. memmap + 1)
280277
else:
281278
self.filename = None

numpy/lib/npyio.py

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from numpy.compat import (
2323
asbytes, asstr, asunicode, asbytes_nested, bytes, basestring, unicode,
24-
is_pathlib_path
24+
os_fspath, os_PathLike
2525
)
2626
from numpy.core.numeric import pickle
2727

@@ -104,8 +104,8 @@ def zipfile_factory(file, *args, **kwargs):
104104
pathlib.Path objects. `args` and `kwargs` are passed to the zipfile.ZipFile
105105
constructor.
106106
"""
107-
if is_pathlib_path(file):
108-
file = str(file)
107+
if not hasattr(file, 'read'):
108+
file = os_fspath(file)
109109
import zipfile
110110
kwargs['allowZip64'] = True
111111
return zipfile.ZipFile(file, *args, **kwargs)
@@ -399,15 +399,12 @@ def load(file, mmap_mode=None, allow_pickle=True, fix_imports=True,
399399
pickle_kwargs = {}
400400

401401
# TODO: Use contextlib.ExitStack once we drop Python 2
402-
if isinstance(file, basestring):
403-
fid = open(file, "rb")
404-
own_fid = True
405-
elif is_pathlib_path(file):
406-
fid = file.open("rb")
407-
own_fid = True
408-
else:
402+
if hasattr(file, 'read'):
409403
fid = file
410404
own_fid = False
405+
else:
406+
fid = open(os_fspath(file), "rb")
407+
own_fid = True
411408

412409
try:
413410
# Code to distinguish from NumPy binary files and pickles.
@@ -497,18 +494,14 @@ def save(file, arr, allow_pickle=True, fix_imports=True):
497494
498495
"""
499496
own_fid = False
500-
if isinstance(file, basestring):
497+
if hasattr(file, 'read'):
498+
fid = file
499+
else:
500+
file = os_fspath(file)
501501
if not file.endswith('.npy'):
502502
file = file + '.npy'
503503
fid = open(file, "wb")
504504
own_fid = True
505-
elif is_pathlib_path(file):
506-
if not file.name.endswith('.npy'):
507-
file = file.parent / (file.name + '.npy')
508-
fid = file.open("wb")
509-
own_fid = True
510-
else:
511-
fid = file
512505

513506
if sys.version_info[0] >= 3:
514507
pickle_kwargs = dict(fix_imports=fix_imports)
@@ -673,12 +666,10 @@ def _savez(file, args, kwds, compress, allow_pickle=True, pickle_kwargs=None):
673666
# component of the so-called standard library.
674667
import zipfile
675668

676-
if isinstance(file, basestring):
669+
if not hasattr(file, 'read'):
670+
file = os_fspath(file)
677671
if not file.endswith('.npz'):
678672
file = file + '.npz'
679-
elif is_pathlib_path(file):
680-
if not file.name.endswith('.npz'):
681-
file = file.parent / (file.name + '.npz')
682673

683674
namedict = kwds
684675
for i, val in enumerate(args):
@@ -926,8 +917,8 @@ def loadtxt(fname, dtype=float, comments='#', delimiter=None,
926917

927918
fown = False
928919
try:
929-
if is_pathlib_path(fname):
930-
fname = str(fname)
920+
if isinstance(fname, os_PathLike):
921+
fname = os_fspath(fname)
931922
if _is_string_like(fname):
932923
fh = np.lib._datasource.open(fname, 'rt', encoding=encoding)
933924
fencoding = getattr(fh, 'encoding', 'latin1')
@@ -1315,8 +1306,8 @@ def first_write(self, v):
13151306
self.write = self.write_bytes
13161307

13171308
own_fh = False
1318-
if is_pathlib_path(fname):
1319-
fname = str(fname)
1309+
if isinstance(fname, os_PathLike):
1310+
fname = os_fspath(fname)
13201311
if _is_string_like(fname):
13211312
# datasource doesn't support creating a new file ...
13221313
open(fname, 'wt').close()
@@ -1699,8 +1690,8 @@ def genfromtxt(fname, dtype=float, comments='#', delimiter=None,
16991690
# Initialize the filehandle, the LineSplitter and the NameValidator
17001691
own_fhd = False
17011692
try:
1702-
if is_pathlib_path(fname):
1703-
fname = str(fname)
1693+
if isinstance(fname, os_PathLike):
1694+
fname = os_fspath(fname)
17041695
if isinstance(fname, basestring):
17051696
fhd = iter(np.lib._datasource.open(fname, 'rt', encoding=encoding))
17061697
own_fhd = True

0 commit comments

Comments
 (0)