diff --git a/doc/release/upcoming_changes/22541.expired.rst b/doc/release/upcoming_changes/22541.expired.rst new file mode 100644 index 000000000000..a4c0f7b9227a --- /dev/null +++ b/doc/release/upcoming_changes/22541.expired.rst @@ -0,0 +1,3 @@ +* The ``dtype=`` argument to comparison ufuncs is now applied + correctly. That means that only ``bool`` and ``object`` are valid + values and ``dtype=object`` is enforced. diff --git a/numpy/core/src/umath/dispatching.c b/numpy/core/src/umath/dispatching.c index 79de6c3c85bc..077d56f33bc8 100644 --- a/numpy/core/src/umath/dispatching.c +++ b/numpy/core/src/umath/dispatching.c @@ -667,12 +667,9 @@ legacy_promote_using_legacy_type_resolver(PyUFuncObject *ufunc, Py_DECREF(out_descrs[i]); } /* - * The PyUFunc_SimpleBinaryComparisonTypeResolver has a deprecation - * warning (ignoring `dtype=`) and cannot be cached. - * All datetime ones *should* have a warning, but currently don't, - * but ignore all signature passing also. So they can also - * not be cached, and they mutate the signature which of course is wrong, - * but not doing it would confuse the code later. + * datetime legacy resolvers ignore the signature, which should be + * warn/raise (when used). In such cases, the signature is (incorrectly) + * mutated, and caching is not possible. */ for (int i = 0; i < nargs; i++) { if (signature[i] != NULL && signature[i] != operation_DTypes[i]) { @@ -1042,13 +1039,6 @@ default_ufunc_promoter(PyUFuncObject *ufunc, PyArray_DTypeMeta *op_dtypes[], PyArray_DTypeMeta *signature[], PyArray_DTypeMeta *new_op_dtypes[]) { - if (ufunc->type_resolver == &PyUFunc_SimpleBinaryComparisonTypeResolver - && signature[0] == NULL && signature[1] == NULL - && signature[2] != NULL && signature[2]->type_num != NPY_BOOL) { - /* bail out, this is _only_ to give future/deprecation warning! */ - return -1; - } - /* If nin < 2 promotion is a no-op, so it should not be registered */ assert(ufunc->nin > 1); if (op_dtypes[0] == NULL) { diff --git a/numpy/core/src/umath/ufunc_type_resolution.c b/numpy/core/src/umath/ufunc_type_resolution.c index 94338e0313d9..855ad39130e8 100644 --- a/numpy/core/src/umath/ufunc_type_resolution.c +++ b/numpy/core/src/umath/ufunc_type_resolution.c @@ -382,53 +382,9 @@ PyUFunc_SimpleBinaryComparisonTypeResolver(PyUFuncObject *ufunc, } } else { - PyArray_Descr *descr; - /* - * DEPRECATED 2021-03, NumPy 1.20 - * - * If the type tuple was originally a single element (probably), - * issue a deprecation warning, but otherwise accept it. Since the - * result dtype is always boolean, this is not actually valid unless it - * is `object` (but if there is an object input we already deferred). - * - * TODO: Once this deprecation is gone, the special case for - * `PyUFunc_SimpleBinaryComparisonTypeResolver` in dispatching.c - * can be removed. - */ - if (PyTuple_Check(type_tup) && PyTuple_GET_SIZE(type_tup) == 3 && - PyTuple_GET_ITEM(type_tup, 0) == Py_None && - PyTuple_GET_ITEM(type_tup, 1) == Py_None && - PyArray_DescrCheck(PyTuple_GET_ITEM(type_tup, 2))) { - descr = (PyArray_Descr *)PyTuple_GET_ITEM(type_tup, 2); - if (descr->type_num == NPY_OBJECT) { - if (DEPRECATE_FUTUREWARNING( - "using `dtype=object` (or equivalent signature) will " - "return object arrays in the future also when the " - "inputs do not already have `object` dtype.") < 0) { - return -1; - } - } - else if (descr->type_num != NPY_BOOL) { - if (DEPRECATE( - "using `dtype=` in comparisons is only useful for " - "`dtype=object` (and will do nothing for bool). " - "This operation will fail in the future.") < 0) { - return -1; - } - } - } - else { - /* Usually a failure, but let the default version handle it */ - return PyUFunc_DefaultTypeResolver(ufunc, casting, - operands, type_tup, out_dtypes); - } - - out_dtypes[0] = NPY_DT_CALL_ensure_canonical(descr); - if (out_dtypes[0] == NULL) { - return -1; - } - out_dtypes[1] = out_dtypes[0]; - Py_INCREF(out_dtypes[1]); + /* Usually a failure, but let the default version handle it */ + return PyUFunc_DefaultTypeResolver(ufunc, casting, + operands, type_tup, out_dtypes); } /* Output type is always boolean */ diff --git a/numpy/core/tests/test_deprecations.py b/numpy/core/tests/test_deprecations.py index e09ec27b92fd..b808ffc8d7be 100644 --- a/numpy/core/tests/test_deprecations.py +++ b/numpy/core/tests/test_deprecations.py @@ -1000,31 +1000,6 @@ def test_deprecated(self): self.assert_deprecated(lambda: np.add(1, 2, sig=(np.dtype("l"),))) -class TestComparisonBadDType(_DeprecationTestCase): - # Deprecated 2021-04-01, NumPy 1.21 - message = r"using `dtype=` in comparisons is only useful for" - - def test_deprecated(self): - self.assert_deprecated(lambda: np.equal(1, 1, dtype=np.int64)) - # Not an error only for the transition - self.assert_deprecated(lambda: np.equal(1, 1, sig=(None, None, "l"))) - - def test_not_deprecated(self): - np.equal(True, False, dtype=bool) - np.equal(3, 5, dtype=bool, casting="unsafe") - np.equal([None], [4], dtype=object) - -class TestComparisonBadObjectDType(_DeprecationTestCase): - # Deprecated 2021-04-01, NumPy 1.21 (different branch of the above one) - message = r"using `dtype=object` \(or equivalent signature\) will" - warning_cls = FutureWarning - - def test_deprecated(self): - self.assert_deprecated(lambda: np.equal(1, 1, dtype=object)) - self.assert_deprecated( - lambda: np.equal(1, 1, sig=(None, None, object))) - - class TestCtypesGetter(_DeprecationTestCase): # Deprecated 2021-05-18, Numpy 1.21.0 warning_cls = DeprecationWarning diff --git a/numpy/core/tests/test_umath.py b/numpy/core/tests/test_umath.py index e2e95ec69a64..d98eca792d8b 100644 --- a/numpy/core/tests/test_umath.py +++ b/numpy/core/tests/test_umath.py @@ -338,6 +338,21 @@ def test_error_in_equal_reduce(self): assert_equal(np.equal.reduce(a, dtype=bool), True) assert_raises(TypeError, np.equal.reduce, a) + def test_object_dtype(self): + assert np.equal(1, [1], dtype=object).dtype == object + assert np.equal(1, [1], signature=(None, None, "O")).dtype == object + + def test_object_nonbool_dtype_error(self): + # bool output dtype is fine of course: + assert np.equal(1, [1], dtype=bool).dtype == bool + + # but the following are examples do not have a loop: + with pytest.raises(TypeError, match="No loop matching"): + np.equal(1, 1, dtype=np.int64) + + with pytest.raises(TypeError, match="No loop matching"): + np.equal(1, 1, sig=(None, None, "l")) + class TestAdd: def test_reduce_alignment(self): @@ -1095,7 +1110,7 @@ def test_integer_to_negative_power(self): assert_raises(ValueError, np.power, a, minusone) assert_raises(ValueError, np.power, one, b) assert_raises(ValueError, np.power, one, minusone) - + def test_float_to_inf_power(self): for dt in [np.float32, np.float64]: a = np.array([1, 1, 2, 2, -2, -2, np.inf, -np.inf], dt) @@ -1348,7 +1363,7 @@ def test_sincos_values(self): yf = np.array(y, dtype=dt) assert_equal(np.sin(yf), xf) assert_equal(np.cos(yf), xf) - + with np.errstate(invalid='raise'): for callable in [np.sin, np.cos]: