summaryrefslogtreecommitdiff
path: root/numpy
diff options
context:
space:
mode:
authorSebastian Berg <sebastian@sipsolutions.net>2021-10-07 16:42:34 -0700
committerSebastian Berg <sebastian@sipsolutions.net>2021-10-07 16:52:46 -0700
commitc78ae0bc8fce9dd0cbee21dbca3c01750bea23b1 (patch)
tree54e5f678dc4c502d454fc89ffaa487a92342ed88 /numpy
parentf53077617d5d92ab76cc9a01bb55913b1f4fcbe7 (diff)
downloadnumpy-c78ae0bc8fce9dd0cbee21dbca3c01750bea23b1.tar.gz
BUG: Add workaround for missing ufunc error propagation
Some ufuncs (currently `np.power` soon hopefully only some bad user ufunc) may release the GIL but still set Python errors. The best bet we have is attempting to blanket check for errors before returning from the ufunc. This adds a new ufunc (since we should move `np.power` to the new style definition with error return), and tests all relevant ufunc entrypoints. Closes gh-19634
Diffstat (limited to 'numpy')
-rw-r--r--numpy/core/src/umath/_umath_tests.c.src25
-rw-r--r--numpy/core/src/umath/ufunc_object.c15
-rw-r--r--numpy/core/tests/test_umath.py28
3 files changed, 67 insertions, 1 deletions
diff --git a/numpy/core/src/umath/_umath_tests.c.src b/numpy/core/src/umath/_umath_tests.c.src
index ed4c617a4..33d8539d5 100644
--- a/numpy/core/src/umath/_umath_tests.c.src
+++ b/numpy/core/src/umath/_umath_tests.c.src
@@ -58,6 +58,19 @@
*****************************************************************************
*/
+static void
+always_error_loop(
+ char **NPY_UNUSED(args), npy_intp const *NPY_UNUSED(dimensions),
+ npy_intp const *NPY_UNUSED(steps), void *NPY_UNUSED(func))
+{
+ NPY_ALLOW_C_API_DEF
+ NPY_ALLOW_C_API;
+ PyErr_SetString(PyExc_RuntimeError, "How unexpected :)!");
+ NPY_DISABLE_C_API;
+ return;
+}
+
+
char *inner1d_signature = "(i),(i)->()";
/**begin repeat
@@ -348,6 +361,9 @@ defdict = {
*/
+static PyUFuncGenericFunction always_error_functions[] = { always_error_loop };
+static void *always_error_data[] = { (void *)NULL };
+static char always_error_signatures[] = { NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE };
static PyUFuncGenericFunction inner1d_functions[] = { LONG_inner1d, DOUBLE_inner1d };
static void *inner1d_data[] = { (void *)NULL, (void *)NULL };
static char inner1d_signatures[] = { NPY_LONG, NPY_LONG, NPY_LONG, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE };
@@ -375,6 +391,15 @@ static int
addUfuncs(PyObject *dictionary) {
PyObject *f;
+ f = PyUFunc_FromFuncAndData(always_error_functions, always_error_data,
+ always_error_signatures, 1, 2, 1, PyUFunc_None, "always_error",
+ "simply, broken, ufunc that sets an error (but releases the GIL).",
+ 0);
+ if (f == NULL) {
+ return -1;
+ }
+ PyDict_SetItemString(dictionary, "always_error", f);
+ Py_DECREF(f);
f = PyUFunc_FromFuncAndDataAndSignature(inner1d_functions, inner1d_data,
inner1d_signatures, 2, 2, 1, PyUFunc_None, "inner1d",
"inner on the last dimension and broadcast on the rest \n"
diff --git a/numpy/core/src/umath/ufunc_object.c b/numpy/core/src/umath/ufunc_object.c
index 714afb273..42290e8c9 100644
--- a/numpy/core/src/umath/ufunc_object.c
+++ b/numpy/core/src/umath/ufunc_object.c
@@ -1325,6 +1325,14 @@ try_trivial_single_output_loop(PyArrayMethod_Context *context,
NPY_END_THREADS;
NPY_AUXDATA_FREE(auxdata);
+ /*
+ * An error should only be possible if `res != 0` is already set.
+ * But this is not strictly correct for old-style ufuncs (e.g. `power`
+ * released the GIL but manually set an Exception).
+ */
+ if (PyErr_Occurred()) {
+ res = -1;
+ }
if (res == 0 && !(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) {
/* NOTE: We could check float errors even when `res < 0` */
@@ -6162,7 +6170,12 @@ ufunc_at(PyUFuncObject *ufunc, PyObject *args)
Py_XDECREF(array_operands[i]);
}
- if (needs_api && PyErr_Occurred()) {
+ /*
+ * An error should only be possible if needs_api is true, but this is not
+ * strictly correct for old-style ufuncs (e.g. `power` released the GIL
+ * but manually set an Exception).
+ */
+ if (PyErr_Occurred()) {
return NULL;
}
else {
diff --git a/numpy/core/tests/test_umath.py b/numpy/core/tests/test_umath.py
index 4f57c0088..d98afada8 100644
--- a/numpy/core/tests/test_umath.py
+++ b/numpy/core/tests/test_umath.py
@@ -3852,3 +3852,31 @@ def test_outer_exceeds_maxdims():
with assert_raises(ValueError):
np.add.outer(deep, deep)
+def test_bad_legacy_ufunc_silent_errors():
+ # legacy ufuncs can't report errors and NumPy can't check if the GIL
+ # is released. So NumPy has to check after the GIL is released just to
+ # cover all bases. `np.power` uses/used to use this.
+ arr = np.arange(3).astype(np.float64)
+
+ with pytest.raises(RuntimeError, match=r"How unexpected :\)!"):
+ ncu_tests.always_error(arr, arr)
+
+ with pytest.raises(RuntimeError, match=r"How unexpected :\)!"):
+ # not contiguous means the fast-path cannot be taken
+ non_contig = arr.repeat(20).reshape(-1, 6)[:, ::2]
+ ncu_tests.always_error(non_contig, arr)
+
+ with pytest.raises(RuntimeError, match=r"How unexpected :\)!"):
+ ncu_tests.always_error.outer(arr, arr)
+
+ with pytest.raises(RuntimeError, match=r"How unexpected :\)!"):
+ ncu_tests.always_error.reduce(arr)
+
+ with pytest.raises(RuntimeError, match=r"How unexpected :\)!"):
+ ncu_tests.always_error.reduceat(arr, [0, 1])
+
+ with pytest.raises(RuntimeError, match=r"How unexpected :\)!"):
+ ncu_tests.always_error.accumulate(arr)
+
+ with pytest.raises(RuntimeError, match=r"How unexpected :\)!"):
+ ncu_tests.always_error.at(arr, [0, 1, 2], arr)