Repository: pydata/xarray

Issue:
testing.assert_allclose on string dtype gives error with NumPy 2.0.0
### What happened?

This error came up in an [nc2zarr](https://github.com/bcdev/nc2zarr) unit test in a nightly CI run, just after the release of NumPy 2.0.0. [This](https://github.com/bcdev/nc2zarr/blob/4841f39b40e0e4b4de47e6d297d5dacd0d4a2799/tests/test_writer.py#L312) is the test in question, and [here](https://github.com/bcdev/nc2zarr/blob/4841f39b40e0e4b4de47e6d297d5dacd0d4a2799/tests/test_writer.py#L476) is the call to `xarray.testing.allclose` which triggers the error:

```
AttributeError: Module 'numpy._core' has no attribute 'defchararray'
```

### What did you expect to happen?

We expected that `xarray.testing.allclose` would perform as stated rather than producing an error.

### Minimal Complete Verifiable Example

```Python
import numpy as np
import xarray as xr

xr.testing.assert_allclose(
    da := xr.DataArray(np.array("a", dtype="|S1")),
    da.copy()
)
```


### MVCE confirmation

- [X] Minimal example — the example is as focused as reasonably possible to demonstrate the underlying issue in xarray.
- [X] Complete example — the example is self-contained, including all data and the text of any traceback.
- [X] Verifiable example — the example copy & pastes into an IPython prompt or [Binder notebook](https://mybinder.org/v2/gh/pydata/xarray/main?urlpath=lab/tree/doc/examples/blank_template.ipynb), returning the result.
- [X] New issue — a search of GitHub Issues suggests this is not a duplicate.
- [X] Recent environment — the issue occurs with the latest version of xarray and its dependencies.

### Relevant log output

```Python
Traceback (most recent call last):
  File "/home/pont/allclose_error.py", line 6, in <module>
    xr.testing.assert_allclose(
  File "/home/pont/loc/repos/xarray/xarray/testing/assertions.py", line 32, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/home/pont/loc/repos/xarray/xarray/testing/assertions.py", line 282, in assert_allclose
    ) and compat_variable(a.variable, b.variable)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pont/loc/repos/xarray/xarray/testing/assertions.py", line 274, in compat_variable
    return a.dims == b.dims and (a._data is b._data or equiv(a.data, b.data))
                                                       ^^^^^^^^^^^^^^^^^^^^^
  File "/home/pont/loc/repos/xarray/xarray/testing/assertions.py", line 45, in _data_allclose_or_equiv
    arr1 = _decode_string_data(arr1)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pont/loc/repos/xarray/xarray/testing/assertions.py", line 39, in _decode_string_data
    return np.core.defchararray.decode(data, "utf-8", "replace")
           ^^^^^^^^^^^^^^^^^^^^
  File "/home/pont/mambaforge/envs/xrrepo/lib/python3.12/site-packages/numpy/core/__init__.py", line 30, in __getattr__
    attr = getattr(_core, attr_name)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pont/mambaforge/envs/xrrepo/lib/python3.12/site-packages/numpy/_core/__init__.py", line 167, in __getattr__
    raise AttributeError(f"Module {__name__!r} has no attribute {name!r}")
AttributeError: Module 'numpy._core' has no attribute 'defchararray'
```


### Anything else we need to know?

Error appears to be due to a failed import of `defchararray` from a path that's no longer valid in NumPy 2. `numpy._core.defchararray` says "The preferred alias for `defchararray` is `numpy.char`.". Replacing `np.core.defchararray` with `np.char.chararray` in `xarray.testing.assertions` seems to fix it.

### Environment

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.12.4 | packaged by conda-forge | (main, Jun 17 2024, 10:23:07) [GCC 12.3.0]
python-bits: 64
OS: Linux
OS-release: 5.15.0-107-generic
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: en_GB.UTF-8
LOCALE: ('en_GB', 'UTF-8')
libhdf5: 1.14.2
libnetcdf: 4.9.3-development

xarray: 0.1.dev5453+g872c1c5
pandas: 2.2.2
numpy: 2.0.0
scipy: 1.13.1
netCDF4: 1.7.1
pydap: None
h5netcdf: 1.3.0
h5py: 3.11.0
zarr: 2.18.2
cftime: 1.6.4
nc_time_axis: 1.4.1
iris: None
bottleneck: 1.4.0
dask: 2024.6.2
distributed: 2024.6.2
matplotlib: 3.9.0
cartopy: None
seaborn: 0.13.2
numbagg: 0.8.1
fsspec: 2024.6.0
cupy: None
pint: None
sparse: None
flox: 0.9.8
numpy_groupies: 0.11.1
setuptools: 70.1.0
pip: 24.0
conda: None
pytest: 8.2.2
mypy: 1.10.0
IPython: 8.25.0
sphinx: None
</details>


Discussion:
welcome[bot]: Thank you for opening this pull request! It may take us a few days to respond here, so thank you for being patient.
If you have questions, some answers may be found in our [contributing guidelines](http://docs.xarray.dev/en/stable/contributing.html).

pont-us: > You might also want to add a note to `whats-new.rst`

I didn't think this would qualify as a "notable" bug fix, but glad to oblige :)
pont-us: From a quick look, it appears that the failing Doctests are due to other, unrelated NumPy 2 changes (mainly [NEP 51](https://numpy.org/neps/nep-0051-scalar-representation.html)).
keewis: yep, this is also fails on `main`, so you can ignore these failing tests.

Patch:
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index c3383a56..97631b4c 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -35,6 +35,8 @@ Deprecations
 
 Bug fixes
 ~~~~~~~~~
+- Make :py:func:`testing.assert_allclose` work with numpy 2.0 (:issue:`9165`, :pull:`9166`).
+  By `Pontus Lurcock <https://github.com/pont-us>`_.
 
 
 Documentation
diff --git a/xarray/backends/file_manager.py b/xarray/backends/file_manager.py
index df901f9a..86d84f53 100644
--- a/xarray/backends/file_manager.py
+++ b/xarray/backends/file_manager.py
@@ -63,7 +63,7 @@ class CachingFileManager(FileManager):
     FileManager.close(), which ensures that closed files are removed from the
     cache as well.
 
-    Example usage:
+    Example usage::
 
         manager = FileManager(open, 'example.txt', mode='w')
         f = manager.acquire()
@@ -71,7 +71,7 @@ class CachingFileManager(FileManager):
         manager.close()  # ensures file is closed
 
     Note that as long as previous files are still cached, acquiring a file
-    multiple times from the same FileManager is essentially free:
+    multiple times from the same FileManager is essentially free::
 
         f1 = manager.acquire()
         f2 = manager.acquire()
diff --git a/xarray/core/datatree.py b/xarray/core/datatree.py
index 4e4d3088..c923ca2e 100644
--- a/xarray/core/datatree.py
+++ b/xarray/core/datatree.py
@@ -1314,11 +1314,12 @@ class DataTree(
         ...     }
         ... )
         >>> dt.match("*/B")
-        DataTree('None', parent=None)
-        ├── DataTree('a')
-        │   └── DataTree('B')
-        └── DataTree('b')
-            └── DataTree('B')
+        <xarray.DataTree>
+        Group: /
+        ├── Group: /a
+        │   └── Group: /a/B
+        └── Group: /b
+            └── Group: /b/B
         """
         matching_nodes = {
             node.path: node.ds
diff --git a/xarray/core/datatree_render.py b/xarray/core/datatree_render.py
index d0690714..f10f2540 100644
--- a/xarray/core/datatree_render.py
+++ b/xarray/core/datatree_render.py
@@ -57,11 +57,12 @@ class ContStyle(AbstractStyle):
         >>> s0a = DataTree(name="sub0A", parent=s0)
         >>> s1 = DataTree(name="sub1", parent=root)
         >>> print(RenderDataTree(root))
-        DataTree('root', parent=None)
-        ├── DataTree('sub0')
-        │   ├── DataTree('sub0B')
-        │   └── DataTree('sub0A')
-        └── DataTree('sub1')
+        <xarray.DataTree 'root'>
+        Group: /
+        ├── Group: /sub0
+        │   ├── Group: /sub0/sub0B
+        │   └── Group: /sub0/sub0A
+        └── Group: /sub1
         """
         super().__init__("\u2502   ", "\u251c\u2500\u2500 ", "\u2514\u2500\u2500 ")
 
diff --git a/xarray/core/formatting.py b/xarray/core/formatting.py
index ad65a44d..c15df34b 100644
--- a/xarray/core/formatting.py
+++ b/xarray/core/formatting.py
@@ -1023,20 +1023,21 @@ def diff_datatree_repr(a: DataTree, b: DataTree, compat):
 
 def _single_node_repr(node: DataTree) -> str:
     """Information about this node, not including its relationships to other nodes."""
-    node_info = f"DataTree('{node.name}')"
-
     if node.has_data or node.has_attrs:
         ds_info = "\n" + repr(node.ds)
     else:
         ds_info = ""
-    return node_info + ds_info
+    return f"Group: {node.path}{ds_info}"
 
 
 def datatree_repr(dt: DataTree):
     """A printable representation of the structure of this entire tree."""
     renderer = RenderDataTree(dt)
 
-    lines = []
+    name_info = "" if dt.name is None else f" {dt.name!r}"
+    header = f"<xarray.DataTree{name_info}>"
+
+    lines = [header]
     for pre, fill, node in renderer:
         node_repr = _single_node_repr(node)
 
@@ -1051,12 +1052,6 @@ def datatree_repr(dt: DataTree):
                 else:
                     lines.append(f"{fill}{' ' * len(renderer.style.vertical)}{line}")
 
-    # Tack on info about whether or not root node has a parent at the start
-    first_line = lines[0]
-    parent = f'"{dt.parent.name}"' if dt.parent is not None else "None"
-    first_line_with_parent = first_line[:-1] + f", parent={parent})"
-    lines[0] = first_line_with_parent
-
     return "\n".join(lines)
 
 
diff --git a/xarray/core/iterators.py b/xarray/core/iterators.py
index dd5fa7ee..ae748b00 100644
--- a/xarray/core/iterators.py
+++ b/xarray/core/iterators.py
@@ -39,15 +39,16 @@ class LevelOrderIter(Iterator):
     >>> i = DataTree(name="i", parent=g)
     >>> h = DataTree(name="h", parent=i)
     >>> print(f)
-    DataTree('f', parent=None)
-    ├── DataTree('b')
-    │   ├── DataTree('a')
-    │   └── DataTree('d')
-    │       ├── DataTree('c')
-    │       └── DataTree('e')
-    └── DataTree('g')
-        └── DataTree('i')
-            └── DataTree('h')
+    <xarray.DataTree 'f'>
+    Group: /
+    ├── Group: /b
+    │   ├── Group: /b/a
+    │   └── Group: /b/d
+    │       ├── Group: /b/d/c
+    │       └── Group: /b/d/e
+    └── Group: /g
+        └── Group: /g/i
+            └── Group: /g/i/h
     >>> [node.name for node in LevelOrderIter(f)]
     ['f', 'b', 'g', 'a', 'd', 'i', 'c', 'e', 'h']
     >>> [node.name for node in LevelOrderIter(f, maxlevel=3)]


Test patch:
diff --git a/xarray/testing/assertions.py b/xarray/testing/assertions.py
index 69885868..2a4c17e1 100644
--- a/xarray/testing/assertions.py
+++ b/xarray/testing/assertions.py
@@ -36,7 +36,7 @@ def ensure_warnings(func):
 
 def _decode_string_data(data):
     if data.dtype.kind == "S":
-        return np.core.defchararray.decode(data, "utf-8", "replace")
+        return np.char.decode(data, "utf-8", "replace")
     return data
 
 
diff --git a/xarray/tests/test_assertions.py b/xarray/tests/test_assertions.py
index aa0ea46f..20b5e163 100644
--- a/xarray/tests/test_assertions.py
+++ b/xarray/tests/test_assertions.py
@@ -52,6 +52,11 @@ def test_allclose_regression() -> None:
             xr.Dataset({"a": ("x", [0, 2]), "b": ("y", [0, 1])}),
             id="Dataset",
         ),
+        pytest.param(
+            xr.DataArray(np.array("a", dtype="|S1")),
+            xr.DataArray(np.array("b", dtype="|S1")),
+            id="DataArray_with_character_dtype",
+        ),
     ),
 )
 def test_assert_allclose(obj1, obj2) -> None:
diff --git a/xarray/tests/test_datatree.py b/xarray/tests/test_datatree.py
index 58fec20d..b0dc2acc 100644
--- a/xarray/tests/test_datatree.py
+++ b/xarray/tests/test_datatree.py
@@ -623,6 +623,63 @@ class TestAccess:
         dt.sel(dim_0=0)
 
 
+class TestRepr:
+    def test_repr(self):
+        dt: DataTree = DataTree.from_dict(
+            {
+                "/": xr.Dataset(
+                    {"e": (("x",), [1.0, 2.0])},
+                    coords={"x": [2.0, 3.0]},
+                ),
+                "/b": xr.Dataset({"f": (("y",), [3.0])}),
+                "/b/c": xr.Dataset(),
+                "/b/d": xr.Dataset({"g": 4.0}),
+            }
+        )
+
+        result = repr(dt)
+        expected = dedent(
+            """
+            <xarray.DataTree>
+            Group: /
+            │   Dimensions:  (x: 2)
+            │   Coordinates:
+            │     * x        (x) float64 16B 2.0 3.0
+            │   Data variables:
+            │       e        (x) float64 16B 1.0 2.0
+            └── Group: /b
+                │   Dimensions:  (y: 1)
+                │   Dimensions without coordinates: y
+                │   Data variables:
+                │       f        (y) float64 8B 3.0
+                ├── Group: /b/c
+                └── Group: /b/d
+                        Dimensions:  ()
+                        Data variables:
+                            g        float64 8B 4.0
+            """
+        ).strip()
+        assert result == expected
+
+        result = repr(dt.b)
+        expected = dedent(
+            """
+            <xarray.DataTree 'b'>
+            Group: /b
+            │   Dimensions:  (y: 1)
+            │   Dimensions without coordinates: y
+            │   Data variables:
+            │       f        (y) float64 8B 3.0
+            ├── Group: /b/c
+            └── Group: /b/d
+                    Dimensions:  ()
+                    Data variables:
+                        g        float64 8B 4.0
+            """
+        ).strip()
+        assert result == expected
+
+
 class TestRestructuring:
     def test_drop_nodes(self):
         sue = DataTree.from_dict({"Mary": None, "Kate": None, "Ashley": None})
diff --git a/xarray/tests/test_formatting.py b/xarray/tests/test_formatting.py
index b9d5f401..d7a46eea 100644
--- a/xarray/tests/test_formatting.py
+++ b/xarray/tests/test_formatting.py
@@ -555,16 +555,17 @@ class TestFormatting:
 
     def test_datatree_print_empty_node(self):
         dt: DataTree = DataTree(name="root")
-        printout = dt.__str__()
-        assert printout == "DataTree('root', parent=None)"
+        printout = str(dt)
+        assert printout == "<xarray.DataTree 'root'>\nGroup: /"
 
     def test_datatree_print_empty_node_with_attrs(self):
         dat = xr.Dataset(attrs={"note": "has attrs"})
         dt: DataTree = DataTree(name="root", data=dat)
-        printout = dt.__str__()
+        printout = str(dt)
         assert printout == dedent(
             """\
-            DataTree('root', parent=None)
+            <xarray.DataTree 'root'>
+            Group: /
                 Dimensions:  ()
                 Data variables:
                     *empty*
@@ -575,9 +576,10 @@ class TestFormatting:
     def test_datatree_print_node_with_data(self):
         dat = xr.Dataset({"a": [0, 2]})
         dt: DataTree = DataTree(name="root", data=dat)
-        printout = dt.__str__()
+        printout = str(dt)
         expected = [
-            "DataTree('root', parent=None)",
+            "<xarray.DataTree 'root'>",
+            "Group: /",
             "Dimensions",
             "Coordinates",
             "a",
@@ -591,8 +593,8 @@ class TestFormatting:
         dat = xr.Dataset({"a": [0, 2]})
         root: DataTree = DataTree(name="root")
         DataTree(name="results", data=dat, parent=root)
-        printout = root.__str__()
-        assert printout.splitlines()[2].startswith("    ")
+        printout = str(root)
+        assert printout.splitlines()[3].startswith("    ")
 
     def test_datatree_repr_of_node_with_data(self):
         dat = xr.Dataset({"a": [0, 2]})