Skip to content

Commit ab1b589

Browse files
authored
Merge pull request #32477 from charris/backport-32423
BUG: fix stale fill_value after ufuncs change MaskedArray dtype (#32423)
2 parents 86c83ed + fc5820e commit ab1b589

3 files changed

Lines changed: 76 additions & 3 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
`MaskedArray._fill_value` would become stale when ufuncs that change dtype
2+
left the result holding a fill_value typed for the old dtype. The mismatch
3+
was silent until something later called `_check_fill_value`, such as
4+
`.view()`, and then a `TypeError` would be raised. Now, when the copied
5+
fill_value is no longer valid for the new dtype, fall back to the
6+
default fill_value for that dtype instead of propagating the stale value.
7+
This can now raise a `ComplexWarning` if the fill_value is complex and the
8+
new dtype is real.

‎numpy/ma/core.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3037,7 +3037,13 @@ def _update_from(self, obj):
30373037
_optinfo.update(getattr(obj, '_basedict', {}))
30383038
if not isinstance(obj, MaskedArray):
30393039
_optinfo.update(getattr(obj, '__dict__', {}))
3040-
_dict = {'_fill_value': getattr(obj, '_fill_value', None),
3040+
_fill_value = getattr(obj, '_fill_value', None)
3041+
if _fill_value is not None and getattr(obj, 'dtype', None) != self.dtype:
3042+
try:
3043+
_fill_value = _check_fill_value(_fill_value, self.dtype)
3044+
except (TypeError, ValueError, OverflowError):
3045+
_fill_value = None
3046+
_dict = {'_fill_value': _fill_value,
30413047
'_hardmask': getattr(obj, '_hardmask', False),
30423048
'_sharedmask': getattr(obj, '_sharedmask', False),
30433049
'_isfield': getattr(obj, '_isfield', False),

‎numpy/ma/tests/test_core.py‎

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,8 +1285,11 @@ def test_basic_ufuncs(self):
12851285
assert_equal(np.arctan(z), arctan(zm))
12861286
assert_equal(np.arctan2(x, y), arctan2(xm, ym))
12871287
assert_equal(np.absolute(x), absolute(xm))
1288-
assert_equal(np.angle(x + 1j * y), angle(xm + 1j * ym))
1289-
assert_equal(np.angle(x + 1j * y, deg=True), angle(xm + 1j * ym, deg=True))
1288+
# Complex fill_value becomes real, raising ComplexWarning
1289+
with pytest.warns(np.exceptions.ComplexWarning):
1290+
assert_equal(np.angle(x + 1j * y), angle(xm + 1j * ym))
1291+
with pytest.warns(np.exceptions.ComplexWarning):
1292+
assert_equal(np.angle(x + 1j * y, deg=True), angle(xm + 1j * ym, deg=True))
12901293
assert_equal(np.equal(x, y), equal(xm, ym))
12911294
assert_equal(np.not_equal(x, y), not_equal(xm, ym))
12921295
assert_equal(np.less(x, y), less(xm, ym))
@@ -2625,6 +2628,62 @@ def test_fillvalue_in_view(self):
26252628
y = x.view(dtype=np.int32)
26262629
assert_(y.fill_value == 999999)
26272630

2631+
def test_fillvalue_reset_after_dtype_changing_ufunc(self):
2632+
# Test that a fill_value which no longer matches the dtype of the
2633+
# result of a ufunc (because the ufunc changed the dtype) is reset
2634+
# to the default for the new dtype.
2635+
a = array(['foo', 'bar', 'baz'], mask=False, fill_value='N/A')
2636+
result = np.strings.find(a, 'foo')
2637+
2638+
# np.strings.find returns the platform default int dtype
2639+
assert_(result.dtype.kind == 'i')
2640+
assert_equal(result.fill_value, default_fill_value(result.dtype))
2641+
assert_equal(result.filled().dtype, result.dtype)
2642+
2643+
viewed = result.view(MaskedArray)
2644+
assert_equal(viewed.fill_value, default_fill_value(result.dtype))
2645+
2646+
def test_fillvalue_castable_after_dtype_changing_ufunc(self):
2647+
# Test that fill_values that *can* be cast into the new dtype.
2648+
# The cast value should be kept rather than being reset to the default,
2649+
# and .filled() should not be forced to fall back to `object` dtype.
2650+
a = array(['ab', 'cd'], mask=[0, 1], fill_value='-1')
2651+
result = np.strings.find(a, 'a')
2652+
assert_(result.dtype.kind == 'i')
2653+
assert_equal(result.fill_value, -1)
2654+
assert_equal(result.filled(), [0, -1])
2655+
assert_equal(result.filled().dtype, result.dtype)
2656+
2657+
x = array([1.5, np.nan, 2.5], mask=[0, 0, 1], fill_value=1e20)
2658+
y = np.isnan(x)
2659+
assert_equal(y.dtype, np.dtype(bool))
2660+
assert_equal(y.fill_value, True)
2661+
assert_equal(y.filled(), [False, True, True])
2662+
assert_equal(y.filled().dtype, y.dtype)
2663+
2664+
d = array(np.array(['2020-01-01', '2020-01-02'], 'M8[D]'), mask=[0, 1])
2665+
d.set_fill_value(np.datetime64('NaT', 'D'))
2666+
z = d - np.datetime64('2020-01-01', 'D')
2667+
assert_equal(z.dtype, np.dtype('timedelta64[D]'))
2668+
assert_(np.isnat(z.fill_value))
2669+
assert_equal(z.filled().dtype, z.dtype)
2670+
2671+
# the fill_value's complex dtype is cast into the new real dtype
2672+
c = array([1 + 1j, 2j], mask=[0, 1], fill_value=2 + 3j)
2673+
with pytest.warns(np.exceptions.ComplexWarning):
2674+
r = np.abs(c)
2675+
assert_equal(r.dtype, np.dtype(float))
2676+
assert_equal(r.fill_value, 2.0)
2677+
assert_equal(r.filled().dtype, r.dtype)
2678+
2679+
# a fill_value that overflows the new dtype during the cast
2680+
wide = np.dtype([('id', object)])
2681+
narrow = np.dtype([('id', 'i8')])
2682+
e = array([(1,), (2,)], dtype=wide, mask=[0, 1], fill_value=(10 ** 30,))
2683+
f = e.astype(narrow)
2684+
assert_equal(f.fill_value, default_fill_value(narrow))
2685+
assert_equal(f.filled().dtype, f.dtype)
2686+
26282687
def test_fillvalue_bytes_or_str(self):
26292688
# Test whether fill values work as expected for structured dtypes
26302689
# containing bytes or str. See issue #7259.

0 commit comments

Comments
 (0)