From f3b669d391cfe660b064af052973b9bff9372dd8 Mon Sep 17 00:00:00 2001 From: Sebastian Berg Date: Wed, 10 Jul 2024 16:52:27 +0200 Subject: [PATCH 1/2] API: Do not consider subclasses for NEP 50 weak promotion This disables remaining checks for subclasses of floats. We only apply the weak rules to literals, and thus just ignore subclasses. --- numpy/_core/src/multiarray/abstractdtypes.h | 17 +++---------- numpy/_core/src/multiarray/scalartypes.c.src | 6 ++--- numpy/_core/src/umath/scalarmath.c.src | 26 +++----------------- numpy/_core/tests/test_scalarmath.py | 19 +++++++++----- 4 files changed, 22 insertions(+), 46 deletions(-) diff --git a/numpy/_core/src/multiarray/abstractdtypes.h b/numpy/_core/src/multiarray/abstractdtypes.h index b4cf1a13f673..7bf8191e6917 100644 --- a/numpy/_core/src/multiarray/abstractdtypes.h +++ b/numpy/_core/src/multiarray/abstractdtypes.h @@ -41,16 +41,7 @@ static inline int npy_mark_tmp_array_if_pyscalar( PyObject *obj, PyArrayObject *arr, PyArray_DTypeMeta **dtype) { - /* - * We check the array dtype for two reasons: First, booleans are - * integer subclasses. Second, an int, float, or complex could have - * a custom DType registered, and then we should use that. - * Further, `np.float64` is a double subclass, so must reject it. - */ - // TODO,NOTE: This function should be changed to do exact long checks - // For NumPy 2.1! - if (PyLong_Check(obj) - && (PyArray_ISINTEGER(arr) || PyArray_ISOBJECT(arr))) { + if (PyLong_CheckExact(obj)) { ((PyArrayObject_fields *)arr)->flags |= NPY_ARRAY_WAS_PYTHON_INT; if (dtype != NULL) { Py_INCREF(&PyArray_PyLongDType); @@ -58,8 +49,7 @@ npy_mark_tmp_array_if_pyscalar( } return 1; } - else if (PyFloat_Check(obj) && !PyArray_IsScalar(obj, Double) - && PyArray_TYPE(arr) == NPY_DOUBLE) { + else if (PyFloat_CheckExact(obj)) { ((PyArrayObject_fields *)arr)->flags |= NPY_ARRAY_WAS_PYTHON_FLOAT; if (dtype != NULL) { Py_INCREF(&PyArray_PyFloatDType); @@ -67,8 +57,7 @@ npy_mark_tmp_array_if_pyscalar( } return 1; } - else if (PyComplex_Check(obj) && !PyArray_IsScalar(obj, CDouble) - && PyArray_TYPE(arr) == NPY_CDOUBLE) { + else if (PyComplex_CheckExact(obj)) { ((PyArrayObject_fields *)arr)->flags |= NPY_ARRAY_WAS_PYTHON_COMPLEX; if (dtype != NULL) { Py_INCREF(&PyArray_PyComplexDType); diff --git a/numpy/_core/src/multiarray/scalartypes.c.src b/numpy/_core/src/multiarray/scalartypes.c.src index 2c0525253cf2..f3f931de33bc 100644 --- a/numpy/_core/src/multiarray/scalartypes.c.src +++ b/numpy/_core/src/multiarray/scalartypes.c.src @@ -191,9 +191,9 @@ find_binary_operation_path( *self_op = NULL; if (PyArray_IsScalar(other, Generic) || - PyLong_Check(other) || - PyFloat_Check(other) || - PyComplex_Check(other) || + PyLong_CheckExact(other) || + PyFloat_CheckExact(other) || + PyComplex_CheckExact(other) || PyBool_Check(other)) { /* * The other operand is ready for the operation already. Must pass on diff --git a/numpy/_core/src/umath/scalarmath.c.src b/numpy/_core/src/umath/scalarmath.c.src index fe492805eae3..d98b343b2d96 100644 --- a/numpy/_core/src/umath/scalarmath.c.src +++ b/numpy/_core/src/umath/scalarmath.c.src @@ -954,15 +954,7 @@ convert_to_@name@(PyObject *value, @type@ *result, npy_bool *may_need_deferring) return CONVERSION_SUCCESS; } - if (PyFloat_Check(value)) { - if (!PyFloat_CheckExact(value)) { - /* A NumPy double is a float subclass, but special. */ - if (PyArray_IsScalar(value, Double)) { - descr = PyArray_DescrFromType(NPY_DOUBLE); - goto numpy_scalar; - } - *may_need_deferring = NPY_TRUE; - } + if (PyFloat_CheckExact(value)) { if (!IS_SAFE(NPY_DOUBLE, NPY_@TYPE@)) { if (get_npy_promotion_state() != NPY_USE_WEAK_PROMOTION) { /* Legacy promotion and weak-and-warn not handled here */ @@ -978,10 +970,7 @@ convert_to_@name@(PyObject *value, @type@ *result, npy_bool *may_need_deferring) return CONVERSION_SUCCESS; } - if (PyLong_Check(value)) { - if (!PyLong_CheckExact(value)) { - *may_need_deferring = NPY_TRUE; - } + if (PyLong_CheckExact(value)) { if (!IS_SAFE(NPY_LONG, NPY_@TYPE@)) { /* * long -> (c)longdouble is safe, so `OTHER_IS_UNKNOWN_OBJECT` will @@ -1009,15 +998,7 @@ convert_to_@name@(PyObject *value, @type@ *result, npy_bool *may_need_deferring) return CONVERSION_SUCCESS; } - if (PyComplex_Check(value)) { - if (!PyComplex_CheckExact(value)) { - /* A NumPy complex double is a float subclass, but special. */ - if (PyArray_IsScalar(value, CDouble)) { - descr = PyArray_DescrFromType(NPY_CDOUBLE); - goto numpy_scalar; - } - *may_need_deferring = NPY_TRUE; - } + if (PyComplex_CheckExact(value)) { if (!IS_SAFE(NPY_CDOUBLE, NPY_@TYPE@)) { if (get_npy_promotion_state() != NPY_USE_WEAK_PROMOTION) { /* Legacy promotion and weak-and-warn not handled here */ @@ -1079,7 +1060,6 @@ convert_to_@name@(PyObject *value, @type@ *result, npy_bool *may_need_deferring) return OTHER_IS_UNKNOWN_OBJECT; } - numpy_scalar: if (descr->typeobj != Py_TYPE(value)) { /* * This is a subclass of a builtin type, we may continue normally, diff --git a/numpy/_core/tests/test_scalarmath.py b/numpy/_core/tests/test_scalarmath.py index cdbb2fad910a..4429e70fe66b 100644 --- a/numpy/_core/tests/test_scalarmath.py +++ b/numpy/_core/tests/test_scalarmath.py @@ -1073,6 +1073,9 @@ def test_longdouble_complex(): @pytest.mark.parametrize("subtype", [float, int, complex, np.float16]) @np._no_nep50_warning() def test_pyscalar_subclasses(subtype, __op__, __rop__, op, cmp): + # This tests that python scalar subclasses behave like a float64 (if they + # don't override it). + # In an earlier version of NEP 50, they behaved like the Python buildins. def op_func(self, other): return __op__ @@ -1095,25 +1098,29 @@ def rop_func(self, other): # When no deferring is indicated, subclasses are handled normally. myt = type("myt", (subtype,), {__rop__: rop_func}) + behaves_like = lambda x: np.array(subtype(x))[()] # Check for float32, as a float subclass float64 may behave differently res = op(myt(1), np.float16(2)) - expected = op(subtype(1), np.float16(2)) + expected = op(behaves_like(1), np.float16(2)) assert res == expected assert type(res) == type(expected) res = op(np.float32(2), myt(1)) - expected = op(np.float32(2), subtype(1)) + expected = op(np.float32(2), behaves_like(1)) assert res == expected assert type(res) == type(expected) - # Same check for longdouble: + # Same check for longdouble (compare via dtype to accept float64 when + # longdouble has the identical size), which is currently not perfectly + # consistent. res = op(myt(1), np.longdouble(2)) - expected = op(subtype(1), np.longdouble(2)) + expected = op(behaves_like(1), np.longdouble(2)) assert res == expected - assert type(res) == type(expected) + assert np.dtype(type(res)) == np.dtype(type(expected)) res = op(np.float32(2), myt(1)) - expected = op(np.longdouble(2), subtype(1)) + expected = op(np.float32(2), behaves_like(1)) assert res == expected + assert np.dtype(type(res)) == np.dtype(type(expected)) def test_truediv_int(): From 1e9291790aeb7e24b877f334b484b995eb59d452 Mon Sep 17 00:00:00 2001 From: Sebastian Berg Date: Thu, 25 Jul 2024 07:21:25 +0200 Subject: [PATCH 2/2] MAINT: Move assignment to (hopefully) avoid warning --- numpy/_core/src/umath/scalarmath.c.src | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/numpy/_core/src/umath/scalarmath.c.src b/numpy/_core/src/umath/scalarmath.c.src index d98b343b2d96..cd28e4405b6d 100644 --- a/numpy/_core/src/umath/scalarmath.c.src +++ b/numpy/_core/src/umath/scalarmath.c.src @@ -1389,7 +1389,8 @@ static PyObject * npy_bool may_need_deferring; conversion_result res = convert_to_@name@( other, &other_val_conv, &may_need_deferring); - other_val = other_val_conv; /* Need a float value */ + /* Actual float cast `other_val` is set below on success. */ + if (res == CONVERSION_ERROR) { return NULL; /* an error occurred (should never happen) */ } @@ -1400,6 +1401,7 @@ static PyObject * case DEFER_TO_OTHER_KNOWN_SCALAR: Py_RETURN_NOTIMPLEMENTED; case CONVERSION_SUCCESS: + other_val = other_val_conv; /* Need a float value */ break; /* successfully extracted value we can proceed */ case OTHER_IS_UNKNOWN_OBJECT: case PROMOTION_REQUIRED: