Repository: astropy/astropy

Issue:
`wcs_info_str` doesn't play nice with numpy 2.0 reprs
### Description

https://github.com/astropy/astropy/blob/main/astropy/wcs/wcsapi/utils.py#L36

See this example of how the repr is ugly with numpy 2 in this ndcube PR: https://github.com/sunpy/ndcube/pull/729/files#diff-ebd49ed76e6cc53d57fd47500f57f6227378a7c65189a5ead08dad31f42d7127R20-R41

### Expected behavior

_No response_

### How to Reproduce

1. Make a wcs
2. look at output of wcs_info_str


### Versions

_No response_

Discussion:
github-actions[bot]: 👋 Thank you for your draft pull request! Do you know that you can use `[ci skip]` or `[skip ci]` in your commit messages to skip running continuous integration tests until you are ready?
neutrinoceros: Meanwhile, it took me a couple iterations to not break existing tests so maybe I can draw some inspiration from these.
neutrinoceros: Alright, added a (partial) test where it seemed to make sense, let me know if you think it's worth building a more complete one.
mhvk: Thinking a bit more, the current approach does seem best - we should not have to worry about the exact type used in the shape, etc. So, just the suggestion for a quick helper function remains.
neutrinoceros: @mhvk I'm not sure that a helper function would be helpful here as we actually have 3 slightly different situations:
- in one case, we want to use `'None'` as a placeholder if `wcs.array_shape is None`
- then, on the same condition and for the same tuple, we use `'(0,)'` as the placeholder
- later, we use `(None, ..., None)` (with a varying number of `None`s) as a placeholder for `wcs.pixel_bounds`

Trying to fit all this logic in one function wouldn't be much simpler than just implementing 3 in-place solutions, IMHO.
However, in the last case, I think the logic can be simplified, so let me do that now.
neutrinoceros: @Cadair what do you think of this patch ?
neutrinoceros: @Cadair Sorry to insist, but could you review this please ?

Patch:
diff --git a/astropy/modeling/core.py b/astropy/modeling/core.py
index f108f2876d..5459b9a813 100644
--- a/astropy/modeling/core.py
+++ b/astropy/modeling/core.py
@@ -3739,29 +3739,12 @@ class CompoundModel(Model):
                     self._parameters_[new_param_name] = param
                     self._param_names.append(new_param_name)
                     param_map[new_param_name] = (lindex, param_name)
-        self._param_metrics = {}
+        self._param_metrics = defaultdict(dict)
         self._param_map = param_map
         self._param_map_inverse = {v: k for k, v in param_map.items()}
         self._initialize_slices()
         self._param_names = tuple(self._param_names)
 
-    def _initialize_slices(self):
-        param_metrics = self._param_metrics
-        total_size = 0
-
-        for name in self.param_names:
-            param = getattr(self, name)
-            value = param.value
-            param_size = np.size(value)
-            param_shape = np.shape(value)
-            param_slice = slice(total_size, total_size + param_size)
-            param_metrics[name] = {}
-            param_metrics[name]["slice"] = param_slice
-            param_metrics[name]["shape"] = param_shape
-            param_metrics[name]["size"] = param_size
-            total_size += param_size
-        self._parameters = np.empty(total_size, dtype=np.float64)
-
     @staticmethod
     def _recursive_lookup(branch, adict, key):
         if isinstance(branch, CompoundModel):
diff --git a/astropy/wcs/wcsapi/utils.py b/astropy/wcs/wcsapi/utils.py
index 97b6671453..13a3dbb935 100644
--- a/astropy/wcs/wcsapi/utils.py
+++ b/astropy/wcs/wcsapi/utils.py
@@ -36,16 +36,21 @@ def deserialize_class(tpl, construct=True):
 def wcs_info_str(wcs):
     # Overall header
 
+    if wcs.array_shape is None:
+        array_shape = None
+    else:
+        array_shape = tuple(int(n) for n in wcs.array_shape)
+
     s = (
         f"{type(wcs).__name__} Transformation\n\n"
         f"This transformation has {wcs.pixel_n_dim} pixel and {wcs.world_n_dim} "
         "world dimensions\n\n"
-        f"Array shape (Numpy order): {wcs.array_shape}\n\n"
+        f"Array shape (Numpy order): {array_shape}\n\n"
     )
 
     # Pixel dimensions table
 
