Repository: astropy/astropy

Issue:
# BUG: incoming deprecation warnings from numpy 2.5 (dev): direct mutations to array shape attributes are deprecated

xref: https://github.com/numpy/numpy/pull/29536

this PR (just merged) breaks about 150 tests throughout astropy. Some deprecations are more easily handled than others, so I'll start with the easy ones to reduce the number of failures so we can streamline the discussion where it's really needed.

upstream reports/PRs:
- https://github.com/brandon-rhodes/python-jplephem/issues/63
- https://github.com/brandon-rhodes/python-jplephem/pull/64

linked PRs:
- #19127 
- #19128 
- #19129 
- #19130 
- #19131 
- #19132 
- #19152
- #19158
- #19182 
- #19184
- #19187

Implementation patch:
diff --git a/astropy/time/core.py b/astropy/time/core.py
index 52cd4d76d507..1e0b13c42a1f 100644
--- a/astropy/time/core.py
+++ b/astropy/time/core.py
@@ -27,6 +27,7 @@
 from astropy.extern import _strptime
 from astropy.units import UnitConversionError
 from astropy.utils import lazyproperty
+from astropy.utils.compat import NUMPY_LT_2_5
 from astropy.utils.data_info import MixinInfo, data_info_factory
 from astropy.utils.decorators import deprecated
 from astropy.utils.exceptions import AstropyDeprecationWarning, AstropyWarning
@@ -925,15 +926,22 @@ def shape(self, shape):
             (self, "location"),
         ):
             val = getattr(obj, attr, None)
-            if val is not None and val.size > 1:
-                try:
+            if val is None or val.size <= 1:
+                continue
+            try:
+                if NUMPY_LT_2_5:
                     val.shape = shape
-                except Exception:
-                    for val2 in reshaped:
-                        val2.shape = oldshape
-                    raise
                 else:
-                    reshaped.append(val)
+                    val._set_shape(shape)
+            except Exception:
+                for val2 in reshaped:
+                    if NUMPY_LT_2_5:
+                        val2.shape = oldshape
+                    else:
+                        val2._set_shape(oldshape)
+                raise
+            else:
+                reshaped.append(val)
 
     def _shaped_like_input(self, value):
         if self.masked:
diff --git a/astropy/utils/masked/core.py b/astropy/utils/masked/core.py
index 5564e74af1b0..1805aca6d850 100644
--- a/astropy/utils/masked/core.py
+++ b/astropy/utils/masked/core.py
@@ -22,6 +22,7 @@
 
 import numpy as np
 
+from astropy.utils.compat import NUMPY_LT_2_5
 from astropy.utils.data_info import ParentDtypeInfo
 from astropy.utils.shapes import NDArrayShapeMethods, ShapedLikeNDArray
 
@@ -743,14 +744,26 @@ def shape(self):
 
     @shape.setter
     def shape(self, shape):
+        return self._set_shape(shape)
+
+    def _set_shape(self, shape):
         old_shape = self.shape
-        self._mask.shape = shape
+        if NUMPY_LT_2_5:
+            self._mask.shape = shape
+        else:
+            self._mask._set_shape(shape)
         # Reshape array proper in try/except just in case some broadcasting
         # or so causes it to fail.
         try:
-            super(MaskedNDArray, type(self)).shape.__set__(self, shape)
+            if NUMPY_LT_2_5:
+                super(MaskedNDArray, type(self)).shape.__set__(self, shape)
+            else:
+                super()._set_shape(shape)
         except Exception as exc:
-            self._mask.shape = old_shape
+            if NUMPY_LT_2_5:
+                self._mask.shape = old_shape
+            else:
+                self._mask._set_shape(old_shape)
             # Given that the mask reshaping succeeded, the only logical
             # reason for an exception is something like a broadcast error in
             # in __array_finalize__, or a different memory ordering between



Test patch:
diff --git a/astropy/time/tests/test_methods.py b/astropy/time/tests/test_methods.py
index 263f8be6718e..e410d44ade0f 100644
--- a/astropy/time/tests/test_methods.py
+++ b/astropy/time/tests/test_methods.py
@@ -11,6 +11,7 @@
 from astropy.time import Time
 from astropy.time.utils import day_frac
 from astropy.utils import iers
+from astropy.utils.compat import NUMPY_LT_2_5
 from astropy.utils.exceptions import AstropyDeprecationWarning
 
 
@@ -314,7 +315,7 @@ def test_shape_setting(self, use_mask):
 
         t0_reshape = self.t0.copy()
         mjd = t0_reshape.mjd  # Creates a cache of the mjd attribute
-        t0_reshape.shape = (5, 2, 5)
+        t0_reshape = np.reshape(t0_reshape, (5, 2, 5))
         assert t0_reshape.shape == (5, 2, 5)
         assert mjd.shape != t0_reshape.mjd.shape  # Cache got cleared
         assert np.all(t0_reshape.jd1 == self.t0._time.jd1.reshape(5, 2, 5))
@@ -322,16 +323,26 @@ def test_shape_setting(self, use_mask):
         assert t0_reshape.location is None
         # But if the shape doesn't work, one should get an error.
         t0_reshape_t = t0_reshape.T
-        with pytest.raises(ValueError):
-            t0_reshape_t.shape = (12,)  # Wrong number of elements.
-        with pytest.raises(AttributeError):
-            t0_reshape_t.shape = (10, 5)  # Cannot be done without copy.
+
+        if NUMPY_LT_2_5:
+            depr_warning_action = "error"
+        else:
+            depr_warning_action = "ignore"
+        with warnings.catch_warnings():
+            warnings.simplefilter(depr_warning_action, DeprecationWarning)
+            with pytest.raises(ValueError):
+                t0_reshape_t.shape = (12,)  # Wrong number of elements.
+            with pytest.raises((AttributeError, ValueError)):
+                # the exact exception type isn't really in our control,
+                # as it ultimately comes from numpy but depends on astropy's
+                # execution flow
+                t0_reshape_t.shape = (10, 5)  # Cannot be done without copy.
         # check no shape was changed.
         assert t0_reshape_t.shape == t0_reshape.T.shape
         assert t0_reshape_t.jd1.shape == t0_reshape.T.shape
         assert t0_reshape_t.jd2.shape == t0_reshape.T.shape
         t1_reshape = self.t1.copy()
-        t1_reshape.shape = (2, 5, 5)
+        t1_reshape = np.reshape(t1_reshape, (2, 5, 5))
         assert t1_reshape.shape == (2, 5, 5)
         assert np.all(t1_reshape.jd1 == self.t1.jd1.reshape(2, 5, 5))
         # location is a single element, so its shape should not change.