Skip to content

Conversation

@ReemHamraz
Copy link

MAINT: Update function helpers for NumPy 2.4 parity and import resilience

Description

This pull request addresses compatibility issues with the upcoming NumPy 2.4 release and refactors the internal dispatching registry in astropy.units to be more resilient.

Previously, FUNCTION_HELPERS used NumPy function objects as dictionary keys. This caused import-time failures if a function was removed or renamed in a newer version of NumPy. By switching to string-based keys, we ensure that astropy.units remains importable even if the underlying NumPy environment changes. Additionally, this PR updates function signatures to include the positional-only argument marker (/) where required for parity with NumPy 2.4.

Summary of Changes:

astropy.units

  • Registry Refactor: Updated FUNCTION_HELPERS in quantity_helper/function_helpers.py to use function names (strings) as keys (e.g., np.sin -> "sin").
  • Dispatcher Update: Modified Quantity.__array_function__ in quantity.py to perform lookups using function.__name__.
  • NumPy 2.4 Parity: Updated method signatures for reductions (like sum, mean, std) in quantity.py to include the positional-only / marker.

astropy.utils.masked

  • Signature Updates: Added positional-only markers to array2string, array_str, ediff1d, in1d, isin, and setdiff1d in function_helpers.py.
  • Code Maintenance: Introduced internal _impl functions to reduce logic duplication across NumPy version branches.
  • Version Control: Added NUMPY_LT_2_4 constant in core.py for cleaner conditional logic.

Scope of Future Changes:
This PR focuses on the most critical function helpers. Further changes might be needed in astropy/utils/masked/structured.py once the CI results identify any remaining signature mismatches in structured array handling.

Fixes #19093

  • By checking this box, the PR author has requested that maintainers do NOT use the "Squash and Merge" button. Maintainers should respect this when possible; however, the final decision is at the discretion of the maintainer that merges the PR.

This commit addresses positional-only argument changes in NumPy 2.4 and handles the removal of the 'style' parameter in array2string. Functions updated include array2string, array_str, ediff1d, in1d, isin, and setdiff1d.
@github-actions
Copy link
Contributor

Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.

  • Do the proposed changes actually accomplish desired goals?
  • Do the proposed changes follow the Astropy coding guidelines?
  • Are tests added/updated as required? If so, do they follow the Astropy testing guidelines?
  • Are docs added/updated as required? If so, do they follow the Astropy documentation guidelines?
  • Is rebase and/or squash necessary? If so, please provide the author with appropriate instructions. Also see instructions for rebase and squash.
  • Did the CI pass? If no, are the failures related? If you need to run daily and weekly cron jobs as part of the PR, please apply the "Extra CI" label. Codestyle issues can be fixed by the bot.
  • Is a change log needed? If yes, did the change log check pass? If no, add the "no-changelog-entry-needed" label. If this is a manual backport, use the "skip-changelog-checks" label unless special changelog handling is necessary.
  • Is this a big PR that makes a "What's new?" entry worthwhile and if so, is (1) a "what's new" entry included in this PR and (2) the "whatsnew-needed" label applied?
  • At the time of adding the milestone, if the milestone set requires a backport to release branch(es), apply the appropriate "backport-X.Y.x" label(s) before merge.

@ReemHamraz ReemHamraz marked this pull request as draft December 26, 2025 22:28
@pllim
Copy link
Member

pllim commented Dec 27, 2025

Too much changes. Please look at past PRs.

@pllim pllim closed this Dec 27, 2025
@neutrinoceros
Copy link
Contributor

I understand where you're coming from @pllim , but a bit of meta-context seems needed: I've been talking with @ReemHamraz about how she could get into contributing to astropy. I suggested she took a shot at #19093 and gave her a couple hints how she might do that. This PR is her attempt, but I haven't reviewed it in any aspect yet, so I do expect her work might not be perfect at first, which of course is perfectly okay.
I'll open it back but switch it to draft. I hope that's okay.

@neutrinoceros neutrinoceros reopened this Dec 27, 2025
@neutrinoceros
Copy link
Contributor

(nevermind it was in draft state already)

Copy link
Contributor

@neutrinoceros neutrinoceros left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, this PR updates function signatures to include the positional-only argument marker (/) where required for parity with NumPy 2.4.