-    array_shape = wcs.array_shape or (0,)
+    array_shape = array_shape or (0,)
     pixel_shape = wcs.pixel_shape or (None,) * wcs.pixel_n_dim
 
     # Find largest between header size and value length
@@ -60,13 +65,25 @@ def wcs_info_str(wcs):
             'Bounds\n')
     # fmt: on
 
+    if wcs.pixel_bounds is None:
+        pixel_bounds = [None for _ in range(wcs.pixel_n_dim)]
+    else:
+        # converting to scalar arrays and back to Python with np.array(val).item()
+        # guarantees that we end up with Python scalars (int or float) with
+        # simple reprs, while not making any unnecessary type promotion
+        # (e.g. int to float)
+        pixel_bounds = [
+            tuple(np.array(b).item() for b in bounds) for bounds in wcs.pixel_bounds
+        ]
+
     for ipix in range(wcs.pixel_n_dim):
         # fmt: off
         s += (('{0:' + str(pixel_dim_width) + 'g}').format(ipix) + '  ' +
                 ('{0:' + str(pixel_nam_width) + 's}').format(wcs.pixel_axis_names[ipix] or 'None') + '  ' +
                 (" " * 5 + str(None) if pixel_shape[ipix] is None else
                 ('{0:' + str(pixel_siz_width) + 'g}').format(pixel_shape[ipix])) + '  ' +
-                '{:s}'.format(str(None if wcs.pixel_bounds is None else wcs.pixel_bounds[ipix]) + '\n'))
+                f"{pixel_bounds[ipix]}\n"
+              )
         # fmt: on
 
     s += "\n"
diff --git a/docs/changes/wcs/16586.bugfix.rst b/docs/changes/wcs/16586.bugfix.rst
new file mode 100644
index 0000000000..ee480b4a07
--- /dev/null
+++ b/docs/changes/wcs/16586.bugfix.rst
@@ -0,0 +1,2 @@
+Fix a bug where ``wcs_info_str``'s results would look different in numpy 2 VS
+numpy 1.


Test patch:
diff --git a/astropy/wcs/wcsapi/wrappers/tests/test_sliced_wcs.py b/astropy/wcs/wcsapi/wrappers/tests/test_sliced_wcs.py
index 861dbef8b3..2714e8945f 100644
--- a/astropy/wcs/wcsapi/wrappers/tests/test_sliced_wcs.py
+++ b/astropy/wcs/wcsapi/wrappers/tests/test_sliced_wcs.py
@@ -702,6 +702,11 @@ HEADER_SPECTRAL_CUBE_NONE_TYPES = {
 WCS_SPECTRAL_CUBE_NONE_TYPES = WCS(header=HEADER_SPECTRAL_CUBE_NONE_TYPES)
 WCS_SPECTRAL_CUBE_NONE_TYPES.pixel_bounds = [(-1, 11), (-2, 18), (5, 15)]
 
+WCS_SPECTRAL_CUBE_NONE_TYPES_NP = WCS(header=HEADER_SPECTRAL_CUBE_NONE_TYPES)
+WCS_SPECTRAL_CUBE_NONE_TYPES_NP.pixel_bounds = [
+    tuple(np.int64(b) for b in t) for t in WCS_SPECTRAL_CUBE_NONE_TYPES.pixel_bounds
+]
+
 
 EXPECTED_ELLIPSIS_REPR_NONE_TYPES = """
 SlicedLowLevelWCS Transformation
@@ -771,6 +776,10 @@ def test_ellipsis_none_types():
     assert str(wcs) == EXPECTED_ELLIPSIS_REPR_NONE_TYPES.strip()
     assert EXPECTED_ELLIPSIS_REPR_NONE_TYPES.strip() in repr(wcs)
 
+    wcs_np = SlicedLowLevelWCS(WCS_SPECTRAL_CUBE_NONE_TYPES_NP, Ellipsis)
+    assert str(wcs_np) == EXPECTED_ELLIPSIS_REPR_NONE_TYPES.strip()
+    assert EXPECTED_ELLIPSIS_REPR_NONE_TYPES.strip() in repr(wcs)
+
 
 CASES = [
     (slice(None), slice(None), slice(None)),