BUG: Reset an inherited fill_value in MaskedArray.__new__ as well - #32562
Conversation
There was a problem hiding this comment.
existing release notes shouldn't be edited
There was a problem hiding this comment.
@ngoldbaum suggested this in #32508: making the content identical so that towncrier groups the entries into one. Happy to revert the two existing fragments and keep only the new one if you'd rather they stay untouched.
There was a problem hiding this comment.
@jdavies-st do you mind the combined release note for all three PRs?
There was a problem hiding this comment.
Yes, no problem. That makes a lot of sense, as these 3 PRs are an expanding circle of fixes for the same problem.
There was a problem hiding this comment.
I asked them to do that. It’s fine to edit release notes IMO although we should ping the original author too.
ngoldbaum
left a comment
There was a problem hiding this comment.
Thanks for the followup! I have some feedback below.
| """ | ||
| Like `_check_fill_value`, but return None when `fill_value` cannot be | ||
| represented in `ndtype`, so the caller falls back to the default. | ||
|
|
||
| Used where a fill_value is inherited from another array rather than | ||
| given by the user. A user-given fill_value must still raise. A float | ||
| that overflows an integer dtype does not raise in `_check_fill_value`; | ||
| it only sets the floating point invalid flag, so that is raised here. | ||
|
|
||
| """ |
There was a problem hiding this comment.
this private helper doesn't need a docstring IMO
| fill_value = _check_fill_value_or_default(fill_value, | ||
| _data.dtype) |
There was a problem hiding this comment.
| fill_value = _check_fill_value_or_default(fill_value, | |
| _data.dtype) | |
| fill_value = _check_fill_value_or_default( | |
| fill_value, _data.dtype) |
This is a more readable way to format arguments if you need to break it up onto two lines
| if fill_value is None: | ||
| fill_value = getattr(data, '_fill_value', None) | ||
| if fill_value is not None: | ||
| # Inherited from `data`, which may have a different dtype. |
There was a problem hiding this comment.
IMO this comment isn't needed either because it's obvious from two lines above this came from data. When working on any codebase with an LLM, always be skeptical of whether or not a comment it adds is actually useful to a human or just the LLM thinking out loud in its context window.
| # Update fill_value. | ||
| if fill_value is None: | ||
| fill_value = getattr(data, '_fill_value', None) | ||
| if fill_value is not None: | ||
| # Inherited from `data`, which may have a different dtype. | ||
| fill_value = _check_fill_value_or_default(fill_value, | ||
| _data.dtype) | ||
| # But don't run the check unless we have something to check. | ||
| if fill_value is not None: | ||
| _data._fill_value = _check_fill_value(fill_value, _data.dtype) |
There was a problem hiding this comment.
Your PR sets the errstate and calls _check_fill_value twice on every ma.array/ma.asarray from a masked array with a materialized fill_value, even when the dtype doesn't change.
How about this instead?
| # Update fill_value. | |
| if fill_value is None: | |
| fill_value = getattr(data, '_fill_value', None) | |
| if fill_value is not None: | |
| # Inherited from `data`, which may have a different dtype. | |
| fill_value = _check_fill_value_or_default(fill_value, | |
| _data.dtype) | |
| # But don't run the check unless we have something to check. | |
| if fill_value is not None: | |
| _data._fill_value = _check_fill_value(fill_value, _data.dtype) | |
| # Update fill_value. | |
| if fill_value is not None: | |
| _data._fill_value = _check_fill_value(fill_value, _data.dtype) | |
| else: | |
| fill_value = getattr(data, '_fill_value', None) | |
| if fill_value is not None: | |
| if getattr(data, 'dtype', None) != _data.dtype: | |
| fill_value = _check_fill_value_or_default(fill_value, | |
| _data.dtype) | |
| if fill_value is not None: | |
| _data._fill_value = fill_value | |
| else: | |
| _data._fill_value = _check_fill_value(fill_value, | |
| _data.dtype) |
| with warnings.catch_warnings(): | ||
| warnings.simplefilter("error") | ||
| b = array(a, dtype="int64") | ||
| assert_equal(b.fill_value, default_fill_value(b.dtype)) |
There was a problem hiding this comment.
Could you add two more cases here? A user-supplied fill_value=1e20 in the constructor should still raise TypeError, and an object array with the default '?' fill passed with dtype=float now gets the default instead of raising.
There was a problem hiding this comment.
Added both. The object one reads a.fill_value first, otherwise there's nothing to inherit.
Follow-up to #32508, as suggested there by @ngoldbaum.
MaskedArray.__new__re-checks the fill_value inherited fromdatawithout the fallback that #32423 and #32508 added to_update_from, so an out-of-range value was still kept on this path:The fallback now lives in one helper,
_check_fill_value_or_default, used by both_update_fromand__new__. In__new__it applies only to the fill_value inherited fromdata; afill_valuepassed by the caller still goes through_check_fill_valueunchanged and raises as before.I checked the other
_check_fill_valuecall sites: the remaining ones either take a fill_value given by the user, where raising is correct, or run after_update_fromhas already normalised it. This was the last path where an inherited fill_value could survive a dtype it cannot represent.The release notes for #32423, #32508 and this PR are made identical so that towncrier groups them into one entry.
Testing: one case added next to the ones from #32508, covering the constructor path (no warning, default fill_value). The rest of the
numpy/masuite passes unchanged.AI Disclosure
I used an AI assistant (Claude) for the patch prototype, for checking the other call sites, and for translation and grammar editing of this text. I ran the reproduction and the
numpy/masuite before and after myself.