From 4a94ee439bc237a7790635a7e8391c4aaf891f7f Mon Sep 17 00:00:00 2001 From: Soren Rasmussen Date: Fri, 1 Feb 2019 15:15:38 +0000 Subject: [PATCH 1/6] ENH: pathlib support for fromfile(), .tofile() and .dump() See #8576 --- numpy/core/src/multiarray/methods.c | 3 ++ numpy/core/src/multiarray/methods.h | 33 ++++++++++++++++++++ numpy/core/src/multiarray/multiarraymodule.c | 2 ++ numpy/core/tests/test_multiarray.py | 19 +++++++++++ 4 files changed, 57 insertions(+) diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index 0ddec2995336..1c36f4da173b 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -577,6 +577,7 @@ array_tofile(PyArrayObject *self, PyObject *args, PyObject *kwds) return NULL; } + file = NpyPath_PathlikeToFspath(file); if (PyBytes_Check(file) || PyUnicode_Check(file)) { file = npy_PyFile_OpenFile(file, "wb"); if (file == NULL) { @@ -2110,6 +2111,8 @@ PyArray_Dump(PyObject *self, PyObject *file, int protocol) if (cpick == NULL) { return -1; } + + file = NpyPath_PathlikeToFspath(file); if (PyBytes_Check(file) || PyUnicode_Check(file)) { file = npy_PyFile_OpenFile(file, "wb"); if (file == NULL) { diff --git a/numpy/core/src/multiarray/methods.h b/numpy/core/src/multiarray/methods.h index 7bf87f42d53f..d8778f6b4e11 100644 --- a/numpy/core/src/multiarray/methods.h +++ b/numpy/core/src/multiarray/methods.h @@ -6,4 +6,37 @@ extern NPY_NO_EXPORT PyMethodDef array_methods[]; NPY_NO_EXPORT const char * npy_casting_to_string(NPY_CASTING casting); +/* + * Pathlib support. + * For Python >= 3.6, use the os.Pathlike interface. + * Else, for Python >= 3.4, use file = str(file) if file is a PurePath + * For older Python, do nothing. + */ +static inline PyObject * +NpyPath_PathlikeToFspath(PyObject *file) +{ +#if PY_VERSION_HEX >= 0x03060000 /* os.pathlike arrived in 3.6 */ + if (PyObject_HasAttrString(file, "__fspath__")) { + file = PyOS_FSPath(file); + } + return file; +#elif PY_VERSION_HEX >= 0x03040000 /* pathlib arrived in 3.4 */ + PyObject *pathlib, *pathlib_PurePath; + int fileIsPurePath; + + pathlib = PyImport_ImportModule("pathlib"); + if (!pathlib) { + return NULL; + } + pathlib_PurePath = PyObject_GetAttrString(pathlib, "PurePath"); + fileIsPurePath = PyObject_IsInstance(file, pathlib_PurePath); + Py_XDECREF(pathlib); + Py_XDECREF(pathlib_PurePath); + if (fileIsPurePath) { + file = PyObject_Str(file); + } +#endif + return file; +} + #endif diff --git a/numpy/core/src/multiarray/multiarraymodule.c b/numpy/core/src/multiarray/multiarraymodule.c index ce6a3870fd62..b2d54885a7e8 100644 --- a/numpy/core/src/multiarray/multiarraymodule.c +++ b/numpy/core/src/multiarray/multiarraymodule.c @@ -2071,6 +2071,8 @@ array_fromfile(PyObject *NPY_UNUSED(ignored), PyObject *args, PyObject *keywds) Py_XDECREF(type); return NULL; } + + file = NpyPath_PathlikeToFspath(file); if (PyString_Check(file) || PyUnicode_Check(file)) { file = npy_PyFile_OpenFile(file, "rb"); if (file == NULL) { diff --git a/numpy/core/tests/test_multiarray.py b/numpy/core/tests/test_multiarray.py index 834b4b483bec..a8afd1eae0d7 100644 --- a/numpy/core/tests/test_multiarray.py +++ b/numpy/core/tests/test_multiarray.py @@ -23,6 +23,8 @@ from numpy.core.numeric import pickle +if sys.version_info[:2] >= (3, 4): + import pathlib if sys.version_info[0] >= 3: import builtins else: @@ -4540,6 +4542,23 @@ def test_roundtrip_filename(self): y = np.fromfile(self.filename, dtype=self.dtype) assert_array_equal(y, self.x.flat) + def test_roundtrip_pathlib(self): + if sys.version_info[:2] >= (3, 4): + p = pathlib.Path(self.filename) + self.x.tofile(p) + y = np.fromfile(p, dtype=self.dtype) + assert_array_equal(y, self.x.flat) + + def test_roundtrip_dump_pathlib(self): + if sys.version_info[:2] >= (3, 4): + p = pathlib.Path(self.filename) + self.x.dump(p) + if sys.version_info[:2] >= (3, 6): + y = np.load(p) + else: + y = np.load(str(p)) + assert_array_equal(y, self.x) + def test_roundtrip_binary_str(self): s = self.x.tobytes() y = np.frombuffer(s, dtype=self.dtype) From eea6b8c119a69f79ee3d76bf20286a593a4c0880 Mon Sep 17 00:00:00 2001 From: Soren Rasmussen Date: Mon, 4 Feb 2019 15:04:00 +0000 Subject: [PATCH 2/6] Switched to using numpy.compat --- numpy/core/src/multiarray/methods.c | 3 ++ numpy/core/src/multiarray/methods.h | 38 +++++++------------- numpy/core/src/multiarray/multiarraymodule.c | 3 ++ 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index 1c36f4da173b..c074343a959c 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -578,6 +578,9 @@ array_tofile(PyArrayObject *self, PyObject *args, PyObject *kwds) } file = NpyPath_PathlikeToFspath(file); + if (file == NULL) { + return NULL; + } if (PyBytes_Check(file) || PyUnicode_Check(file)) { file = npy_PyFile_OpenFile(file, "wb"); if (file == NULL) { diff --git a/numpy/core/src/multiarray/methods.h b/numpy/core/src/multiarray/methods.h index d8778f6b4e11..961d3c2c5b46 100644 --- a/numpy/core/src/multiarray/methods.h +++ b/numpy/core/src/multiarray/methods.h @@ -1,42 +1,28 @@ #ifndef _NPY_ARRAY_METHODS_H_ #define _NPY_ARRAY_METHODS_H_ +#include "npy_import.h" + extern NPY_NO_EXPORT PyMethodDef array_methods[]; NPY_NO_EXPORT const char * npy_casting_to_string(NPY_CASTING casting); -/* - * Pathlib support. - * For Python >= 3.6, use the os.Pathlike interface. - * Else, for Python >= 3.4, use file = str(file) if file is a PurePath - * For older Python, do nothing. - */ +/* Pathlib support */ static inline PyObject * NpyPath_PathlikeToFspath(PyObject *file) { -#if PY_VERSION_HEX >= 0x03060000 /* os.pathlike arrived in 3.6 */ - if (PyObject_HasAttrString(file, "__fspath__")) { - file = PyOS_FSPath(file); - } - return file; -#elif PY_VERSION_HEX >= 0x03040000 /* pathlib arrived in 3.4 */ - PyObject *pathlib, *pathlib_PurePath; - int fileIsPurePath; - - pathlib = PyImport_ImportModule("pathlib"); - if (!pathlib) { - return NULL; + static PyObject *os_PathLike = NULL; + static PyObject *os_fspath = NULL; + npy_cache_import("numpy.compat", "os_PathLike", &os_PathLike); + if (os_PathLike == NULL) { + return file; } - pathlib_PurePath = PyObject_GetAttrString(pathlib, "PurePath"); - fileIsPurePath = PyObject_IsInstance(file, pathlib_PurePath); - Py_XDECREF(pathlib); - Py_XDECREF(pathlib_PurePath); - if (fileIsPurePath) { - file = PyObject_Str(file); + npy_cache_import("numpy.compat", "os_fspath", &os_fspath); + if ((os_fspath == NULL) || (!PyObject_IsInstance(file, os_PathLike))) { + return file; } -#endif - return file; + return PyObject_CallFunctionObjArgs(os_fspath, file, NULL); } #endif diff --git a/numpy/core/src/multiarray/multiarraymodule.c b/numpy/core/src/multiarray/multiarraymodule.c index b2d54885a7e8..d0a564793938 100644 --- a/numpy/core/src/multiarray/multiarraymodule.c +++ b/numpy/core/src/multiarray/multiarraymodule.c @@ -2073,6 +2073,9 @@ array_fromfile(PyObject *NPY_UNUSED(ignored), PyObject *args, PyObject *keywds) } file = NpyPath_PathlikeToFspath(file); + if (file == NULL) { + return NULL; + } if (PyString_Check(file) || PyUnicode_Check(file)) { file = npy_PyFile_OpenFile(file, "rb"); if (file == NULL) { From 3c060d5db6049dc83a66a05f266fad96a4bf96f5 Mon Sep 17 00:00:00 2001 From: Soren Rasmussen Date: Tue, 5 Feb 2019 08:32:14 +0000 Subject: [PATCH 3/6] Fixed remaining issues, pointed out by @eric-weiser --- numpy/core/src/multiarray/methods.c | 3 +++ numpy/core/src/multiarray/methods.h | 8 +++++-- numpy/core/tests/test_multiarray.py | 34 +++++++++++++++++------------ 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index c074343a959c..057670821dc2 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -2116,6 +2116,9 @@ PyArray_Dump(PyObject *self, PyObject *file, int protocol) } file = NpyPath_PathlikeToFspath(file); + if (file == NULL) { + return -1; + } if (PyBytes_Check(file) || PyUnicode_Check(file)) { file = npy_PyFile_OpenFile(file, "wb"); if (file == NULL) { diff --git a/numpy/core/src/multiarray/methods.h b/numpy/core/src/multiarray/methods.h index 961d3c2c5b46..b96a3c8a8fa4 100644 --- a/numpy/core/src/multiarray/methods.h +++ b/numpy/core/src/multiarray/methods.h @@ -16,10 +16,14 @@ NpyPath_PathlikeToFspath(PyObject *file) static PyObject *os_fspath = NULL; npy_cache_import("numpy.compat", "os_PathLike", &os_PathLike); if (os_PathLike == NULL) { - return file; + return NULL; } npy_cache_import("numpy.compat", "os_fspath", &os_fspath); - if ((os_fspath == NULL) || (!PyObject_IsInstance(file, os_PathLike))) { + if (os_fspath == NULL) { + return NULL; + } + + if (!PyObject_IsInstance(file, os_PathLike)) { return file; } return PyObject_CallFunctionObjArgs(os_fspath, file, NULL); diff --git a/numpy/core/tests/test_multiarray.py b/numpy/core/tests/test_multiarray.py index a8afd1eae0d7..f834c1557e24 100644 --- a/numpy/core/tests/test_multiarray.py +++ b/numpy/core/tests/test_multiarray.py @@ -23,8 +23,14 @@ from numpy.core.numeric import pickle -if sys.version_info[:2] >= (3, 4): +try: import pathlib +except ImportError: + try: + import pathlib2 as pathlib + except ImportError: + pathlib = None + if sys.version_info[0] >= 3: import builtins else: @@ -4542,22 +4548,22 @@ def test_roundtrip_filename(self): y = np.fromfile(self.filename, dtype=self.dtype) assert_array_equal(y, self.x.flat) + @pytest.mark.skipif(pathlib is None, reason="pathlib not found") def test_roundtrip_pathlib(self): - if sys.version_info[:2] >= (3, 4): - p = pathlib.Path(self.filename) - self.x.tofile(p) - y = np.fromfile(p, dtype=self.dtype) - assert_array_equal(y, self.x.flat) + p = pathlib.Path(self.filename) + self.x.tofile(p) + y = np.fromfile(p, dtype=self.dtype) + assert_array_equal(y, self.x.flat) + @pytest.mark.skipif(pathlib is None, reason="pathlib not found") def test_roundtrip_dump_pathlib(self): - if sys.version_info[:2] >= (3, 4): - p = pathlib.Path(self.filename) - self.x.dump(p) - if sys.version_info[:2] >= (3, 6): - y = np.load(p) - else: - y = np.load(str(p)) - assert_array_equal(y, self.x) + p = pathlib.Path(self.filename) + self.x.dump(p) + if sys.version_info[:2] >= (3, 6): + y = np.load(p) + else: + y = np.load(str(p)) + assert_array_equal(y, self.x) def test_roundtrip_binary_str(self): s = self.x.tobytes() From 703c59c8c35d7a9d0d748866de278214befd4176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Rasmussen?= Date: Tue, 12 Mar 2019 09:53:17 +0100 Subject: [PATCH 4/6] Added release note --- doc/release/1.17.0-notes.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/release/1.17.0-notes.rst b/doc/release/1.17.0-notes.rst index ab09f85bd6b4..b6745c56bea7 100644 --- a/doc/release/1.17.0-notes.rst +++ b/doc/release/1.17.0-notes.rst @@ -127,6 +127,11 @@ but with this change, you can do:: thereby saving a level of indentation +Pathlib support for `np.fromfile`, `ndarray.tofile` and `ndarray.dump` +---------------------------------------------------------------------- +`np.fromfile`, `np.ndarray.tofile` and `np.ndarray.dump` now support +the `pathlib.Path` type for the `file`/`fid` parameter. + Changes ======= From 0933c152c3629376fab9c89e93733fa20b5c2eea Mon Sep 17 00:00:00 2001 From: sorenrasmussenai <47032123+sorenrasmussenai@users.noreply.github.com> Date: Tue, 19 Mar 2019 09:26:41 +0100 Subject: [PATCH 5/6] Update 1.17.0-notes.rst --- doc/release/1.17.0-notes.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/release/1.17.0-notes.rst b/doc/release/1.17.0-notes.rst index fdcb3f77596f..29ba7e71e892 100644 --- a/doc/release/1.17.0-notes.rst +++ b/doc/release/1.17.0-notes.rst @@ -163,10 +163,10 @@ thereby saving a level of indentation In some cases where ``np.interp`` would previously return ``np.nan``, it now returns an appropriate infinity. -Pathlib support for `np.fromfile`, `ndarray.tofile` and `ndarray.dump` ----------------------------------------------------------------------- -`np.fromfile`, `np.ndarray.tofile` and `np.ndarray.dump` now support -the `pathlib.Path` type for the `file`/`fid` parameter. +Pathlib support for ``np.fromfile``, ``ndarray.tofile`` and ``ndarray.dump`` +---------------------------------------------------------------------------- +``np.fromfile``, ``np.ndarray.tofile`` and ``np.ndarray.dump`` now support +the `pathlib.Path` type for the ``file``/``fid`` parameter. Changes From 6c8f7fb81a9a298455ff1a09125da20e8354fe59 Mon Sep 17 00:00:00 2001 From: Eric Wieser Date: Thu, 30 May 2019 19:32:23 -0700 Subject: [PATCH 6/6] TST, DOC: Fix tests, add versionchanged sections --- numpy/core/_add_newdocs.py | 17 ++++++++++++++--- numpy/core/tests/test_multiarray.py | 5 +---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/numpy/core/_add_newdocs.py b/numpy/core/_add_newdocs.py index 7a7a433c017f..f1c1d3606a5a 100644 --- a/numpy/core/_add_newdocs.py +++ b/numpy/core/_add_newdocs.py @@ -1152,8 +1152,12 @@ Parameters ---------- - file : file or str + file : file or str or Path Open file object or filename. + + .. versionchanged:: 1.17.0 + `pathlib.Path` objects are now accepted. + dtype : data-type Data type of the returned array. For binary files, it is used to determine the size and byte-order @@ -2957,9 +2961,12 @@ Parameters ---------- - file : str + file : str or Path A string naming the dump file. + .. versionchanged:: 1.17.0 + `pathlib.Path` objects are now accepted. + """)) @@ -4004,8 +4011,12 @@ Parameters ---------- - fid : file or str + fid : file or str or Path An open file object, or a string containing a filename. + + .. versionchanged:: 1.17.0 + `pathlib.Path` objects are now accepted. + sep : str Separator between array items for text output. If "" (empty), a binary file is written, equivalent to diff --git a/numpy/core/tests/test_multiarray.py b/numpy/core/tests/test_multiarray.py index c7d2bf6b733f..6741c3bb5efa 100644 --- a/numpy/core/tests/test_multiarray.py +++ b/numpy/core/tests/test_multiarray.py @@ -4654,10 +4654,7 @@ def test_roundtrip_pathlib(self): def test_roundtrip_dump_pathlib(self): p = pathlib.Path(self.filename) self.x.dump(p) - if sys.version_info[:2] >= (3, 6): - y = np.load(p) - else: - y = np.load(str(p)) + y = np.load(p, allow_pickle=True) assert_array_equal(y, self.x) def test_roundtrip_binary_str(self):