From 74cb500548f9dc9a3281cb593ab219d34d7b609f Mon Sep 17 00:00:00 2001 From: Romain Hugonnet Date: Fri, 21 Mar 2025 16:57:03 +0100 Subject: [PATCH 1/6] Add NEP50 logic for masked arrays and tests --- numpy/ma/core.py | 98 ++++++++++++++++----- numpy/ma/tests/test_core.py | 164 +++++++++++++++++++++++++++++++++++- 2 files changed, 237 insertions(+), 25 deletions(-) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index 3918bd89130a..bc1adc719e83 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -710,7 +710,7 @@ def get_masked_subclass(*arrays): return rcls -def getdata(a, subok=True): +def getdata(a, subok=True, return_scalar=False): """ Return the data of a masked array as an ndarray. @@ -756,7 +756,10 @@ def getdata(a, subok=True): try: data = a._data except AttributeError: - data = np.array(a, copy=None, subok=subok) + if isinstance(a, (bool, int, float)) and return_scalar: + data = a + else: + data = np.array(a, copy=None, subok=subok) if not subok: return data.view(ndarray) return data @@ -1058,7 +1061,7 @@ def __call__(self, a, b, *args, **kwargs): """ # Get the data, as ndarray - (da, db) = (getdata(a), getdata(b)) + (da, db) = (getdata(a, return_scalar=True), getdata(b, return_scalar=True)) # Get the result with np.errstate(): np.seterr(divide='ignore', invalid='ignore') @@ -1134,7 +1137,7 @@ def outer(self, a, b): Return the function applied to the outer product of a and b. """ - (da, db) = (getdata(a), getdata(b)) + (da, db) = (getdata(a, return_scalar=True), getdata(b, return_scalar=True)) d = self.f.outer(da, db) ma = getmask(a) mb = getmask(b) @@ -1201,7 +1204,7 @@ def __init__(self, dbfunc, domain, fillx=0, filly=0): def __call__(self, a, b, *args, **kwargs): "Execute the call behavior." # Get the data - (da, db) = (getdata(a), getdata(b)) + (da, db) = (getdata(a, return_scalar=True), getdata(b, return_scalar=True)) # Get the result with np.errstate(divide='ignore', invalid='ignore'): result = self.f(da, db, *args, **kwargs) @@ -4429,8 +4432,15 @@ def __iadd__(self, other): else: if m is not nomask: self._mask += m - other_data = getdata(other) - other_data = np.where(self._mask, other_data.dtype.type(0), other_data) + other_data = getdata(other, return_scalar=True) + if isinstance(other_data, (bool, int, float)): + # Trick to raise appropriate error and get output dtype + placehold_arr = np.array([0], dtype=self._data.dtype) + placehold_arr.__iadd__(other_data) + other_dtype = placehold_arr.dtype + else: + other_dtype = other_data.dtype + other_data = np.where(self._mask, other_dtype.type(0), other_data) self._data.__iadd__(other_data) return self @@ -4446,8 +4456,15 @@ def __isub__(self, other): self._mask += m elif m is not nomask: self._mask += m - other_data = getdata(other) - other_data = np.where(self._mask, other_data.dtype.type(0), other_data) + other_data = getdata(other, return_scalar=True) + if isinstance(other_data, (bool, int, float)): + # Trick to raise appropriate error and get output dtype + placehold_arr = np.array([0], dtype=self._data.dtype) + placehold_arr.__iadd__(other_data) + other_dtype = placehold_arr.dtype + else: + other_dtype = other_data.dtype + other_data = np.where(self._mask, other_dtype.type(0), other_data) self._data.__isub__(other_data) return self @@ -4463,8 +4480,15 @@ def __imul__(self, other): self._mask += m elif m is not nomask: self._mask += m - other_data = getdata(other) - other_data = np.where(self._mask, other_data.dtype.type(1), other_data) + other_data = getdata(other, return_scalar=True) + if isinstance(other_data, (bool, int, float)): + # Trick to raise appropriate error and get output dtype + placehold_arr = np.array([0], dtype=self._data.dtype) + placehold_arr.__iadd__(other_data) + other_dtype = placehold_arr.dtype + else: + other_dtype = other_data.dtype + other_data = np.where(self._mask, other_dtype.type(1), other_data) self._data.__imul__(other_data) return self @@ -4473,7 +4497,14 @@ def __idiv__(self, other): Divide self by other in-place. """ - other_data = getdata(other) + other_data = getdata(other, return_scalar=True) + if isinstance(other_data, (bool, int, float)): + # Trick to raise appropriate error and get output dtype + placehold_arr = np.array([0], dtype=self._data.dtype) + placehold_arr.__iadd__(other_data) + other_dtype = placehold_arr.dtype + else: + other_dtype = other_data.dtype dom_mask = _DomainSafeDivide().__call__(self._data, other_data) other_mask = getmask(other) new_mask = mask_or(other_mask, dom_mask) @@ -4481,9 +4512,9 @@ def __idiv__(self, other): if dom_mask.any(): (_, fval) = ufunc_fills[np.divide] other_data = np.where( - dom_mask, other_data.dtype.type(fval), other_data) + dom_mask, other_dtype.type(fval), other_data) self._mask |= new_mask - other_data = np.where(self._mask, other_data.dtype.type(1), other_data) + other_data = np.where(self._mask, other_dtype.type(1), other_data) self._data.__idiv__(other_data) return self @@ -4492,7 +4523,14 @@ def __ifloordiv__(self, other): Floor divide self by other in-place. """ - other_data = getdata(other) + other_data = getdata(other, return_scalar=True) + if isinstance(other_data, (bool, int, float)): + # Trick to raise appropriate error and get output dtype + placehold_arr = np.array([0], dtype=self._data.dtype) + placehold_arr.__iadd__(other_data) + other_dtype = placehold_arr.dtype + else: + other_dtype = other_data.dtype dom_mask = _DomainSafeDivide().__call__(self._data, other_data) other_mask = getmask(other) new_mask = mask_or(other_mask, dom_mask) @@ -4500,9 +4538,9 @@ def __ifloordiv__(self, other): if dom_mask.any(): (_, fval) = ufunc_fills[np.floor_divide] other_data = np.where( - dom_mask, other_data.dtype.type(fval), other_data) + dom_mask, other_dtype.type(fval), other_data) self._mask |= new_mask - other_data = np.where(self._mask, other_data.dtype.type(1), other_data) + other_data = np.where(self._mask, other_dtype.type(1), other_data) self._data.__ifloordiv__(other_data) return self @@ -4511,7 +4549,14 @@ def __itruediv__(self, other): True divide self by other in-place. """ - other_data = getdata(other) + other_data = getdata(other, return_scalar=True) + if isinstance(other_data, (bool, int, float)): + # Trick to raise appropriate error and get output dtype + placehold_arr = np.array([0], dtype=self._data.dtype) + placehold_arr.__iadd__(other_data) + other_dtype = placehold_arr.dtype + else: + other_dtype = other_data.dtype dom_mask = _DomainSafeDivide().__call__(self._data, other_data) other_mask = getmask(other) new_mask = mask_or(other_mask, dom_mask) @@ -4519,9 +4564,9 @@ def __itruediv__(self, other): if dom_mask.any(): (_, fval) = ufunc_fills[np.true_divide] other_data = np.where( - dom_mask, other_data.dtype.type(fval), other_data) + dom_mask, other_dtype.type(fval), other_data) self._mask |= new_mask - other_data = np.where(self._mask, other_data.dtype.type(1), other_data) + other_data = np.where(self._mask, other_dtype.type(1), other_data) self._data.__itruediv__(other_data) return self @@ -4530,8 +4575,15 @@ def __ipow__(self, other): Raise self to the power other, in place. """ - other_data = getdata(other) - other_data = np.where(self._mask, other_data.dtype.type(1), other_data) + other_data = getdata(other, return_scalar=True) + if isinstance(other_data, (bool, int, float)): + # Trick to raise appropriate error and get output dtype + placehold_arr = np.array([0], dtype=self._data.dtype) + placehold_arr.__iadd__(other_data) + other_dtype = placehold_arr.dtype + else: + other_dtype = other_data.dtype + other_data = np.where(self._mask, other_dtype.type(1), other_data) other_mask = getmask(other) with np.errstate(divide='ignore', invalid='ignore'): self._data.__ipow__(other_data) @@ -6271,7 +6323,7 @@ def take(self, indices, axis=None, out=None, mode='raise'): mask=[[False, False], [ True, False]], fill_value=999999) - """ + """ (_data, _mask) = (self._data, self._mask) cls = type(self) # Make sure the indices are not masked diff --git a/numpy/ma/tests/test_core.py b/numpy/ma/tests/test_core.py index ab699e14f7b1..cc4f7e00b626 100644 --- a/numpy/ma/tests/test_core.py +++ b/numpy/ma/tests/test_core.py @@ -1017,8 +1017,8 @@ def test_object_with_array(self): def test_maskedarray_tofile_raises_notimplementederror(self): xm = masked_array([1, 2, 3], mask=[False, True, False]) - # Test case to check the NotImplementedError. - # It is not implemented at this point of time. We can change this in future + # Test case to check the NotImplementedError. + # It is not implemented at this point of time. We can change this in future with temppath(suffix='.npy') as path: with pytest.raises(NotImplementedError): np.save(path, xm) @@ -5789,3 +5789,163 @@ def test_uint_fill_value_and_filled(): # And this ensures things like filled work: np.testing.assert_array_equal( a.filled(), np.array([999999, 1]).astype("uint16"), strict=True) + + +@pytest.mark.skipif(IS_WASM, reason="wasm doesn't have support for fp errors") +def test_nep50_examples(): + """Adapted for masked arrays from numpy ndarray NP50 tests.""" + + res = np.ma.masked_array([1], dtype=np.uint8) + 2 + assert res.dtype == np.uint8 + + res = np.ma.masked_array([1], dtype=np.uint8) + np.int64(1) + assert res.dtype == np.int64 + + res = np.ma.masked_array([1], dtype=np.uint8) + np.ma.masked_array(1, dtype=np.int64) + assert res.dtype == np.int64 + + res = np.ma.masked_array([0.1], dtype=np.float32) == np.float64(0.1) + assert res[0] == False + + res = np.ma.masked_array([0.1], dtype=np.float32) + np.float64(0.1) + assert res.dtype == np.float64 + + res = np.ma.masked_array([1.], dtype=np.float32) + np.int64(3) + assert res.dtype == np.float64 + + +@pytest.mark.parametrize("dtype", np.typecodes["AllInteger"]) +def test_nep50_weak_integers(dtype): + """Adapted for masked arrays from numpy ndarray NP50 tests.""" + + maxint = int(np.iinfo(dtype).max) + + # Array operations are not expected to warn, but should give the same + # result dtype. + res = np.ma.masked_array([100], dtype=dtype) + maxint + assert res.dtype == dtype + + +@pytest.mark.parametrize("dtype", np.typecodes["AllFloat"]) +def test_nep50_weak_integers_with_inexact(dtype): + """Adapted for masked arrays from numpy ndarray NP50 tests.""" + + too_big_int = int(np.finfo(dtype).max) * 2 + + if dtype in "dDG": + # These dtypes currently convert to Python float internally, which + # raises an OverflowError, while the other dtypes overflow to inf. + # NOTE: It may make sense to normalize the behavior! + with pytest.raises(OverflowError): + np.ma.masked_array([1], dtype=dtype) + too_big_int + else: + # NumPy uses (or used) `int -> string -> longdouble` for the + # conversion. But Python may refuse `str(int)` for huge ints. + # In that case, RuntimeWarning would be correct, but conversion + # fails earlier (seems to happen on 32bit linux, possibly only debug). + if dtype in "gG": + try: + str(too_big_int) + except ValueError: + pytest.skip("`huge_int -> string -> longdouble` failed") + + with pytest.warns(RuntimeWarning): + # We force the dtype here, since windows may otherwise pick the + # double instead of the longdouble loop. That leads to slightly + # different results (conversion of the int fails as above). + res = np.add(np.ma.masked_array([1], dtype=dtype), too_big_int, dtype=dtype) + assert res.dtype == dtype + assert res == np.inf + + +def test_nep50_integer_conversion_errors(): + """Adapted for masked arrays from numpy ndarray NP50 tests.""" + + # Implementation for error paths is mostly missing (as of writing) + with pytest.raises(OverflowError, match=".*uint8"): + np.ma.masked_array([1], dtype=np.uint8) + 300 + + +def test_nep50_with_axisconcatenator(): + """Adapted for masked arrays from numpy ndarray NP50 tests.""" + + # Concatenate/r_ does not promote, so this has to error: + with pytest.raises(OverflowError): + np.r_[np.ma.arange(5, dtype=np.int8), 255] + + +@pytest.mark.parametrize("arr", [ + np.ma.ones((100, 100), dtype=np.uint8)[::2], # not trivially iterable + np.ma.ones(20000, dtype=">u4"), # cast and >buffersize + np.ma.ones(100, dtype=">u4"), # fast path compatible with cast +]) +def test_integer_comparison_with_cast(arr): + """Adapted for masked arrays from numpy ndarray NP50 tests.""" + + # Similar to above, but mainly test a few cases that cover the slow path + # the test is limited to unsigned ints and -1 for simplicity. + res = arr >= -1 + assert_array_equal(res.data, np.ones_like(arr, dtype=bool)) + res = arr < -1 + assert_array_equal(res.data, np.zeros_like(arr, dtype=bool)) + + +def create_with_ma_array(sctype, value): + return np.ma.masked_array([value], dtype=sctype) + +@pytest.mark.parametrize("sctype", + [np.int8, np.int16, np.int32, np.int64, + np.uint8, np.uint16, np.uint32, np.uint64]) +@pytest.mark.parametrize("create", [create_with_ma_array]) +def test_oob_creation(sctype, create): + """Adapted for masked arrays from numpy ndarray NP50 tests.""" + + iinfo = np.iinfo(sctype) + + with pytest.raises(OverflowError): + create(sctype, iinfo.min - 1) + + with pytest.raises(OverflowError): + create(sctype, iinfo.max + 1) + + with pytest.raises(OverflowError): + create(sctype, str(iinfo.min - 1)) + + with pytest.raises(OverflowError): + create(sctype, str(iinfo.max + 1)) + + assert create(sctype, iinfo.min) == iinfo.min + assert create(sctype, iinfo.max) == iinfo.max + +@pytest.mark.parametrize("operator", ["__iadd__", "__isub__", "__imul__", + "__ifloordiv__", "__itruediv__", "__ipow__"]) +@pytest.mark.parametrize("scalar", [True, 1, 1.]) +@pytest.mark.parametrize("dtype", [np.uint8, np.int8, np.uint32, np.int32, np.float32, np.float64]) +def test_nep50_inplace_ma(operator, scalar, dtype): + """Check that in-place operations respect NEP50 like ndarrays do.""" + + # Define ndarray, and apply in-place operation with scalar, capturing exceptions + arr = np.array([1], dtype=dtype) + try: + getattr(arr, operator)(scalar) + msg = None + except (OverflowError, np._core._exceptions._UFuncOutputCastingError) as e: + # Skip case where casting error occurs + if isinstance(e, np._core._exceptions._UFuncOutputCastingError): + return + # Otherwise continue to check overflow error of NEP50 + else: + msg = str(e) + + # Define masked array, and apply the same operation with the same scalar + ma_arr = np.ma.masked_array([1], dtype=dtype) + # If overflow error should be raised, + if msg is not None: + with pytest.raises(OverflowError, match=msg): + getattr(ma_arr, operator)(scalar) + # Otherwise, apply in-place operation + else: + getattr(ma_arr, operator)(scalar) + + # Output data types should be the same + assert ma_arr.dtype == arr.dtype From 738bb00a89cb67025771389a6b2a9833856cefdc Mon Sep 17 00:00:00 2001 From: Romain Hugonnet Date: Fri, 21 Mar 2025 17:53:36 +0100 Subject: [PATCH 2/6] Remove space edits from editor in test file --- numpy/ma/tests/test_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numpy/ma/tests/test_core.py b/numpy/ma/tests/test_core.py index cc4f7e00b626..360caca7523e 100644 --- a/numpy/ma/tests/test_core.py +++ b/numpy/ma/tests/test_core.py @@ -1017,7 +1017,7 @@ def test_object_with_array(self): def test_maskedarray_tofile_raises_notimplementederror(self): xm = masked_array([1, 2, 3], mask=[False, True, False]) - # Test case to check the NotImplementedError. + # Test case to check the NotImplementedError. # It is not implemented at this point of time. We can change this in future with temppath(suffix='.npy') as path: with pytest.raises(NotImplementedError): From 3ab5f4dd4b9baa60783138c18f861c25f6183011 Mon Sep 17 00:00:00 2001 From: Romain Hugonnet Date: Fri, 21 Mar 2025 17:54:37 +0100 Subject: [PATCH 3/6] Further fix edited spaces --- numpy/ma/tests/test_core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/numpy/ma/tests/test_core.py b/numpy/ma/tests/test_core.py index 360caca7523e..76fb166115d1 100644 --- a/numpy/ma/tests/test_core.py +++ b/numpy/ma/tests/test_core.py @@ -1017,8 +1017,8 @@ def test_object_with_array(self): def test_maskedarray_tofile_raises_notimplementederror(self): xm = masked_array([1, 2, 3], mask=[False, True, False]) - # Test case to check the NotImplementedError. - # It is not implemented at this point of time. We can change this in future + # Test case to check the NotImplementedError. + # It is not implemented at this point of time. We can change this in future with temppath(suffix='.npy') as path: with pytest.raises(NotImplementedError): np.save(path, xm) From f39ffc76c14a95f8fea34935f7f8ec7c7b5a8510 Mon Sep 17 00:00:00 2001 From: Romain Hugonnet Date: Fri, 21 Mar 2025 17:55:41 +0100 Subject: [PATCH 4/6] Final fix for spaces --- numpy/ma/tests/test_core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/numpy/ma/tests/test_core.py b/numpy/ma/tests/test_core.py index 76fb166115d1..360caca7523e 100644 --- a/numpy/ma/tests/test_core.py +++ b/numpy/ma/tests/test_core.py @@ -1017,8 +1017,8 @@ def test_object_with_array(self): def test_maskedarray_tofile_raises_notimplementederror(self): xm = masked_array([1, 2, 3], mask=[False, True, False]) - # Test case to check the NotImplementedError. - # It is not implemented at this point of time. We can change this in future + # Test case to check the NotImplementedError. + # It is not implemented at this point of time. We can change this in future with temppath(suffix='.npy') as path: with pytest.raises(NotImplementedError): np.save(path, xm) From 901fe80c89699682af91607b1a15e53e044192b0 Mon Sep 17 00:00:00 2001 From: Romain Hugonnet Date: Fri, 4 Apr 2025 22:29:54 +0200 Subject: [PATCH 5/6] Use np.ufunc.resolve_dtypes instead of placeholder operation --- numpy/ma/core.py | 83 ++++++++++++++++--------------------- numpy/ma/tests/test_core.py | 4 +- 2 files changed, 38 insertions(+), 49 deletions(-) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index bc1adc719e83..2faa0c2ecece 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -710,7 +710,7 @@ def get_masked_subclass(*arrays): return rcls -def getdata(a, subok=True, return_scalar=False): +def getdata(a, subok=True, return_pyscalar=False): """ Return the data of a masked array as an ndarray. @@ -724,6 +724,9 @@ def getdata(a, subok=True, return_scalar=False): subok : bool Whether to force the output to be a `pure` ndarray (False) or to return a subclass of ndarray if appropriate (True, default). + return_pyscalar : bool + Whether to force the ouput to remain a Python scalar if the input was + also a Python scalar (True), or to return an array in any case (False, default). See Also -------- @@ -756,7 +759,7 @@ def getdata(a, subok=True, return_scalar=False): try: data = a._data except AttributeError: - if isinstance(a, (bool, int, float)) and return_scalar: + if type(a) in (int, float, complex) and return_pyscalar: data = a else: data = np.array(a, copy=None, subok=subok) @@ -1061,7 +1064,7 @@ def __call__(self, a, b, *args, **kwargs): """ # Get the data, as ndarray - (da, db) = (getdata(a, return_scalar=True), getdata(b, return_scalar=True)) + (da, db) = (getdata(a, return_pyscalar=True), getdata(b, return_pyscalar=True)) # Get the result with np.errstate(): np.seterr(divide='ignore', invalid='ignore') @@ -1137,7 +1140,7 @@ def outer(self, a, b): Return the function applied to the outer product of a and b. """ - (da, db) = (getdata(a, return_scalar=True), getdata(b, return_scalar=True)) + (da, db) = (getdata(a, return_pyscalar=True), getdata(b, return_pyscalar=True)) d = self.f.outer(da, db) ma = getmask(a) mb = getmask(b) @@ -1204,7 +1207,7 @@ def __init__(self, dbfunc, domain, fillx=0, filly=0): def __call__(self, a, b, *args, **kwargs): "Execute the call behavior." # Get the data - (da, db) = (getdata(a, return_scalar=True), getdata(b, return_scalar=True)) + (da, db) = (getdata(a, return_pyscalar=True), getdata(b, return_pyscalar=True)) # Get the result with np.errstate(divide='ignore', invalid='ignore'): result = self.f(da, db, *args, **kwargs) @@ -4432,12 +4435,10 @@ def __iadd__(self, other): else: if m is not nomask: self._mask += m - other_data = getdata(other, return_scalar=True) - if isinstance(other_data, (bool, int, float)): - # Trick to raise appropriate error and get output dtype - placehold_arr = np.array([0], dtype=self._data.dtype) - placehold_arr.__iadd__(other_data) - other_dtype = placehold_arr.dtype + other_data = getdata(other, return_pyscalar=True) + if type(other_data) in (int, float, complex): + other_dtype = np.add.resolve_dtypes( + (self._data.dtype, type(other_data), None), casting="unsafe")[1] else: other_dtype = other_data.dtype other_data = np.where(self._mask, other_dtype.type(0), other_data) @@ -4456,12 +4457,10 @@ def __isub__(self, other): self._mask += m elif m is not nomask: self._mask += m - other_data = getdata(other, return_scalar=True) - if isinstance(other_data, (bool, int, float)): - # Trick to raise appropriate error and get output dtype - placehold_arr = np.array([0], dtype=self._data.dtype) - placehold_arr.__iadd__(other_data) - other_dtype = placehold_arr.dtype + other_data = getdata(other, return_pyscalar=True) + if type(other_data) in (int, float, complex): + other_dtype = np.subtract.resolve_dtypes( + (self._data.dtype, type(other_data), None), casting="unsafe")[1] else: other_dtype = other_data.dtype other_data = np.where(self._mask, other_dtype.type(0), other_data) @@ -4480,12 +4479,10 @@ def __imul__(self, other): self._mask += m elif m is not nomask: self._mask += m - other_data = getdata(other, return_scalar=True) - if isinstance(other_data, (bool, int, float)): - # Trick to raise appropriate error and get output dtype - placehold_arr = np.array([0], dtype=self._data.dtype) - placehold_arr.__iadd__(other_data) - other_dtype = placehold_arr.dtype + other_data = getdata(other, return_pyscalar=True) + if type(other_data) in (int, float, complex): + other_dtype = np.multiply.resolve_dtypes( + (self._data.dtype, type(other_data), None), casting="unsafe")[1] else: other_dtype = other_data.dtype other_data = np.where(self._mask, other_dtype.type(1), other_data) @@ -4497,12 +4494,10 @@ def __idiv__(self, other): Divide self by other in-place. """ - other_data = getdata(other, return_scalar=True) - if isinstance(other_data, (bool, int, float)): - # Trick to raise appropriate error and get output dtype - placehold_arr = np.array([0], dtype=self._data.dtype) - placehold_arr.__iadd__(other_data) - other_dtype = placehold_arr.dtype + other_data = getdata(other, return_pyscalar=True) + if type(other_data) in (int, float, complex): + other_dtype = np.divide.resolve_dtypes( + (self._data.dtype, type(other_data), None), casting="unsafe")[1] else: other_dtype = other_data.dtype dom_mask = _DomainSafeDivide().__call__(self._data, other_data) @@ -4523,12 +4518,10 @@ def __ifloordiv__(self, other): Floor divide self by other in-place. """ - other_data = getdata(other, return_scalar=True) - if isinstance(other_data, (bool, int, float)): - # Trick to raise appropriate error and get output dtype - placehold_arr = np.array([0], dtype=self._data.dtype) - placehold_arr.__iadd__(other_data) - other_dtype = placehold_arr.dtype + other_data = getdata(other, return_pyscalar=True) + if type(other_data) in (int, float, complex): + other_dtype = np.floor_divide.resolve_dtypes( + (self._data.dtype, type(other_data), None), casting="unsafe")[1] else: other_dtype = other_data.dtype dom_mask = _DomainSafeDivide().__call__(self._data, other_data) @@ -4549,12 +4542,10 @@ def __itruediv__(self, other): True divide self by other in-place. """ - other_data = getdata(other, return_scalar=True) - if isinstance(other_data, (bool, int, float)): - # Trick to raise appropriate error and get output dtype - placehold_arr = np.array([0], dtype=self._data.dtype) - placehold_arr.__iadd__(other_data) - other_dtype = placehold_arr.dtype + other_data = getdata(other, return_pyscalar=True) + if type(other_data) in (int, float, complex): + other_dtype = np.true_divide.resolve_dtypes( + (self._data.dtype, type(other_data), None), casting="unsafe")[1] else: other_dtype = other_data.dtype dom_mask = _DomainSafeDivide().__call__(self._data, other_data) @@ -4575,12 +4566,10 @@ def __ipow__(self, other): Raise self to the power other, in place. """ - other_data = getdata(other, return_scalar=True) - if isinstance(other_data, (bool, int, float)): - # Trick to raise appropriate error and get output dtype - placehold_arr = np.array([0], dtype=self._data.dtype) - placehold_arr.__iadd__(other_data) - other_dtype = placehold_arr.dtype + other_data = getdata(other, return_pyscalar=True) + if type(other_data) in (int, float, complex): + other_dtype = np.power.resolve_dtypes( + (self._data.dtype, type(other_data), None), casting="unsafe")[1] else: other_dtype = other_data.dtype other_data = np.where(self._mask, other_dtype.type(1), other_data) diff --git a/numpy/ma/tests/test_core.py b/numpy/ma/tests/test_core.py index 360caca7523e..a0c445b60632 100644 --- a/numpy/ma/tests/test_core.py +++ b/numpy/ma/tests/test_core.py @@ -1017,7 +1017,7 @@ def test_object_with_array(self): def test_maskedarray_tofile_raises_notimplementederror(self): xm = masked_array([1, 2, 3], mask=[False, True, False]) - # Test case to check the NotImplementedError. + # Test case to check the NotImplementedError. # It is not implemented at this point of time. We can change this in future with temppath(suffix='.npy') as path: with pytest.raises(NotImplementedError): @@ -5919,7 +5919,7 @@ def test_oob_creation(sctype, create): @pytest.mark.parametrize("operator", ["__iadd__", "__isub__", "__imul__", "__ifloordiv__", "__itruediv__", "__ipow__"]) -@pytest.mark.parametrize("scalar", [True, 1, 1.]) +@pytest.mark.parametrize("scalar", [1, 1.]) @pytest.mark.parametrize("dtype", [np.uint8, np.int8, np.uint32, np.int32, np.float32, np.float64]) def test_nep50_inplace_ma(operator, scalar, dtype): """Check that in-place operations respect NEP50 like ndarrays do.""" From a77d5c31b78993415027f57ba182e23c48b059d2 Mon Sep 17 00:00:00 2001 From: Romain Hugonnet Date: Fri, 4 Apr 2025 23:16:31 +0200 Subject: [PATCH 6/6] Reverse trailing space removal from automatic editor linter --- numpy/ma/core.py | 2 +- numpy/ma/tests/test_core.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index 2faa0c2ecece..a8332cd2d85b 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -6312,7 +6312,7 @@ def take(self, indices, axis=None, out=None, mode='raise'): mask=[[False, False], [ True, False]], fill_value=999999) - """ + """ (_data, _mask) = (self._data, self._mask) cls = type(self) # Make sure the indices are not masked diff --git a/numpy/ma/tests/test_core.py b/numpy/ma/tests/test_core.py index a0c445b60632..972d98145b73 100644 --- a/numpy/ma/tests/test_core.py +++ b/numpy/ma/tests/test_core.py @@ -1017,8 +1017,8 @@ def test_object_with_array(self): def test_maskedarray_tofile_raises_notimplementederror(self): xm = masked_array([1, 2, 3], mask=[False, True, False]) - # Test case to check the NotImplementedError. - # It is not implemented at this point of time. We can change this in future + # Test case to check the NotImplementedError. + # It is not implemented at this point of time. We can change this in future with temppath(suffix='.npy') as path: with pytest.raises(NotImplementedError): np.save(path, xm)