diff options
Diffstat (limited to 'numpy/core')
-rw-r--r-- | numpy/core/src/multiarray/_multiarray_tests.c.src | 32 | ||||
-rw-r--r-- | numpy/core/src/multiarray/conversion_utils.c | 225 | ||||
-rw-r--r-- | numpy/core/tests/test_conversion_utils.py | 5 | ||||
-rw-r--r-- | numpy/core/tests/test_multiarray.py | 39 | ||||
-rw-r--r-- | numpy/core/tests/test_regression.py | 2 |
5 files changed, 219 insertions, 84 deletions
diff --git a/numpy/core/src/multiarray/_multiarray_tests.c.src b/numpy/core/src/multiarray/_multiarray_tests.c.src index fd7c1d070..0fcebedc7 100644 --- a/numpy/core/src/multiarray/_multiarray_tests.c.src +++ b/numpy/core/src/multiarray/_multiarray_tests.c.src @@ -2351,6 +2351,32 @@ npy_ensurenocopy(PyObject* NPY_UNUSED(self), PyObject* args) Py_RETURN_NONE; } +static PyObject * +run_scalar_intp_converter(PyObject *NPY_UNUSED(self), PyObject *obj) +{ + PyArray_Dims dims; + if (!PyArray_IntpConverter(obj, &dims)) { + return NULL; + } + else { + PyObject *result = PyArray_IntTupleFromIntp(dims.len, dims.ptr); + PyDimMem_FREE(dims.ptr); + return result; + } +} + +static PyObject * +run_scalar_intp_from_sequence(PyObject *NPY_UNUSED(self), PyObject *obj) +{ + npy_intp vals[1]; + + int output = PyArray_IntpFromSequence(obj, vals, 1); + if (output == -1) { + return NULL; + } + return PyArray_IntTupleFromIntp(1, vals); +} + static PyMethodDef Multiarray_TestsMethods[] = { {"argparse_example_function", (PyCFunction)argparse_example_function, @@ -2541,6 +2567,12 @@ static PyMethodDef Multiarray_TestsMethods[] = { {"run_casting_converter", run_casting_converter, METH_VARARGS, NULL}, + {"run_scalar_intp_converter", + run_scalar_intp_converter, + METH_O, NULL}, + {"run_scalar_intp_from_sequence", + run_scalar_intp_from_sequence, + METH_O, NULL}, {"run_intp_converter", run_intp_converter, METH_VARARGS, NULL}, diff --git a/numpy/core/src/multiarray/conversion_utils.c b/numpy/core/src/multiarray/conversion_utils.c index e4eb4f49e..6859b557c 100644 --- a/numpy/core/src/multiarray/conversion_utils.c +++ b/numpy/core/src/multiarray/conversion_utils.c @@ -78,6 +78,27 @@ PyArray_OutputConverter(PyObject *object, PyArrayObject **address) } } + +/* + * Convert the given value to an integer. Replaces the error when compared + * to `PyArray_PyIntAsIntp`. Exists mainly to retain old behaviour of + * `PyArray_IntpConverter` and `PyArray_IntpFromSequence` + */ +static NPY_INLINE npy_intp +dimension_from_scalar(PyObject *ob) +{ + npy_intp value = PyArray_PyIntAsIntp(ob); + + if (error_converting(value)) { + if (PyErr_ExceptionMatches(PyExc_OverflowError)) { + PyErr_SetString(PyExc_ValueError, + "Maximum allowed dimension exceeded"); + } + return -1; + } + return value; +} + /*NUMPY_API * Get intp chunk from sequence * @@ -90,9 +111,6 @@ PyArray_OutputConverter(PyObject *object, PyArrayObject **address) NPY_NO_EXPORT int PyArray_IntpConverter(PyObject *obj, PyArray_Dims *seq) { - Py_ssize_t len; - int nd; - seq->ptr = NULL; seq->len = 0; @@ -110,42 +128,85 @@ PyArray_IntpConverter(PyObject *obj, PyArray_Dims *seq) return NPY_SUCCEED; } - len = PySequence_Size(obj); - if (len == -1) { - /* Check to see if it is an integer number */ - if (PyNumber_Check(obj)) { - /* - * After the deprecation the PyNumber_Check could be replaced - * by PyIndex_Check. - * FIXME 1.9 ? - */ - len = 1; + PyObject *seq_obj = NULL; + + /* + * If obj is a scalar we skip all the useless computations and jump to + * dimension_from_scalar as soon as possible. + */ + if (!PyLong_CheckExact(obj) && PySequence_Check(obj)) { + seq_obj = PySequence_Fast(obj, + "expected a sequence of integers or a single integer."); + if (seq_obj == NULL) { + /* continue attempting to parse as a single integer. */ + PyErr_Clear(); } } - if (len < 0) { - PyErr_SetString(PyExc_TypeError, - "expected sequence object with len >= 0 or a single integer"); - return NPY_FAIL; - } - if (len > NPY_MAXDIMS) { - PyErr_Format(PyExc_ValueError, "maximum supported dimension for an ndarray is %d" - ", found %d", NPY_MAXDIMS, len); - return NPY_FAIL; - } - if (len > 0) { - seq->ptr = npy_alloc_cache_dim(len); + + if (seq_obj == NULL) { + /* + * obj *might* be a scalar (if dimension_from_scalar does not fail, at + * the moment no check have been performed to verify this hypothesis). + */ + seq->ptr = npy_alloc_cache_dim(1); if (seq->ptr == NULL) { PyErr_NoMemory(); return NPY_FAIL; } + else { + seq->len = 1; + + seq->ptr[0] = dimension_from_scalar(obj); + if (error_converting(seq->ptr[0])) { + /* + * If the error occurred is a type error (cannot convert the + * value to an integer) communicate that we expected a sequence + * or an integer from the user. + */ + if (PyErr_ExceptionMatches(PyExc_TypeError)) { + PyErr_Format(PyExc_TypeError, + "expected a sequence of integers or a single " + "integer, got '%.100R'", obj); + } + npy_free_cache_dim_obj(*seq); + seq->ptr = NULL; + return NPY_FAIL; + } + } } - seq->len = len; - nd = PyArray_IntpFromIndexSequence(obj, (npy_intp *)seq->ptr, len); - if (nd == -1 || nd != len) { - npy_free_cache_dim_obj(*seq); - seq->ptr = NULL; - return NPY_FAIL; + else { + /* + * `obj` is a sequence converted to the `PySequence_Fast` in `seq_obj` + */ + Py_ssize_t len = PySequence_Fast_GET_SIZE(seq_obj); + if (len > NPY_MAXDIMS) { + PyErr_Format(PyExc_ValueError, + "maximum supported dimension for an ndarray " + "is %d, found %d", NPY_MAXDIMS, len); + Py_DECREF(seq_obj); + return NPY_FAIL; + } + if (len > 0) { + seq->ptr = npy_alloc_cache_dim(len); + if (seq->ptr == NULL) { + PyErr_NoMemory(); + Py_DECREF(seq_obj); + return NPY_FAIL; + } + } + + seq->len = len; + int nd = PyArray_IntpFromIndexSequence(seq_obj, + (npy_intp *)seq->ptr, len); + Py_DECREF(seq_obj); + + if (nd == -1 || nd != len) { + npy_free_cache_dim_obj(*seq); + seq->ptr = NULL; + return NPY_FAIL; + } } + return NPY_SUCCEED; } @@ -1004,64 +1065,35 @@ PyArray_IntpFromPyIntConverter(PyObject *o, npy_intp *val) } -/* - * PyArray_IntpFromIndexSequence - * Returns the number of dimensions or -1 if an error occurred. - * vals must be large enough to hold maxvals. - * Opposed to PyArray_IntpFromSequence it uses and returns npy_intp - * for the number of values. +/** + * Reads values from a sequence of integers and stores them into an array. + * + * @param seq A sequence created using `PySequence_Fast`. + * @param vals Array used to store dimensions (must be large enough to + * hold `maxvals` values). + * @param max_vals Maximum number of dimensions that can be written into `vals`. + * @return Number of dimensions or -1 if an error occurred. + * + * .. note:: + * + * Opposed to PyArray_IntpFromSequence it uses and returns `npy_intp` + * for the number of values. */ NPY_NO_EXPORT npy_intp PyArray_IntpFromIndexSequence(PyObject *seq, npy_intp *vals, npy_intp maxvals) { - Py_ssize_t nd; - npy_intp i; - PyObject *op, *err; - /* - * Check to see if sequence is a single integer first. - * or, can be made into one + * First of all, check if sequence is a scalar integer or if it can be + * "casted" into a scalar. */ - nd = PySequence_Length(seq); - if (nd == -1) { - if (PyErr_Occurred()) { - PyErr_Clear(); - } + Py_ssize_t nd = PySequence_Fast_GET_SIZE(seq); + PyObject *op; + for (Py_ssize_t i = 0; i < PyArray_MIN(nd, maxvals); i++) { + op = PySequence_Fast_GET_ITEM(seq, i); - vals[0] = PyArray_PyIntAsIntp(seq); - if(vals[0] == -1) { - err = PyErr_Occurred(); - if (err && - PyErr_GivenExceptionMatches(err, PyExc_OverflowError)) { - PyErr_SetString(PyExc_ValueError, - "Maximum allowed dimension exceeded"); - } - if(err != NULL) { - return -1; - } - } - nd = 1; - } - else { - for (i = 0; i < PyArray_MIN(nd,maxvals); i++) { - op = PySequence_GetItem(seq, i); - if (op == NULL) { - return -1; - } - - vals[i] = PyArray_PyIntAsIntp(op); - Py_DECREF(op); - if(vals[i] == -1) { - err = PyErr_Occurred(); - if (err && - PyErr_GivenExceptionMatches(err, PyExc_OverflowError)) { - PyErr_SetString(PyExc_ValueError, - "Maximum allowed dimension exceeded"); - } - if(err != NULL) { - return -1; - } - } + vals[i] = dimension_from_scalar(op); + if (error_converting(vals[i])) { + return -1; } } return nd; @@ -1075,7 +1107,34 @@ PyArray_IntpFromIndexSequence(PyObject *seq, npy_intp *vals, npy_intp maxvals) NPY_NO_EXPORT int PyArray_IntpFromSequence(PyObject *seq, npy_intp *vals, int maxvals) { - return PyArray_IntpFromIndexSequence(seq, vals, (npy_intp)maxvals); + PyObject *seq_obj = NULL; + if (!PyLong_CheckExact(seq) && PySequence_Check(seq)) { + seq_obj = PySequence_Fast(seq, + "expected a sequence of integers or a single integer"); + if (seq_obj == NULL) { + /* continue attempting to parse as a single integer. */ + PyErr_Clear(); + } + } + + if (seq_obj == NULL) { + vals[0] = dimension_from_scalar(seq); + if (error_converting(vals[0])) { + if (PyErr_ExceptionMatches(PyExc_TypeError)) { + PyErr_Format(PyExc_TypeError, + "expected a sequence of integers or a single " + "integer, got '%.100R'", seq); + } + return -1; + } + return 1; + } + else { + int res; + res = PyArray_IntpFromIndexSequence(seq_obj, vals, (npy_intp)maxvals); + Py_DECREF(seq_obj); + return res; + } } diff --git a/numpy/core/tests/test_conversion_utils.py b/numpy/core/tests/test_conversion_utils.py index d8849ee29..c602eba4b 100644 --- a/numpy/core/tests/test_conversion_utils.py +++ b/numpy/core/tests/test_conversion_utils.py @@ -2,12 +2,13 @@ Tests for numpy/core/src/multiarray/conversion_utils.c """ import re +import sys import pytest import numpy as np import numpy.core._multiarray_tests as mt -from numpy.testing import assert_warns +from numpy.testing import assert_warns, IS_PYPY class StringConverterTestCase: @@ -189,6 +190,8 @@ class TestIntpConverter: with pytest.warns(DeprecationWarning): assert self.conv(None) == () + @pytest.mark.skipif(IS_PYPY and sys.implementation.version <= (7, 3, 8), + reason="PyPy bug in error formatting") def test_float(self): with pytest.raises(TypeError): self.conv(1.0) diff --git a/numpy/core/tests/test_multiarray.py b/numpy/core/tests/test_multiarray.py index d4cc3d753..9ba365211 100644 --- a/numpy/core/tests/test_multiarray.py +++ b/numpy/core/tests/test_multiarray.py @@ -445,6 +445,9 @@ class TestArrayConstruction: def test_array_empty(self): assert_raises(TypeError, np.array) + def test_0d_array_shape(self): + assert np.ones(np.array(3)).shape == (3,) + def test_array_copy_false(self): d = np.array([1, 2, 3]) e = np.array(d, copy=False) @@ -507,6 +510,7 @@ class TestArrayConstruction: else: func(a=3) + class TestAssignment: def test_assignment_broadcasting(self): a = np.arange(6).reshape(2, 3) @@ -3981,6 +3985,41 @@ class TestCAPI: assert_(IsPythonScalar(2.)) assert_(IsPythonScalar("a")) + @pytest.mark.parametrize("converter", + [_multiarray_tests.run_scalar_intp_converter, + _multiarray_tests.run_scalar_intp_from_sequence]) + def test_intp_sequence_converters(self, converter): + # Test simple values (-1 is special for error return paths) + assert converter(10) == (10,) + assert converter(-1) == (-1,) + # A 0-D array looks a bit like a sequence but must take the integer + # path: + assert converter(np.array(123)) == (123,) + # Test simple sequences (intp_from_sequence only supports length 1): + assert converter((10,)) == (10,) + assert converter(np.array([11])) == (11,) + + @pytest.mark.parametrize("converter", + [_multiarray_tests.run_scalar_intp_converter, + _multiarray_tests.run_scalar_intp_from_sequence]) + @pytest.mark.skipif(IS_PYPY and sys.implementation.version <= (7, 3, 8), + reason="PyPy bug in error formatting") + def test_intp_sequence_converters_errors(self, converter): + with pytest.raises(TypeError, + match="expected a sequence of integers or a single integer, "): + converter(object()) + with pytest.raises(TypeError, + match="expected a sequence of integers or a single integer, " + "got '32.0'"): + converter(32.) + with pytest.raises(TypeError, + match="'float' object cannot be interpreted as an integer"): + converter([32.]) + with pytest.raises(ValueError, + match="Maximum allowed dimension"): + # These converters currently convert overflows to a ValueError + converter(2**64) + class TestSubscripting: def test_test_zero_rank(self): diff --git a/numpy/core/tests/test_regression.py b/numpy/core/tests/test_regression.py index e073df376..0ebdfec61 100644 --- a/numpy/core/tests/test_regression.py +++ b/numpy/core/tests/test_regression.py @@ -2310,6 +2310,8 @@ class TestRegression: new_shape = (2, 7, 7, 43826197) assert_raises(ValueError, a.reshape, new_shape) + @pytest.mark.skipif(IS_PYPY and sys.implementation.version <= (7, 3, 8), + reason="PyPy bug in error formatting") def test_invalid_structured_dtypes(self): # gh-2865 # mapping python objects to other dtypes |