summaryrefslogtreecommitdiff
path: root/numpy
diff options
context:
space:
mode:
authorSebastian Berg <sebastian@sipsolutions.net>2022-02-03 14:38:21 -0600
committerSebastian Berg <sebastian@sipsolutions.net>2022-02-03 16:58:31 -0600
commit7c7e41f1a9e06bfe1c852e1971077a3fa6446db7 (patch)
tree751be9fd20a5c34e8cb6d2527402012624acc9cc /numpy
parent9d9cc5ca473d52eeda693a254fc0a1cbd4f4f1c4 (diff)
downloadnumpy-7c7e41f1a9e06bfe1c852e1971077a3fa6446db7.tar.gz
ENH: Allow object and subarray dtypes in fromiter
Allows dtypes with objects (and subarray) in fromiter. There was never really a good reason for rejecting them. Otherwise does some maintanence (e.g. the array is contiguous, so no need to do complicated index calculations), improve the error message, and add tests for those things. Closes gh-4791, gh-15789
Diffstat (limited to 'numpy')
-rw-r--r--numpy/core/_add_newdocs.py5
-rw-r--r--numpy/core/src/multiarray/ctors.c88
-rw-r--r--numpy/core/tests/test_numeric.py45
3 files changed, 100 insertions, 38 deletions
diff --git a/numpy/core/_add_newdocs.py b/numpy/core/_add_newdocs.py
index e432f6a11..560a8b386 100644
--- a/numpy/core/_add_newdocs.py
+++ b/numpy/core/_add_newdocs.py
@@ -1398,6 +1398,11 @@ add_newdoc('numpy.core.multiarray', 'fromiter',
An iterable object providing data for the array.
dtype : data-type
The data-type of the returned array.
+
+ .. versionchanged:: 1.23
+ Object and subarray dtypes are now supported (note that the final
+ result is not 1-D for a subarray dtype).
+
count : int, optional
The number of items to read from *iterable*. The default is -1,
which means all data is read.
diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c
index c2842d7ba..b1d918db7 100644
--- a/numpy/core/src/multiarray/ctors.c
+++ b/numpy/core/src/multiarray/ctors.c
@@ -3874,11 +3874,9 @@ PyArray_FromString(char *data, npy_intp slen, PyArray_Descr *dtype,
NPY_NO_EXPORT PyObject *
PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count)
{
- PyObject *value;
PyObject *iter = NULL;
PyArrayObject *ret = NULL;
npy_intp i, elsize, elcount;
- char *item, *new_data;
if (dtype == NULL) {
return NULL;
@@ -3890,6 +3888,7 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count)
}
if (PyDataType_ISUNSIZED(dtype)) {
+ /* If this error is removed, the `ret` allocation may need fixing */
PyErr_SetString(PyExc_ValueError,
"Must specify length when using variable-size data-type.");
goto done;
@@ -3907,38 +3906,43 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count)
elsize = dtype->elsize;
/*
- * We would need to alter the memory RENEW code to decrement any
- * reference counts before throwing away any memory.
+ * Note that PyArray_DESCR(ret) may not match dtype. There are exactly
+ * two cases where this can happen: empty strings/bytes/void (rejected
+ * above) and subarray dtypes (supported by sticking with `dtype`).
*/
- if (PyDataType_REFCHK(dtype)) {
- PyErr_SetString(PyExc_ValueError,
- "cannot create object arrays from iterator");
- goto done;
- }
-
+ Py_INCREF(dtype);
ret = (PyArrayObject *)PyArray_NewFromDescr(&PyArray_Type, dtype, 1,
&elcount, NULL,NULL, 0, NULL);
- dtype = NULL;
if (ret == NULL) {
goto done;
}
- for (i = 0; (i < count || count == -1) &&
- (value = PyIter_Next(iter)); i++) {
- if (i >= elcount && elsize != 0) {
+
+ char *item = PyArray_BYTES(ret);
+ for (i = 0; i < count || count == -1; i++, item += elsize) {
+ PyObject *value = PyIter_Next(iter);
+ if (value == NULL) {
+ if (PyErr_Occurred()) {
+ /* Fetching next item failed rather than exhausting iterator */
+ goto done;
+ }
+ break;
+ }
+
+ if (NPY_UNLIKELY(i >= elcount) && elsize != 0) {
+ char *new_data = NULL;
npy_intp nbytes;
/*
Grow PyArray_DATA(ret):
this is similar for the strategy for PyListObject, but we use
50% overallocation => 0, 4, 8, 14, 23, 36, 56, 86 ...
+ TODO: The loadtxt code now uses a `growth` helper that would
+ be suitable to reuse here.
*/
elcount = (i >> 1) + (i < 4 ? 4 : 2) + i;
if (!npy_mul_with_overflow_intp(&nbytes, elcount, elsize)) {
/* The handler is always valid */
- new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), nbytes,
- PyArray_HANDLER(ret));
- }
- else {
- new_data = NULL;
+ new_data = PyDataMem_UserRENEW(
+ PyArray_BYTES(ret), nbytes, PyArray_HANDLER(ret));
}
if (new_data == NULL) {
PyErr_SetString(PyExc_MemoryError,
@@ -3947,11 +3951,17 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count)
goto done;
}
((PyArrayObject_fields *)ret)->data = new_data;
+ /* resize array for cleanup: */
+ PyArray_DIMS(ret)[0] = elcount;
+ /* Reset `item` pointer to point into realloc'd chunk */
+ item = new_data + i * elsize;
+ if (PyDataType_FLAGCHK(dtype, NPY_NEEDS_INIT)) {
+ /* Initialize new chunk: */
+ memset(item, 0, nbytes - i * elsize);
+ }
}
- PyArray_DIMS(ret)[0] = i + 1;
- if (((item = index2ptr(ret, i)) == NULL) ||
- PyArray_SETITEM(ret, item, value) == -1) {
+ if (PyArray_Pack(dtype, item, value) < -1) {
Py_DECREF(value);
goto done;
}
@@ -3959,32 +3969,34 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count)
}
- if (PyErr_Occurred()) {
- goto done;
- }
if (i < count) {
- PyErr_SetString(PyExc_ValueError,
- "iterator too short");
+ PyErr_Format(PyExc_ValueError,
+ "iterator too short: Expected %zd but iterator had only %zd "
+ "items.", (Py_ssize_t)count, (Py_ssize_t)i);
goto done;
}
/*
- * Realloc the data so that don't keep extra memory tied up
- * (assuming realloc is reasonably good about reusing space...)
+ * Realloc the data so that don't keep extra memory tied up and fix
+ * the arrays first dimension (there could be more than one).
*/
if (i == 0 || elsize == 0) {
/* The size cannot be zero for realloc. */
- goto done;
}
- /* The handler is always valid */
- new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), i * elsize,
- PyArray_HANDLER(ret));
- if (new_data == NULL) {
- PyErr_SetString(PyExc_MemoryError,
- "cannot allocate array memory");
- goto done;
+ else {
+ /* Resize array to actual final size (it may be too large) */
+ /* The handler is always valid */
+ char *new_data = PyDataMem_UserRENEW(
+ PyArray_DATA(ret), i * elsize, PyArray_HANDLER(ret));
+
+ if (new_data == NULL) {
+ PyErr_SetString(PyExc_MemoryError,
+ "cannot allocate array memory");
+ goto done;
+ }
+ ((PyArrayObject_fields *)ret)->data = new_data;
}
- ((PyArrayObject_fields *)ret)->data = new_data;
+ PyArray_DIMS(ret)[0] = i;
done:
Py_XDECREF(iter);
diff --git a/numpy/core/tests/test_numeric.py b/numpy/core/tests/test_numeric.py
index ad9437911..d6d051c28 100644
--- a/numpy/core/tests/test_numeric.py
+++ b/numpy/core/tests/test_numeric.py
@@ -1215,6 +1215,51 @@ class TestFromiter:
assert_raises(NIterError, np.fromiter,
self.load_data(count, eindex), dtype=int, count=count)
+ @pytest.mark.parametrize("dtype", ["S", "S0", "V0", "U0"])
+ def test_empty_not_structured(self, dtype):
+ # Note, "S0" could be allowed at some point, so long "S" (without
+ # any length) is rejected.
+ with pytest.raises(ValueError, match="Must specify length"):
+ np.fromiter([], dtype=dtype)
+
+ @pytest.mark.parametrize("dtype",
+ # Note that `np.dtype(("O", (10, 5)))` is a subarray dtype
+ ["d", "i,O", np.dtype(("O", (10, 5))), "O"])
+ def test_growth_and_complicated_dtypes(self, dtype):
+ dtype = np.dtype(dtype)
+ data = [1, 2, 3, 4, 5, 6, 7, 8, 9] * 100 # make sure we realloc a bit
+
+ class MyIter:
+ # Class/example from gh-15789
+ def __length_hint__(self):
+ # only required to be an estimate, this is legal
+ return 1
+
+ def __iter__(self):
+ return iter(data)
+
+ res = np.fromiter(MyIter(), dtype=dtype)
+ expected = np.array(data, dtype=dtype)
+
+ assert_array_equal(res, expected)
+
+ def test_empty_result(self):
+ class MyIter:
+ def __length_hint__(self):
+ return 10
+
+ def __iter__(self):
+ return iter([]) # actual iterator is empty.
+
+ res = np.fromiter(MyIter(), dtype="d")
+ assert res.shape == (0,)
+ assert res.dtype == "d"
+
+ def test_too_few_items(self):
+ msg = "iterator too short: Expected 10 but iterator had only 3 items."
+ with pytest.raises(ValueError, match=msg):
+ np.fromiter([1, 2, 3], count=10, dtype=int)
+
class TestNonzero:
def test_nonzero_trivial(self):