Repository: astropy/astropy

Issue:
# BUG: avoid deprecated shape mutation APIs in time

### Description
ref #19126

opening as a draft: what I have seem necessary but not sufficient. Will provide more details later

<!-- Optional opt-out -->

- [ ] 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.


Implementation patch:
diff --git a/astropy/time/core.py b/astropy/time/core.py
index 52d476d222da..5037db2b59e4 100644
--- a/astropy/time/core.py
+++ b/astropy/time/core.py
@@ -27,7 +27,7 @@
 from astropy.extern import _strptime
 from astropy.units import UnitConversionError
 from astropy.utils import lazyproperty
-from astropy.utils.compat import COPY_IF_NEEDED
+from astropy.utils.compat import COPY_IF_NEEDED, 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
@@ -926,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 4ad48e26fc8c..8d69d2fde72f 100644
--- a/astropy/utils/masked/core.py
+++ b/astropy/utils/masked/core.py
@@ -22,7 +22,7 @@
 
 import numpy as np
 
-from astropy.utils.compat import COPY_IF_NEEDED, NUMPY_LT_2_0
+from astropy.utils.compat import COPY_IF_NEEDED, NUMPY_LT_2_0, NUMPY_LT_2_5
 from astropy.utils.data_info import ParentDtypeInfo
 from astropy.utils.shapes import NDArrayShapeMethods, ShapedLikeNDArray
 
@@ -744,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 f295a4978b96..8039974b3578 100644
--- a/astropy/time/tests/test_methods.py
+++ b/astropy/time/tests/test_methods.py
@@ -13,6 +13,7 @@
 from astropy.time.utils import day_frac
 from astropy.units.quantity_helper.function_helpers import ARRAY_FUNCTION_ENABLED
 from astropy.utils import iers
+from astropy.utils.compat import NUMPY_LT_2_5
 from astropy.utils.exceptions import AstropyDeprecationWarning
 
 needs_array_function = pytest.mark.xfail(
@@ -320,7 +321,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))
@@ -328,16 +329,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.