diff options
author | Eric Wieser <wieser.eric@gmail.com> | 2018-10-12 07:08:22 -0700 |
---|---|---|
committer | Eric Wieser <wieser.eric@gmail.com> | 2018-10-12 08:39:40 -0700 |
commit | 489362c0779bd60c688ce87baf9ecd6ac9ccf938 (patch) | |
tree | 98656881b4c774d30783b21e62b760c2905decb0 | |
parent | 28679be331b25a3ae72403e42cc6b17cffddc124 (diff) | |
download | numpy-489362c0779bd60c688ce87baf9ecd6ac9ccf938.tar.gz |
ENH: Add support for third-party path-like objects by backporting os.fspath
-rw-r--r-- | numpy/compat/py3k.py | 70 | ||||
-rw-r--r-- | numpy/core/memmap.py | 19 | ||||
-rw-r--r-- | numpy/lib/npyio.py | 47 |
3 files changed, 94 insertions, 42 deletions
diff --git a/numpy/compat/py3k.py b/numpy/compat/py3k.py index ce4543bc3..8e06ead78 100644 --- a/numpy/compat/py3k.py +++ b/numpy/compat/py3k.py @@ -8,13 +8,13 @@ __all__ = ['bytes', 'asbytes', 'isfileobj', 'getexception', 'strchar', 'unicode', 'asunicode', 'asbytes_nested', 'asunicode_nested', 'asstr', 'open_latin1', 'long', 'basestring', 'sixu', 'integer_types', 'is_pathlib_path', 'npy_load_module', 'Path', - 'contextlib_nullcontext'] + 'contextlib_nullcontext', 'os_fspath', 'os_PathLike'] import sys try: - from pathlib import Path + from pathlib import Path, PurePath except ImportError: - Path = None + Path = PurePath = None if sys.version_info[0] >= 3: import io @@ -95,6 +95,8 @@ def asunicode_nested(x): def is_pathlib_path(obj): """ Check whether obj is a pathlib.Path object. + + Prefer using `isinstance(obj, os_PathLike)` instead of this function. """ return Path is not None and isinstance(obj, Path) @@ -177,3 +179,65 @@ else: finally: fo.close() return mod + +# backport abc.ABC +import abc +if sys.version_info[:2] >= (3, 4): + abc_ABC = abc.ABC +else: + abc_ABC = abc.ABCMeta('ABC', (object,), {'__slots__': ()}) + + +# Backport os.fs_path, os.PathLike, and PurePath.__fspath__ +if sys.version_info[:2] >= (3, 6): + import os + os_fspath = os.fspath + os_PathLike = os.PathLike +else: + def _PurePath__fspath__(self): + return str(self) + + class os_PathLike(abc_ABC): + """Abstract base class for implementing the file system path protocol.""" + + @abc.abstractmethod + def __fspath__(self): + """Return the file system path representation of the object.""" + raise NotImplementedError + + @classmethod + def __subclasshook__(cls, subclass): + if PurePath is not None and issubclass(subclass, PurePath): + return True + return hasattr(subclass, '__fspath__') + + + def os_fspath(path): + """Return the path representation of a path-like object. + If str or bytes is passed in, it is returned unchanged. Otherwise the + os.PathLike interface is used to get the path representation. If the + path representation is not str or bytes, TypeError is raised. If the + provided path is not str, bytes, or os.PathLike, TypeError is raised. + """ + if isinstance(path, (str, bytes)): + return path + + # Work from the object's type to match method resolution of other magic + # methods. + path_type = type(path) + try: + path_repr = path_type.__fspath__(path) + except AttributeError: + if hasattr(path_type, '__fspath__'): + raise + elif PurePath is not None and issubclass(path_type, PurePath): + return _PurePath__fspath__(path) + else: + raise TypeError("expected str, bytes or os.PathLike object, " + "not " + path_type.__name__) + if isinstance(path_repr, (str, bytes)): + return path_repr + else: + raise TypeError("expected {}.__fspath__() to return str or bytes, " + "not {}".format(path_type.__name__, + type(path_repr).__name__)) diff --git a/numpy/core/memmap.py b/numpy/core/memmap.py index 8269f537f..f5cc68bb9 100644 --- a/numpy/core/memmap.py +++ b/numpy/core/memmap.py @@ -3,7 +3,7 @@ from __future__ import division, absolute_import, print_function import numpy as np from .numeric import uint8, ndarray, dtype from numpy.compat import ( - long, basestring, is_pathlib_path, contextlib_nullcontext + long, basestring, os_fspath, contextlib_nullcontext, is_pathlib_path ) __all__ = ['memmap'] @@ -218,10 +218,8 @@ class memmap(ndarray): if hasattr(filename, 'read'): f_ctx = contextlib_nullcontext(filename) - elif is_pathlib_path(filename): - f_ctx = filename.open(('r' if mode == 'c' else mode)+'b') else: - f_ctx = open(filename, ('r' if mode == 'c' else mode)+'b') + f_ctx = open(os_fspath(filename), ('r' if mode == 'c' else mode)+'b') with f_ctx as fid: fid.seek(0, 2) @@ -268,14 +266,13 @@ class memmap(ndarray): self.offset = offset self.mode = mode - if isinstance(filename, basestring): - self.filename = os.path.abspath(filename) - elif is_pathlib_path(filename): + if is_pathlib_path(filename): + # special case - if we were constructed with a pathlib.path, + # then filename is a path object, not a string self.filename = filename.resolve() - # py3 returns int for TemporaryFile().name - elif (hasattr(filename, "name") and - isinstance(filename.name, basestring)): - self.filename = os.path.abspath(filename.name) + elif hasattr(fid, "name") and isinstance(fid.name, basestring): + # py3 returns int for TemporaryFile().name + self.filename = os.path.abspath(fid.name) # same as memmap copies (e.g. memmap + 1) else: self.filename = None diff --git a/numpy/lib/npyio.py b/numpy/lib/npyio.py index 62fc9c5b3..5e4e8e47f 100644 --- a/numpy/lib/npyio.py +++ b/numpy/lib/npyio.py @@ -21,7 +21,7 @@ from ._iotools import ( from numpy.compat import ( asbytes, asstr, asunicode, asbytes_nested, bytes, basestring, unicode, - is_pathlib_path + os_fspath, os_PathLike ) from numpy.core.numeric import pickle @@ -104,8 +104,8 @@ def zipfile_factory(file, *args, **kwargs): pathlib.Path objects. `args` and `kwargs` are passed to the zipfile.ZipFile constructor. """ - if is_pathlib_path(file): - file = str(file) + if not hasattr(file, 'read'): + file = os_fspath(file) import zipfile kwargs['allowZip64'] = True return zipfile.ZipFile(file, *args, **kwargs) @@ -399,15 +399,12 @@ def load(file, mmap_mode=None, allow_pickle=True, fix_imports=True, pickle_kwargs = {} # TODO: Use contextlib.ExitStack once we drop Python 2 - if isinstance(file, basestring): - fid = open(file, "rb") - own_fid = True - elif is_pathlib_path(file): - fid = file.open("rb") - own_fid = True - else: + if hasattr(file, 'read'): fid = file own_fid = False + else: + fid = open(os_fspath(file), "rb") + own_fid = True try: # Code to distinguish from NumPy binary files and pickles. @@ -497,18 +494,14 @@ def save(file, arr, allow_pickle=True, fix_imports=True): """ own_fid = False - if isinstance(file, basestring): + if hasattr(file, 'read'): + fid = file + else: + file = os_fspath(file) if not file.endswith('.npy'): file = file + '.npy' fid = open(file, "wb") own_fid = True - elif is_pathlib_path(file): - if not file.name.endswith('.npy'): - file = file.parent / (file.name + '.npy') - fid = file.open("wb") - own_fid = True - else: - fid = file if sys.version_info[0] >= 3: pickle_kwargs = dict(fix_imports=fix_imports) @@ -673,12 +666,10 @@ def _savez(file, args, kwds, compress, allow_pickle=True, pickle_kwargs=None): # component of the so-called standard library. import zipfile - if isinstance(file, basestring): + if not hasattr(file, 'read'): + file = os_fspath(file) if not file.endswith('.npz'): file = file + '.npz' - elif is_pathlib_path(file): - if not file.name.endswith('.npz'): - file = file.parent / (file.name + '.npz') namedict = kwds for i, val in enumerate(args): @@ -926,8 +917,8 @@ def loadtxt(fname, dtype=float, comments='#', delimiter=None, fown = False try: - if is_pathlib_path(fname): - fname = str(fname) + if isinstance(fname, os_PathLike): + fname = os_fspath(fname) if _is_string_like(fname): fh = np.lib._datasource.open(fname, 'rt', encoding=encoding) fencoding = getattr(fh, 'encoding', 'latin1') @@ -1315,8 +1306,8 @@ def savetxt(fname, X, fmt='%.18e', delimiter=' ', newline='\n', header='', self.write = self.write_bytes own_fh = False - if is_pathlib_path(fname): - fname = str(fname) + if isinstance(fname, os_PathLike): + fname = os_fspath(fname) if _is_string_like(fname): # datasource doesn't support creating a new file ... open(fname, 'wt').close() @@ -1699,8 +1690,8 @@ def genfromtxt(fname, dtype=float, comments='#', delimiter=None, # Initialize the filehandle, the LineSplitter and the NameValidator own_fhd = False try: - if is_pathlib_path(fname): - fname = str(fname) + if isinstance(fname, os_PathLike): + fname = os_fspath(fname) if isinstance(fname, basestring): fhd = iter(np.lib._datasource.open(fname, 'rt', encoding=encoding)) own_fhd = True |