summaryrefslogtreecommitdiff
path: root/numpy
diff options
context:
space:
mode:
Diffstat (limited to 'numpy')
-rw-r--r--numpy/core/src/umath/scalarmath.c.src54
-rw-r--r--numpy/core/tests/test_scalarmath.py31
2 files changed, 34 insertions, 51 deletions
diff --git a/numpy/core/src/umath/scalarmath.c.src b/numpy/core/src/umath/scalarmath.c.src
index 15afaae00..f273cdd91 100644
--- a/numpy/core/src/umath/scalarmath.c.src
+++ b/numpy/core/src/umath/scalarmath.c.src
@@ -657,9 +657,17 @@ static NPY_INLINE int
*
* So we have multiple possible paths:
* - The other scalar is a subclass. In principle *both* inputs could be
- * different subclasses. In this case we should defer, but only if the
- * other subclass does *not* inherit the slot as well.
- * That check is included via `BINOP_IS_FORWARD` (or manually for comparison).
+ * different subclasses. In this case it would make sense to defer, but
+ * Python's `int` does not try this as well, so we do not here:
+ *
+ * class A(int): pass
+ * class B(int):
+ * def __add__(self, other): return "b"
+ * __radd__ = __add__
+ *
+ * A(1) + B(1) # return 2
+ * B(1) + A(1) # return "b"
+ *
* - The other scalar can be converted: All is good, we do the operation
* - The other scalar cannot be converted, there are two possibilities:
* - The reverse should work, so we return NotImplemented to defer.
@@ -757,8 +765,8 @@ typedef enum {
*/
PROMOTION_REQUIRED,
/*
- * The other object may be a subclass, conversion is successful, but we
- * may still need to defer.
+ * The other object may be a subclass, conversion is successful. We do
+ * not special case this as Python's `int` does not either
*/
OTHER_IS_SUBCLASS,
} conversion_result;
@@ -882,6 +890,10 @@ convert_to_@name@(PyObject *value, @type@ *result)
/* Optimize the identical scalar specifically. */
if (PyArray_IsScalar(value, @Name@)) {
*result = PyArrayScalar_VAL(value, @Name@);
+ /*
+ * In principle special, assyemetric, handling could be possible for
+ * subclasses. But in practice even we do not bother later.
+ */
return OTHER_IS_SUBCLASS;
}
@@ -1112,13 +1124,11 @@ static PyObject *
return PyGenericArrType_Type.tp_as_number->nb_@oper@(a,b);
case OTHER_IS_SUBCLASS:
/*
- * Success, but defer if not a forward operator. This implements
- * the asymmetry of `__add__` and `__radd__` for subclassing.
- * (A subclass replacing `__add__`/`__radd__` has precedence.)
+ * Success converting. We _could_ in principle defer in cases
+ * were the other subclass does not inherit the behavior. In
+ * practice not even Python's `int` attempt this, so we also punt.
*/
- if (is_forward && BINOP_IS_FORWARD(a, b, nb_@oper@, @name@_@oper@)) {
- Py_RETURN_NOTIMPLEMENTED;
- }
+ break;
}
#if @fperr@
@@ -1278,13 +1288,11 @@ static PyObject *
return PyGenericArrType_Type.tp_as_number->nb_power(a, b, modulo);
case OTHER_IS_SUBCLASS:
/*
- * Success, but defer if not a forward operator. This implements
- * the asymmetry of `__add__` and `__radd__` for subclassing.
- * (A subclass replacing `__add__`/`__radd__` has precedence.)
+ * Success converting. We _could_ in principle defer in cases
+ * were the other subclass does not inherit the behavior. In
+ * practice not even Python's `int` attempt this, so we also punt.
*/
- if (is_forward && BINOP_IS_FORWARD(a, b, nb_power, @name@_power)) {
- Py_RETURN_NOTIMPLEMENTED;
- }
+ break;
}
#if !@isint@
@@ -1653,14 +1661,12 @@ static PyObject*
return PyGenericArrType_Type.tp_richcompare(self, other, cmp_op);
case OTHER_IS_SUBCLASS:
/*
- * Success, but defer if not a forward operator. This implements
- * the asymmetry of `__add__` and `__radd__` for subclassing.
- * (A subclass replacing `__add__`/`__radd__` has precedence. It
- * also means that such a subclass has to always take charge.)
+ * Success converting. We _could_ in principle defer in cases
+ * were the other subclass does not inherit the behavior. In
+ * practice not even Python's `int` attempt this, so we also punt.
+ * (This is also even trickier for richcompare, though.)
*/
- if ((void *)Py_TYPE(other)->tp_richcompare != (void *)@name@_richcompare) {
- Py_RETURN_NOTIMPLEMENTED;
- }
+ break;
}
arg1 = PyArrayScalar_VAL(self, @Name@);
diff --git a/numpy/core/tests/test_scalarmath.py b/numpy/core/tests/test_scalarmath.py
index 527106973..a0de788a7 100644
--- a/numpy/core/tests/test_scalarmath.py
+++ b/numpy/core/tests/test_scalarmath.py
@@ -950,9 +950,6 @@ ops_with_names = [
@pytest.mark.parametrize(["__op__", "__rop__", "op", "cmp"], ops_with_names)
@pytest.mark.parametrize("sctype", [np.float32, np.float64, np.longdouble])
-@pytest.mark.xfail(IS_PYPY,
- reason="PyPy does not replace `tp_richcompare` for subclasses so"
- "our we do not realize when deferring should be preferred.")
def test_subclass_deferral(sctype, __op__, __rop__, op, cmp):
"""
This test covers scalar subclass deferral. Note that this is exceedingly
@@ -980,36 +977,16 @@ def test_subclass_deferral(sctype, __op__, __rop__, op, cmp):
return __rop__
myf_op = type("myf_op", (sctype,), {__op__: op_func, __rop__: rop_func})
- # Note: __rop__ first, so defer "wins" if __rop__ == __op__
- myf_defer = type("myf_defer", (sctype,), {__rop__: rop_func, __op__: defer})
# inheritance has to override, or this is correctly lost:
res = op(myf_simple1(1), myf_simple2(2))
assert type(res) == sctype or type(res) == np.bool_
- assert myf_simple1(1) + myf_simple2(2) == 3
+ assert op(myf_simple1(1), myf_simple2(2)) == op(1, 2) # inherited
- # When a subclass overrides though, we should always give it a chance,
- # even for the __rop__!
+ # Two independent subclasses do not really define an order. This could
+ # be attempted, but we do not since Python's `int` does neither:
assert op(myf_op(1), myf_simple1(2)) == __op__
- assert op(myf_simple1(1), myf_op(2)) == __rop__
-
- # If the other class defers back to NumPy, we should attempt the operation
- # NOTE: this path goes through the generic array/ufunc path!
- # Note that for comparisons, the class is badly behaved since we cannot
- # know if an operation is "reversed".
- if cmp and op in [operator.eq, operator.ne]:
- # Undefined equality leads to False and True
- assert op(myf_defer(1), myf_simple1(2)) == op(None, 1)
- # since __op__ == __rop__ here, it gives the same result:
- assert op(myf_simple1(2), myf_defer(1)) == op(None, 1)
- elif cmp:
- with pytest.raises(TypeError):
- op(myf_defer(1), myf_simple1(2))
- assert op(myf_simple1(2), myf_defer(1)) == __rop__
- else:
- res = op(myf_defer(1), myf_simple1(2))
- assert type(res) == sctype
- assert op(myf_simple1(2), myf_defer(1)) == __rop__
+ assert op(myf_simple1(1), myf_op(2)) == op(1, 2) # inherited
@pytest.mark.parametrize(["__op__", "__rop__", "op", "cmp"], ops_with_names)