I think this is out of scope in this PR and should be conducted separately. Making each PR as narrowly scoped as possible is extremely beneficial in the long term if for any reason a regression is appears to be attached to a specific PR, so we can easily understand what went wrong and make reverting easy. It's also very helpful in reviews: streamlined changes are much easier to understand and tend to be approved more promptly.

The approach seems sound but I did indeed find a handful of out-of-scope changes here. Most of them seem correct but they should still get their own PRs. We'll work that out. I'll now look into CI failures and try to grasp what's not working yet.

Comment on lines +67 to +74
def _get_np_func_name(func):
"""
Safely get the name of a numpy function.
If the function is already a string, return it.
"""
if isinstance(func, str):
return func
return getattr(func, "__name__", str(func))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be preferable to only use static lookups (i.e. func.__name__) and only allow one type (functions) as inputs. This will make callsite much easier to reason about.
Additionally, it might not be sufficient to retrieve the name of a function, as we sometimes also need to carry the name of it's parent(s) namespaces. Not sure how this applies yet, but that's something to keep in mind.

"iscomplexobj", "isrealobj",
"shares_memory", "may_share_memory",
"apply_along_axis", "take_along_axis", "put_along_axis",
"linalg.cond", "linalg.multi_dot",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a lot of churn, but for the record, this is exactly what I proposed we do.

return func
return getattr(func, "__name__", str(func))

def _interpret_tol(tol, unit):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if I'm following correctly, this is a pre-existing function but you moved its definition ? If I'm correct, please revert this bit and simply import from where it's defined.

# currently return NotImplementedError.
# TODO: move latter to UNSUPPORTED? Would raise TypeError instead.
SUBCLASS_SAFE_FUNCTIONS |= {np.ediff1d}
SUBCLASS_SAFE_FUNCTIONS |= {"ediff1d", "interp", "interp2d", "interp3d"}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this might be correct, but it's still out of scope here. SUBCLASS_SAFE_FUNCTIONS is nice because it's less work to support, but we still need minimal tests for each function that lands here, so we should just leave it be for this PR.

Comment on lines +237 to +238
# If no 'helps' provided, use the name of the function itself
helps = (f.__name__,)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks incorrect (and possibly accidental ?)

@function_helper(helps="linalg.pinv")
def pinv(a, rcond=1e-15, *args, **kwargs):
rcond = _interpret_tol(rcond, a.unit)
rcond = _interpret_tol(rcond, dimensionless_unscaled)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what this is about, but I have a hunch that it's (you guessed it) out of scope

Comment on lines +1634 to +1638
try:
target_unit = next(iter(arr.unit.values()))
except (StopIteration, AttributeError):
# Fallback if the unit isn't structured or is empty
target_unit = arr.unit
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like a bugfix, but it needs its own test(s) -> separate PR

UFUNC_HELPERS[np_umath._ones_like] = helper__ones_like
UFUNC_HELPERS[np.modf] = helper_modf
UFUNC_HELPERS[np.frexp] = helper_frexp
UFUNC_HELPERS[_get_np_func_name(np.sqrt)] = helper_sqrt
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't this be exactly equivalent (and simpler ?) ?

Suggested change
UFUNC_HELPERS[_get_np_func_name(np.sqrt)] = helper_sqrt
UFUNC_HELPERS["sqrt"] = helper_sqrt

Comment on lines +745 to +754
from astropy.units import StructuredUnit
if isinstance(unit, StructuredUnit):
from astropy.units.structured import StructuredQuantity
return StructuredQuantity, True


from astropy.units import LogUnit
if isinstance(unit, LogUnit):
from astropy.units import LogQuantity
return LogQuantity, True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure where that's coming from, but the docstring indicates that changes on the return value should be done as overrides in subclasses. What motivated this change ?

Comment on lines +1609 to +1616
if not NUMPY_LT_2_4:
@dispatched_function
def setdiff1d(ar1, ar2, /, assume_unique=False):
return _setdiff1d_impl(ar1, ar2, assume_unique)
else:
@dispatched_function
def setdiff1d(ar1, ar2, assume_unique=False):
return _setdiff1d_impl(ar1, ar2, assume_unique)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separate PR again :)

@neutrinoceros
Copy link
Contributor

ImportError: cannot import name 'LogUnit' from 'astropy.units.core'

okay at least this is easy to reproduce. A general advice here would be to run tests locally before you push to CI so you can iterate quickly on trivial mistakes like this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improving forward compatibility in for ufunc/array_func registration

3 participants