Repository: pydata/xarray

Issue:
mfdataset - ds.encoding["source"] to retrieve filename not valid key
### What happened?

Looking at the doc https://docs.xarray.dev/en/stable/generated/xarray.open_mfdataset.html 

> preprocess ([callable()](https://docs.python.org/3/library/functions.html#callable), optional) – If provided, call this function on each dataset prior to concatenation. You can find the file-name from which each dataset was loaded in ds.encoding["source"].

I expected to be able to use ds.encoding["source"] in my preprocess function to retrieve the filename. However I get


### What did you expect to happen?

I expected the doc to be correct? unless I missed something trivial.

### Minimal Complete Verifiable Example

```Python
def preprocess_xarray_no_class(ds):
    filename = ds.encoding["source"]
    ds = ds.assign(
        filename=("time"), [filename])
    )  # add new filename variable with time dimension
    return ds

ds = xr.open_mfdataset(
    fileset,
    preprocess=preprocess_xarray_no_class,
    engine='h5netcdf',
    concat_characters=True,
    mask_and_scale=True,
    decode_cf=True,
    decode_times=True,
    use_cftime=True,
    parallel=True,
    decode_coords=True,
    compat="equals",

)
```


### MVCE confirmation

- [X] Minimal example — the example is as focused as reasonably possible to demonstrate the underlying issue in xarray.
- [ ] Complete example — the example is self-contained, including all data and the text of any traceback.
- [ ] 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
...
      1 def preprocess_xarray_no_class(ds):
----> 2     filename = ds.encoding["source"]
      3     ds = ds.assign(
      4         filename=("time",), [filename])
      5     )  # add new filename variable with time dimension

KeyError: 'source'
```


### Anything else we need to know?

_No response_

### Environment

<details>
INSTALLED VERSIONS
------------------
commit: None
python: 3.10.14 | packaged by conda-forge | (main, Mar 20 2024, 12:45:18) [GCC 12.3.0]
python-bits: 64
OS: Linux
OS-release: 6.5.0-1023-oem
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: C.UTF-8
LANG: en_IE.UTF-8
LOCALE: ('en_US', 'UTF-8')
libhdf5: 1.14.2
libnetcdf: 4.9.3-development

xarray: 2024.6.0
pandas: 2.2.2
numpy: 1.26.4
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.3.8
dask: 2024.6.0
distributed: 2024.6.0
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.7
numpy_groupies: 0.11.1
setuptools: 70.0.0
pip: 24.0
conda: None
pytest: 8.2.2
mypy: 1.10.0
IPython: 7.34.0
sphinx: None


</details>


Discussion:
max-sixty: Without knowing much (I generally `ds.reset_encoding()`) it does sound like a good idea!
Illviljan: Shouldn't `_normalize_path` or `_find_absolute_paths` be able to handle this?
keewis: the main use case is indeed to extract additional data, which you'd do immediately after `open_dataset` (after which you could drop the encoding).

> Shouldn't `_normalize_path` or `_find_absolute_paths` be able to handle this?

As far as I can tell, they only convert path-likes to string (which these objects are not, they are file-like, not path-like). Are you suggesting we should change that?
keewis: my impression of that discussion was that we wanted to either return the encoding in a separate object, or somehow remove the encoding after the first operation (i.e. not carry it around). Which would be fine with me, since I would still have access to it.
dcherian: Would a dataset with this in encoding be round tripped without error? Would be good to test that
dcherian: Ah thanks. My mistake m I thought we were sticking in the fsspec object not just the path
keewis: as far as I can tell, we could write anything in that encoding (`fsspec` objects, strings, or other things), and it would simply be ignored.

Patch:
diff --git a/ci/requirements/all-but-dask.yml b/ci/requirements/all-but-dask.yml
index 2f47643c..abf6a886 100644
--- a/ci/requirements/all-but-dask.yml
+++ b/ci/requirements/all-but-dask.yml
@@ -22,12 +22,12 @@ dependencies:
   - netcdf4
   - numba
   - numbagg
-  - numpy
+  - numpy<2
   - packaging
   - pandas
   - pint>=0.22
   - pip
-  - pydap
+  # - pydap
   - pytest
   - pytest-cov
   - pytest-env
diff --git a/ci/requirements/doc.yml b/ci/requirements/doc.yml
index 066d085e..116eee7f 100644
--- a/ci/requirements/doc.yml
+++ b/ci/requirements/doc.yml
@@ -21,7 +21,7 @@ dependencies:
   - nbsphinx
   - netcdf4>=1.5
   - numba
-  - numpy>=1.21
+  - numpy>=1.21,<2
   - packaging>=21.3
   - pandas>=1.4,!=2.1.0
   - pooch
@@ -42,5 +42,6 @@ dependencies:
   - sphinxext-rediraffe
   - zarr>=2.10
   - pip:
+      - sphinxcontrib-mermaid
       # relative to this file. Needs to be editable to be accepted.
       - -e ../..
diff --git a/ci/requirements/environment-windows.yml b/ci/requirements/environment-windows.yml
index 3b2e6dc6..2eedc9b0 100644
--- a/ci/requirements/environment-windows.yml
+++ b/ci/requirements/environment-windows.yml
@@ -23,13 +23,13 @@ dependencies:
   - netcdf4
   - numba
   - numbagg
-  - numpy
+  - numpy<2
   - packaging
   - pandas
   # - pint>=0.22
   - pip
   - pre-commit
-  - pydap
+  # - pydap
   - pytest
   - pytest-cov
   - pytest-env
diff --git a/ci/requirements/environment.yml b/ci/requirements/environment.yml
index 01521e95..317e1fe5 100644
--- a/ci/requirements/environment.yml
+++ b/ci/requirements/environment.yml
@@ -26,7 +26,7 @@ dependencies:
   - numba
   - numbagg
   - numexpr
-  - numpy
+  - numpy<2
   - opt_einsum
   - packaging
   - pandas
@@ -35,7 +35,7 @@ dependencies:
   - pooch
   - pre-commit
   - pyarrow # pandas raises a deprecation warning without this, breaking doctests
-  - pydap
+  # - pydap
   - pytest
   - pytest-cov
   - pytest-env
diff --git a/doc/conf.py b/doc/conf.py
index 80b24445..91bcdf8b 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -59,6 +59,7 @@ except ImportError:
     )
 
 nbsphinx_allow_errors = False
+nbsphinx_requirejs_path = ""
 
 # -- General configuration ------------------------------------------------
 
@@ -68,7 +69,9 @@ nbsphinx_allow_errors = False
 # Add any Sphinx extension module names here, as strings. They can be
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
+
 extensions = [
+    "sphinxcontrib.mermaid",
     "sphinx.ext.autodoc",
     "sphinx.ext.autosummary",
     "sphinx.ext.intersphinx",
@@ -175,6 +178,8 @@ napoleon_type_aliases = {
     "pd.NaT": "~pandas.NaT",
 }
 
+# mermaid config
+mermaid_version = "10.9.1"
 
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ["_templates", sphinx_autosummary_accessors.templates_path]
diff --git a/doc/help-diagram.rst b/doc/help-diagram.rst
new file mode 100644
index 00000000..a42a2f09
--- /dev/null
+++ b/doc/help-diagram.rst
@@ -0,0 +1,75 @@
+Getting Help
+============
+
+Navigating the wealth of resources available for Xarray can be overwhelming.
+We've created this flow chart to help guide you towards the best way to get help, depending on what you're working towards.
+The links to each resource are provided below the diagram.
+Regardless of how you interact with us, we're always thrilled to hear from you!
+
+.. mermaid::
+    :alt: Flowchart illustrating the different ways to access help using or contributing to Xarray.
+
+    flowchart TD
+        intro[Welcome to Xarray! How can we help?]:::quesNodefmt
+        usage(["fa:fa-chalkboard-user Xarray Tutorials
+            fab:fa-readme Xarray Docs
+            fab:fa-google Google/fab:fa-stack-overflow Stack Exchange
+            fa:fa-robot Ask AI/a Language Learning Model (LLM)"]):::ansNodefmt
+        API([fab:fa-readme Xarray Docs
+            fab:fa-readme extension's docs]):::ansNodefmt
+        help([fab:fa-github Xarray Discussions
+            fab:fa-discord Xarray Discord
+            fa:fa-users Xarray Office Hours
+            fa:fa-globe Pangeo Discourse]):::ansNodefmt
+        bug([Report and Propose here:
+            fab:fa-github Xarray Issues]):::ansNodefmt
+        contrib([fa:fa-book-open Xarray Contributor's Guide]):::ansNodefmt
+        pr(["fab:fa-github Pull Request (PR)"]):::ansNodefmt
+        dev([fab:fa-github Comment on your PR
+            fa:fa-users Developer's Meeting]):::ansNodefmt
+        report[Thanks for letting us know!]:::quesNodefmt
+        merged[fa:fa-hands-clapping Your PR was merged.
+            Thanks for contributing to Xarray!]:::quesNodefmt
+
+
+        intro -->|How do I use Xarray?| usage
+        usage -->|"with extensions (like Dask)"| API
+
+        usage -->|I'd like some more help| help
+        intro -->|I found a bug| bug
+        intro -->|I'd like to make a small change| contrib
+        subgraph bugcontrib[Bugs and Contributions]
+            bug
+            contrib
+            bug -->|I just wanted to tell you| report
+            bug<-->|I'd like to fix the bug!| contrib
+            pr -->|my PR was approved| merged
+        end
+
+
+        intro -->|I wish Xarray could...| bug
+
+
+        pr <-->|my PR is quiet| dev
+        contrib -->pr
+
+        classDef quesNodefmt fill:#9DEEF4,stroke:#206C89
+
+        classDef ansNodefmt fill:#FFAA05,stroke:#E37F17
+
+        classDef boxfmt fill:#FFF5ED,stroke:#E37F17
+        class bugcontrib boxfmt
+
+        linkStyle default font-size:20pt,color:#206C89
+
+
+- `Xarray Tutorials <https://tutorial.xarray.dev/>`__
+- `Xarray Docs <https://docs.xarray.dev/en/stable/>`__
+- `Google/Stack Exchange <https://stackoverflow.com/questions/tagged/python-xarray>`__
+- `Xarray Discussions <https://github.com/pydata/xarray/discussions>`__
+- `Xarray Discord <https://discord.com/invite/wEKPCt4PDu>`__
+- `Xarray Office Hours <https://github.com/pydata/xarray/discussions/categories/office-hours>`__
+- `Pangeo Discourse <https://discourse.pangeo.io/>`__
+- `Xarray Issues <https://github.com/pydata/xarray/issues>`__
+- `Xarray Contributors Guide <https://docs.xarray.dev/en/stable/contributing.html>`__
+- `Developer's Meeting <https://docs.xarray.dev/en/stable/developers-meeting.html>`__
diff --git a/doc/index.rst b/doc/index.rst
index 138e9d91..4a5fe4ee 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -14,7 +14,8 @@ efficient, and fun!
 `Releases <https://github.com/pydata/xarray/releases>`__ |
 `Stack Overflow <https://stackoverflow.com/questions/tagged/python-xarray>`__ |
 `Mailing List <https://groups.google.com/g/xarray>`__ |
-`Blog <https://xarray.dev/blog>`__
+`Blog <https://xarray.dev/blog>`__ |
+`Tutorials <https://tutorial.xarray.dev/>`__
 
 
 .. grid:: 1 1 2 2
@@ -65,6 +66,7 @@ efficient, and fun!
    Tutorials & Videos <tutorials-and-videos>
    API Reference <api>
    How do I ... <howdoi>
+   Getting Help <help-diagram>
    Ecosystem <ecosystem>
 
 .. toctree::
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index 51a2c98f..0174e166 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -24,6 +24,8 @@ New Features
 ~~~~~~~~~~~~
 - Allow chunking for arrays with duplicated dimension names (:issue:`8759`, :pull:`9099`).
   By `Martin Raspaud <https://github.com/mraspaud>`_.
+- Extract the source url from fsspec objects (:issue:`9142`, :pull:`8923`).
+  By `Justus Magin <https://github.com/keewis>`_.
 
 Breaking changes
 ~~~~~~~~~~~~~~~~
@@ -35,14 +37,21 @@ 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>`_.
+- Promote floating-point numeric datetimes before decoding (:issue:`9179`, :pull:`9182`).
+  By `Justus Magin <https://github.com/keewis>`_.
 
 
 Documentation
 ~~~~~~~~~~~~~
 
+- Adds a flow-chart diagram to help users navigate help resources (`Discussion #8990 <https://github.com/pydata/xarray/discussions/8990>`_).
+  By `Jessica Scheick <https://github.com/jessicas11>`_.
 - Improvements to Zarr & chunking docs (:pull:`9139`, :pull:`9140`, :pull:`9132`)
   By `Maximilian Roos <https://github.com/max-sixty>`_
 
+
 Internal Changes
 ~~~~~~~~~~~~~~~~
 
diff --git a/xarray/backends/api.py b/xarray/backends/api.py
index 7054c621..521bdf65 100644
--- a/xarray/backends/api.py
+++ b/xarray/backends/api.py
@@ -382,8 +382,11 @@ def _dataset_from_backend_dataset(
     ds.set_close(backend_ds._close)
 
     # Ensure source filename always stored in dataset object
-    if "source" not in ds.encoding and isinstance(filename_or_obj, (str, os.PathLike)):
-        ds.encoding["source"] = _normalize_path(filename_or_obj)
+    if "source" not in ds.encoding:
+        path = getattr(filename_or_obj, "path", filename_or_obj)
+
+        if isinstance(path, (str, os.PathLike)):
+            ds.encoding["source"] = _normalize_path(path)
 
     return ds
 
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/coding/times.py b/xarray/coding/times.py
index 466e847e..34d4f9a2 100644
--- a/xarray/coding/times.py
+++ b/xarray/coding/times.py
@@ -278,6 +278,8 @@ def _decode_datetime_with_pandas(
     # timedelta64 value, and therefore would raise an error in the lines above.
     if flat_num_dates.dtype.kind in "iu":
         flat_num_dates = flat_num_dates.astype(np.int64)
+    elif flat_num_dates.dtype.kind in "f":
+        flat_num_dates = flat_num_dates.astype(np.float64)
 
     # Cast input ordinals to integers of nanoseconds because pd.to_timedelta
     # works much faster when dealing with integers (GH 1399).
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_backends.py b/xarray/tests/test_backends.py
index 177700a5..15485dc1 100644
--- a/xarray/tests/test_backends.py
+++ b/xarray/tests/test_backends.py
@@ -5151,6 +5151,21 @@ def test_source_encoding_always_present_with_pathlib() -> None:
             assert ds.encoding["source"] == tmp
 
 
+@requires_h5netcdf
+@requires_fsspec
+def test_source_encoding_always_present_with_fsspec() -> None:
+    import fsspec
+
+    rnddata = np.random.randn(10)
+    original = Dataset({"foo": ("x", rnddata)})
+    with create_tmp_file() as tmp:
+        original.to_netcdf(tmp)
+
+        fs = fsspec.filesystem("file")
+        with fs.open(tmp) as f, open_dataset(f) as ds:
+            assert ds.encoding["source"] == tmp
+
+
 def _assert_no_dates_out_of_range_warning(record):
     undesired_message = "dates out of range"
     for warning in record:
diff --git a/xarray/tests/test_coding_times.py b/xarray/tests/test_coding_times.py
index 09221d66..393f8400 100644
--- a/xarray/tests/test_coding_times.py
+++ b/xarray/tests/test_coding_times.py
@@ -1182,6 +1182,22 @@ def test_decode_0size_datetime(use_cftime):
     np.testing.assert_equal(expected, actual)
 
 
+def test_decode_float_datetime():
+    num_dates = np.array([1867128, 1867134, 1867140], dtype="float32")
+    units = "hours since 1800-01-01"
+    calendar = "standard"
+
+    expected = np.array(
+        ["2013-01-01T00:00:00", "2013-01-01T06:00:00", "2013-01-01T12:00:00"],
+        dtype="datetime64[ns]",
+    )
+
+    actual = decode_cf_datetime(
+        num_dates, units=units, calendar=calendar, use_cftime=False
+    )
+    np.testing.assert_equal(actual, expected)
+
+
 @requires_cftime
 def test_scalar_unit() -> None:
     # test that a scalar units (often NaN when using to_netcdf) does not raise an error
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]})
diff --git a/xarray/tests/test_missing.py b/xarray/tests/test_missing.py
index 3adcc132..da9513a7 100644
--- a/xarray/tests/test_missing.py
+++ b/xarray/tests/test_missing.py
@@ -84,7 +84,7 @@ def make_interpolate_example_data(shape, frac_nan, seed=12345, non_uniform=False
 
     if non_uniform:
         # construct a datetime index that has irregular spacing
-        deltas = pd.to_timedelta(rs.normal(size=shape[0], scale=10), unit="d")
+        deltas = pd.to_timedelta(rs.normal(size=shape[0], scale=10), unit="D")
         coords = {"time": (pd.Timestamp("2000-01-01") + deltas).sort_values()}
     else:
         coords = {"time": pd.date_range("2000-01-01", freq="D", periods=shape[0])}