Repository: pydata/xarray

Issue:
⚠️ Nightly upstream-dev CI failed ⚠️
[Workflow Run URL](https://github.com/pydata/xarray/actions/runs/459982031)
<details><summary>Python 3.8 Test Summary Info</summary>

```
FAILED xarray/tests/test_dask.py::TestToDaskDataFrame::test_to_dask_dataframe
FAILED xarray/tests/test_dask.py::TestToDaskDataFrame::test_to_dask_dataframe_2D
FAILED xarray/tests/test_dask.py::TestToDaskDataFrame::test_to_dask_dataframe_coordinates
```

</details>

Discussion:
max-sixty: Thanks @keewis 

How should we think about changing the test code vs xfailing?

IIUC the current test code should work, but doesn't because of the pandas issue. Would we expect to revert this after pandas fixes its issue?
keewis: > How should we think about changing the test code vs xfailing?

that's a good point. I'm really not sure: in this case we don't change the behavior of the test, but in general it would probably be better to xfail.

Patch:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 15c97190..91e55284 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,7 +1,7 @@
 <!-- Feel free to remove check-list items aren't relevant to your change -->
 
- - [ ] Closes #xxxx
- - [ ] Tests added
- - [ ] Passes `isort . && black . && mypy . && flake8`
- - [ ] User visible changes (including notable bug fixes) are documented in `whats-new.rst`
- - [ ] New functions/methods are listed in `api.rst`
+- [ ] Closes #xxxx
+- [ ] Tests added
+- [ ] Passes `isort . && black . && mypy . && flake8`
+- [ ] User visible changes (including notable bug fixes) are documented in `whats-new.rst`
+- [ ] New functions/methods are listed in `api.rst`
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index 151af2de..e8209dc1 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -22,9 +22,15 @@ v0.16.3 (unreleased)
 
 Breaking changes
 ~~~~~~~~~~~~~~~~
+- As a result of :pull:`4684` the default units encoding for
+  datetime-like values (``np.datetime64[ns]`` or ``cftime.datetime``) will now
+  always be set such that ``int64`` values can be used.  In the past, no units
+  finer than "seconds" were chosen, which would sometimes mean that ``float64``
+  values were required, which would lead to inaccurate I/O round-trips.
 - remove deprecated ``autoclose`` kwargs from :py:func:`open_dataset` (:pull: `4725`).
   By `Aureliana Barghini <https://github.com/aurghs>`_
 
+
 New Features
 ~~~~~~~~~~~~
 
@@ -34,6 +40,12 @@ Bug fixes
 
 - :py:meth:`DataArray.resample` and :py:meth:`Dataset.resample` do not trigger computations anymore if :py:meth:`Dataset.weighted` or :py:meth:`DataArray.weighted` are applied (:issue:`4625`, :pull:`4668`). By `Julius Busecke <https://github.com/jbusecke>`_.
 - :py:func:`merge` with ``combine_attrs='override'`` makes a copy of the attrs (:issue:`4627`).
+- By default, when possible, xarray will now always use values of type ``int64`` when encoding
+  and decoding ``numpy.datetime64[ns]`` datetimes.  This ensures that maximum
+  precision and accuracy are maintained in the round-tripping process
+  (:issue:`4045`, :pull:`4684`). It also enables encoding and decoding standard calendar
+  dates with time units of nanoseconds (:pull:`4400`). By `Spencer Clark
+  <https://github.com/spencerkclark>`_ and `Mark Harfouche <http://github.com/hmaarrfk>`_.
 - :py:meth:`DataArray.astype`, :py:meth:`Dataset.astype` and :py:meth:`Variable.astype` support
   the ``order`` and ``subok`` parameters again. This fixes a regression introduced in version 0.16.1
   (:issue:`4644`, :pull:`4683`).
@@ -42,6 +54,7 @@ Bug fixes
   By `Anderson Banihirwe <https://github.com/andersy005>`_
 - Fix a crash in orthogonal indexing on geographic coordinates with ``engine='cfgrib'`` (:issue:`4733` :pull:`4737`).
   By `Alessandro Amici <https://github.com/alexamici>`_
+- Limit number of data rows when printing large datasets. (:issue:`4736`, :pull:`4750`). By `Jimmy Westling <https://github.com/illviljan>`_.
 
 Documentation
 ~~~~~~~~~~~~~
@@ -63,6 +76,11 @@ Internal Changes
   - Run the tests in parallel using pytest-xdist (:pull:`4694`).
 
   By `Justus Magin <https://github.com/keewis>`_ and `Mathias Hauser <https://github.com/mathause>`_.
+  
+- Replace all usages of ``assert x.identical(y)`` with ``assert_identical(x,  y)`` 
+  for clearer error messages.
+  (:pull:`4752`);
+  By `Maximilian Roos <https://github.com/max-sixty>`_.
 
 .. _whats-new.0.16.2:
 
diff --git a/xarray/coding/times.py b/xarray/coding/times.py
index 59f8b897..3d877a16 100644
--- a/xarray/coding/times.py
+++ b/xarray/coding/times.py
@@ -26,6 +26,7 @@ from .variables import (
 _STANDARD_CALENDARS = {"standard", "gregorian", "proleptic_gregorian"}
 
 _NS_PER_TIME_DELTA = {
+    "ns": 1,
     "us": int(1e3),
     "ms": int(1e6),
     "s": int(1e9),
@@ -35,7 +36,15 @@ _NS_PER_TIME_DELTA = {
 }
 
 TIME_UNITS = frozenset(
-    ["days", "hours", "minutes", "seconds", "milliseconds", "microseconds"]
+    [
+        "days",
+        "hours",
+        "minutes",
+        "seconds",
+        "milliseconds",
+        "microseconds",
+        "nanoseconds",
+    ]
 )
 
 
@@ -44,6 +53,7 @@ def _netcdf_to_numpy_timeunit(units):
     if not units.endswith("s"):
         units = "%ss" % units
     return {
+        "nanoseconds": "ns",
         "microseconds": "us",
         "milliseconds": "ms",
         "seconds": "s",
@@ -151,21 +161,22 @@ def _decode_datetime_with_pandas(flat_num_dates, units, calendar):
         # strings, in which case we fall back to using cftime
         raise OutOfBoundsDatetime
 
-    # fixes: https://github.com/pydata/pandas/issues/14068
-    # these lines check if the the lowest or the highest value in dates
-    # cause an OutOfBoundsDatetime (Overflow) error
-    with warnings.catch_warnings():
-        warnings.filterwarnings("ignore", "invalid value encountered", RuntimeWarning)
-        pd.to_timedelta(flat_num_dates.min(), delta) + ref_date
-        pd.to_timedelta(flat_num_dates.max(), delta) + ref_date
-
-    # Cast input dates to integers of nanoseconds because `pd.to_datetime`
-    # works much faster when dealing with integers
-    # make _NS_PER_TIME_DELTA an array to ensure type upcasting
-    flat_num_dates_ns_int = (
-        flat_num_dates.astype(np.float64) * _NS_PER_TIME_DELTA[delta]
-    ).astype(np.int64)
+    # To avoid integer overflow when converting to nanosecond units for integer
+    # dtypes smaller than np.int64 cast all integer-dtype arrays to np.int64
+    # (GH 2002).
+    if flat_num_dates.dtype.kind == "i":
+        flat_num_dates = flat_num_dates.astype(np.int64)
 
+    # Cast input ordinals to integers of nanoseconds because pd.to_timedelta
+    # works much faster when dealing with integers (GH 1399).
+    flat_num_dates_ns_int = (flat_num_dates * _NS_PER_TIME_DELTA[delta]).astype(
+        np.int64
+    )
+
+    # Use pd.to_timedelta to safely cast integer values to timedeltas,
+    # and add those to a Timestamp to safely produce a DatetimeIndex.  This
+    # ensures that we do not encounter integer overflow at any point in the
+    # process without raising OutOfBoundsDatetime.
     return (pd.to_timedelta(flat_num_dates_ns_int, "ns") + ref_date).values
 
 
@@ -252,11 +263,24 @@ def decode_cf_timedelta(num_timedeltas, units):
 
 
 def _infer_time_units_from_diff(unique_timedeltas):
-    for time_unit in ["days", "hours", "minutes", "seconds"]:
+    # Note that the modulus operator was only implemented for np.timedelta64
+    # arrays as of NumPy version 1.16.0.  Once our minimum version of NumPy
+    # supported is greater than or equal to this we will no longer need to cast
+    # unique_timedeltas to a TimedeltaIndex.  In the meantime, however, the
+    # modulus operator works for TimedeltaIndex objects.
+    unique_deltas_as_index = pd.TimedeltaIndex(unique_timedeltas)
+    for time_unit in [
+        "days",
+        "hours",
+        "minutes",
+        "seconds",
+        "milliseconds",
+        "microseconds",
+        "nanoseconds",
+    ]:
         delta_ns = _NS_PER_TIME_DELTA[_netcdf_to_numpy_timeunit(time_unit)]
         unit_delta = np.timedelta64(delta_ns, "ns")
-        diffs = unique_timedeltas / unit_delta
-        if np.all(diffs == diffs.astype(int)):
+        if np.all(unique_deltas_as_index % unit_delta == np.timedelta64(0, "ns")):
             return time_unit
     return "seconds"
 
@@ -416,7 +440,15 @@ def encode_cf_datetime(dates, units=None, calendar=None):
         # Wrap the dates in a DatetimeIndex to do the subtraction to ensure
         # an OverflowError is raised if the ref_date is too far away from
         # dates to be encoded (GH 2272).
-        num = (pd.DatetimeIndex(dates.ravel()) - ref_date) / time_delta
+        dates_as_index = pd.DatetimeIndex(dates.ravel())
+        time_deltas = dates_as_index - ref_date
+
+        # Use floor division if time_delta evenly divides all differences
+        # to preserve integer dtype if possible (GH 4045).
+        if np.all(time_deltas % time_delta == np.timedelta64(0, "ns")):
+            num = time_deltas // time_delta
+        else:
+            num = time_deltas / time_delta
         num = num.values.reshape(dates.shape)
 
     except (OutOfBoundsDatetime, OverflowError):
diff --git a/xarray/core/formatting.py b/xarray/core/formatting.py
index de4c0efc..8dd8d43e 100644
--- a/xarray/core/formatting.py
+++ b/xarray/core/formatting.py
@@ -365,12 +365,23 @@ def _calculate_col_width(col_items):
     return col_width
 
 
-def _mapping_repr(mapping, title, summarizer, col_width=None):
+def _mapping_repr(mapping, title, summarizer, col_width=None, max_rows=None):
     if col_width is None:
         col_width = _calculate_col_width(mapping)
+    if max_rows is None:
+        max_rows = OPTIONS["display_max_rows"]
     summary = [f"{title}:"]
     if mapping:
-        summary += [summarizer(k, v, col_width) for k, v in mapping.items()]
+        if len(mapping) > max_rows:
+            first_rows = max_rows // 2 + max_rows % 2
+            items = list(mapping.items())
+            summary += [summarizer(k, v, col_width) for k, v in items[:first_rows]]
+            if max_rows > 1:
+                last_rows = max_rows // 2
+                summary += [pretty_print("    ...", col_width) + " ..."]
+                summary += [summarizer(k, v, col_width) for k, v in items[-last_rows:]]
+        else:
+            summary += [summarizer(k, v, col_width) for k, v in mapping.items()]
     else:
         summary += [EMPTY_REPR]
     return "\n".join(summary)
diff --git a/xarray/core/options.py b/xarray/core/options.py
index 07eddb49..d421b4c4 100644
--- a/xarray/core/options.py
+++ b/xarray/core/options.py
@@ -1,26 +1,28 @@
 import warnings
 
-DISPLAY_WIDTH = "display_width"
 ARITHMETIC_JOIN = "arithmetic_join"
+CMAP_DIVERGENT = "cmap_divergent"
+CMAP_SEQUENTIAL = "cmap_sequential"
+DISPLAY_MAX_ROWS = "display_max_rows"
+DISPLAY_STYLE = "display_style"
+DISPLAY_WIDTH = "display_width"
 ENABLE_CFTIMEINDEX = "enable_cftimeindex"
 FILE_CACHE_MAXSIZE = "file_cache_maxsize"
-WARN_FOR_UNCLOSED_FILES = "warn_for_unclosed_files"
-CMAP_SEQUENTIAL = "cmap_sequential"
-CMAP_DIVERGENT = "cmap_divergent"
 KEEP_ATTRS = "keep_attrs"
-DISPLAY_STYLE = "display_style"
+WARN_FOR_UNCLOSED_FILES = "warn_for_unclosed_files"
 
 
 OPTIONS = {
-    DISPLAY_WIDTH: 80,
     ARITHMETIC_JOIN: "inner",
+    CMAP_DIVERGENT: "RdBu_r",
+    CMAP_SEQUENTIAL: "viridis",
+    DISPLAY_MAX_ROWS: 12,
+    DISPLAY_STYLE: "html",
+    DISPLAY_WIDTH: 80,
     ENABLE_CFTIMEINDEX: True,
     FILE_CACHE_MAXSIZE: 128,
-    WARN_FOR_UNCLOSED_FILES: False,
-    CMAP_SEQUENTIAL: "viridis",
-    CMAP_DIVERGENT: "RdBu_r",
     KEEP_ATTRS: "default",
-    DISPLAY_STYLE: "html",
+    WARN_FOR_UNCLOSED_FILES: False,
 }
 
 _JOIN_OPTIONS = frozenset(["inner", "outer", "left", "right", "exact"])
@@ -32,13 +34,14 @@ def _positive_integer(value):
 
 
 _VALIDATORS = {
-    DISPLAY_WIDTH: _positive_integer,
     ARITHMETIC_JOIN: _JOIN_OPTIONS.__contains__,
+    DISPLAY_MAX_ROWS: _positive_integer,
+    DISPLAY_STYLE: _DISPLAY_OPTIONS.__contains__,
+    DISPLAY_WIDTH: _positive_integer,
     ENABLE_CFTIMEINDEX: lambda value: isinstance(value, bool),
     FILE_CACHE_MAXSIZE: _positive_integer,
-    WARN_FOR_UNCLOSED_FILES: lambda value: isinstance(value, bool),
     KEEP_ATTRS: lambda choice: choice in [True, False, "default"],
-    DISPLAY_STYLE: _DISPLAY_OPTIONS.__contains__,
+    WARN_FOR_UNCLOSED_FILES: lambda value: isinstance(value, bool),
 }
 
 
@@ -57,8 +60,8 @@ def _warn_on_setting_enable_cftimeindex(enable_cftimeindex):
 
 
 _SETTERS = {
-    FILE_CACHE_MAXSIZE: _set_file_cache_maxsize,
     ENABLE_CFTIMEINDEX: _warn_on_setting_enable_cftimeindex,
+    FILE_CACHE_MAXSIZE: _set_file_cache_maxsize,
 }
 
 


Test patch:
diff --git a/xarray/tests/test_coding_times.py b/xarray/tests/test_coding_times.py
index d35cad01..dfd558f7 100644
--- a/xarray/tests/test_coding_times.py
+++ b/xarray/tests/test_coding_times.py
@@ -6,7 +6,7 @@ import pandas as pd
 import pytest
 from pandas.errors import OutOfBoundsDatetime
 
-from xarray import DataArray, Dataset, Variable, coding, decode_cf
+from xarray import DataArray, Dataset, Variable, coding, conventions, decode_cf
 from xarray.coding.times import (
     cftime_to_nptime,
     decode_cf_datetime,
@@ -479,27 +479,36 @@ def test_decoded_cf_datetime_array_2d():
     assert_array_equal(np.asarray(result), expected)
 
 
+FREQUENCIES_TO_ENCODING_UNITS = {
+    "N": "nanoseconds",
+    "U": "microseconds",
+    "L": "milliseconds",
+    "S": "seconds",
+    "T": "minutes",
+    "H": "hours",
+    "D": "days",
+}
+
+
+@pytest.mark.parametrize(("freq", "units"), FREQUENCIES_TO_ENCODING_UNITS.items())
+def test_infer_datetime_units(freq, units):
+    dates = pd.date_range("2000", periods=2, freq=freq)
+    expected = f"{units} since 2000-01-01 00:00:00"
+    assert expected == coding.times.infer_datetime_units(dates)
+
+
 @pytest.mark.parametrize(
     ["dates", "expected"],
     [
-        (pd.date_range("1900-01-01", periods=5), "days since 1900-01-01 00:00:00"),
-        (
-            pd.date_range("1900-01-01 12:00:00", freq="H", periods=2),
-            "hours since 1900-01-01 12:00:00",
-        ),
         (
             pd.to_datetime(["1900-01-01", "1900-01-02", "NaT"]),
             "days since 1900-01-01 00:00:00",
         ),
-        (
-            pd.to_datetime(["1900-01-01", "1900-01-02T00:00:00.005"]),
-            "seconds since 1900-01-01 00:00:00",
-        ),
         (pd.to_datetime(["NaT", "1900-01-01"]), "days since 1900-01-01 00:00:00"),
         (pd.to_datetime(["NaT"]), "days since 1970-01-01 00:00:00"),
     ],
 )
-def test_infer_datetime_units(dates, expected):
+def test_infer_datetime_units_with_NaT(dates, expected):
     assert expected == coding.times.infer_datetime_units(dates)
 
 
@@ -535,6 +544,7 @@ def test_infer_cftime_datetime_units(calendar, date_args, expected):
         ("1h", "hours", np.int64(1)),
         ("1ms", "milliseconds", np.int64(1)),
         ("1us", "microseconds", np.int64(1)),
+        ("1ns", "nanoseconds", np.int64(1)),
         (["NaT", "0s", "1s"], None, [np.nan, 0, 1]),
         (["30m", "60m"], "hours", [0.5, 1.0]),
         ("NaT", "days", np.nan),
@@ -958,3 +968,30 @@ def test_decode_ambiguous_time_warns(calendar):
         assert not record
 
     np.testing.assert_array_equal(result, expected)
+
+
+@pytest.mark.parametrize("encoding_units", FREQUENCIES_TO_ENCODING_UNITS.values())
+@pytest.mark.parametrize("freq", FREQUENCIES_TO_ENCODING_UNITS.keys())
+def test_encode_cf_datetime_defaults_to_correct_dtype(encoding_units, freq):
+    times = pd.date_range("2000", periods=3, freq=freq)
+    units = f"{encoding_units} since 2000-01-01"
+    encoded, _, _ = coding.times.encode_cf_datetime(times, units)
+
+    numpy_timeunit = coding.times._netcdf_to_numpy_timeunit(encoding_units)
+    encoding_units_as_timedelta = np.timedelta64(1, numpy_timeunit)
+    if pd.to_timedelta(1, freq) >= encoding_units_as_timedelta:
+        assert encoded.dtype == np.int64
+    else:
+        assert encoded.dtype == np.float64
+
+
+@pytest.mark.parametrize("freq", FREQUENCIES_TO_ENCODING_UNITS.keys())
+def test_encode_decode_roundtrip(freq):
+    # See GH 4045. Prior to GH 4684 this test would fail for frequencies of
+    # "S", "L", "U", and "N".
+    initial_time = pd.date_range("1678-01-01", periods=1)
+    times = initial_time.append(pd.date_range("1968", periods=2, freq=freq))
+    variable = Variable(["time"], times)
+    encoded = conventions.encode_cf_variable(variable)
+    decoded = conventions.decode_cf_variable("time", encoded)
+    assert_equal(variable, decoded)
diff --git a/xarray/tests/test_combine.py b/xarray/tests/test_combine.py
index 86c96151..109b78f0 100644
--- a/xarray/tests/test_combine.py
+++ b/xarray/tests/test_combine.py
@@ -482,7 +482,7 @@ class TestNestedCombine:
 
         expected = data[["var1", "var2"]]
         actual = combine_nested(objs, concat_dim=[None, "dim2"])
-        assert expected.identical(actual)
+        assert_identical(expected, actual)
 
     def test_auto_combine_2d(self):
         ds = create_test_data
diff --git a/xarray/tests/test_computation.py b/xarray/tests/test_computation.py
index 1922977f..a1c8e75b 100644
--- a/xarray/tests/test_computation.py
+++ b/xarray/tests/test_computation.py
@@ -28,9 +28,11 @@ dask = pytest.importorskip("dask")
 
 
 def assert_identical(a, b):
+    """ A version of this function which accepts numpy arrays """
+    from xarray.testing import assert_identical as assert_identical_
+
     if hasattr(a, "identical"):
-        msg = f"not identical:\n{a!r}\n{b!r}"
-        assert a.identical(b), msg
+        assert_identical_(a, b)
     else:
         assert_array_equal(a, b)
 
@@ -1306,7 +1308,7 @@ def test_dot(use_dask):
     # for only a single array is passed without dims argument, just return
     # as is
     actual = xr.dot(da_a)
-    assert da_a.identical(actual)
+    assert_identical(da_a, actual)
 
     # test for variable
     actual = xr.dot(da_a.variable, da_b.variable)
diff --git a/xarray/tests/test_dask.py b/xarray/tests/test_dask.py
index 083740e3..19a61c60 100644
--- a/xarray/tests/test_dask.py
+++ b/xarray/tests/test_dask.py
@@ -731,11 +731,13 @@ class TestDataArrayAndDataset(DaskTestCase):
 class TestToDaskDataFrame:
     def test_to_dask_dataframe(self):
         # Test conversion of Datasets to dask DataFrames
-        x = da.from_array(np.random.randn(10), chunks=4)
+        x = np.random.randn(10)
         y = np.arange(10, dtype="uint8")
         t = list("abcdefghij")
 
-        ds = Dataset({"a": ("t", x), "b": ("t", y), "t": ("t", t)})
+        ds = Dataset(
+            {"a": ("t", da.from_array(x, chunks=4)), "b": ("t", y), "t": ("t", t)}
+        )
 
         expected_pd = pd.DataFrame({"a": x, "b": y}, index=pd.Index(t, name="t"))
 
@@ -758,8 +760,8 @@ class TestToDaskDataFrame:
 
     def test_to_dask_dataframe_2D(self):
         # Test if 2-D dataset is supplied
-        w = da.from_array(np.random.randn(2, 3), chunks=(1, 2))
-        ds = Dataset({"w": (("x", "y"), w)})
+        w = np.random.randn(2, 3)
+        ds = Dataset({"w": (("x", "y"), da.from_array(w, chunks=(1, 2)))})
         ds["x"] = ("x", np.array([0, 1], np.int64))
         ds["y"] = ("y", list("abc"))
 
@@ -791,10 +793,15 @@ class TestToDaskDataFrame:
 
     def test_to_dask_dataframe_coordinates(self):
         # Test if coordinate is also a dask array
-        x = da.from_array(np.random.randn(10), chunks=4)
-        t = da.from_array(np.arange(10) * 2, chunks=4)
+        x = np.random.randn(10)
+        t = np.arange(10) * 2
 
-        ds = Dataset({"a": ("t", x), "t": ("t", t)})
+        ds = Dataset(
+            {
+                "a": ("t", da.from_array(x, chunks=4)),
+                "t": ("t", da.from_array(t, chunks=4)),
+            }
+        )
 
         expected_pd = pd.DataFrame({"a": x}, index=pd.Index(t, name="t"))
         expected = dd.from_pandas(expected_pd, chunksize=4)
diff --git a/xarray/tests/test_dataarray.py b/xarray/tests/test_dataarray.py
index 1ddb97e5..204ff706 100644
--- a/xarray/tests/test_dataarray.py
+++ b/xarray/tests/test_dataarray.py
@@ -1494,8 +1494,8 @@ class TestDataArray:
         new1 = arr1.broadcast_like(arr2)
         new2 = arr2.broadcast_like(arr1)
 
-        assert orig1.identical(new1)
-        assert orig2.identical(new2)
+        assert_identical(orig1, new1)
+        assert_identical(orig2, new2)
 
         orig3 = DataArray(np.random.randn(5), [("x", range(5))])
         orig4 = DataArray(np.random.randn(6), [("y", range(6))])
diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py
index 099fb5c0..b007aa26 100644
--- a/xarray/tests/test_dataset.py
+++ b/xarray/tests/test_dataset.py
@@ -2127,7 +2127,8 @@ class TestDataset:
     def test_align_non_unique(self):
         x = Dataset({"foo": ("x", [3, 4, 5]), "x": [0, 0, 1]})
         x1, x2 = align(x, x)
-        assert x1.identical(x) and x2.identical(x)
+        assert_identical(x1, x)
+        assert_identical(x2, x)
 
         y = Dataset({"bar": ("x", [6, 7]), "x": [0, 1]})
         with raises_regex(ValueError, "cannot reindex or align"):
diff --git a/xarray/tests/test_extensions.py b/xarray/tests/test_extensions.py
index 5af0f6d8..fa91e5c8 100644
--- a/xarray/tests/test_extensions.py
+++ b/xarray/tests/test_extensions.py
@@ -4,7 +4,7 @@ import pytest
 
 import xarray as xr
 
-from . import raises_regex
+from . import assert_identical, raises_regex
 
 
 @xr.register_dataset_accessor("example_accessor")
@@ -61,20 +61,20 @@ class TestAccessor:
     def test_pickle_dataset(self):
         ds = xr.Dataset()
         ds_restored = pickle.loads(pickle.dumps(ds))
-        assert ds.identical(ds_restored)
+        assert_identical(ds, ds_restored)
 
         # state save on the accessor is restored
         assert ds.example_accessor is ds.example_accessor
         ds.example_accessor.value = "foo"
         ds_restored = pickle.loads(pickle.dumps(ds))
-        assert ds.identical(ds_restored)
+        assert_identical(ds, ds_restored)
         assert ds_restored.example_accessor.value == "foo"
 
     def test_pickle_dataarray(self):
         array = xr.Dataset()
         assert array.example_accessor is array.example_accessor
         array_restored = pickle.loads(pickle.dumps(array))
-        assert array.identical(array_restored)
+        assert_identical(array, array_restored)
 
     def test_broken_accessor(self):
         # regression test for GH933
diff --git a/xarray/tests/test_formatting.py b/xarray/tests/test_formatting.py
index f3434873..f2facf5b 100644
--- a/xarray/tests/test_formatting.py
+++ b/xarray/tests/test_formatting.py
@@ -463,3 +463,36 @@ def test_large_array_repr_length():
 
     result = repr(da).splitlines()
     assert len(result) < 50
+
+
+@pytest.mark.parametrize(
+    "display_max_rows, n_vars, n_attr",
+    [(50, 40, 30), (35, 40, 30), (11, 40, 30), (1, 40, 30)],
+)
+def test__mapping_repr(display_max_rows, n_vars, n_attr):
+    long_name = "long_name"
+    a = np.core.defchararray.add(long_name, np.arange(0, n_vars).astype(str))
+    b = np.core.defchararray.add("attr_", np.arange(0, n_attr).astype(str))
+    attrs = {k: 2 for k in b}
+    coords = dict(time=np.array([0, 1]))
+    data_vars = dict()
+    for v in a:
+        data_vars[v] = xr.DataArray(
+            name=v,
+            data=np.array([3, 4]),
+            dims=["time"],
+            coords=coords,
+        )
+    ds = xr.Dataset(data_vars)
+    ds.attrs = attrs
+
+    with xr.set_options(display_max_rows=display_max_rows):
+
+        # Parse the data_vars print and show only data_vars rows:
+        summary = formatting.data_vars_repr(ds.data_vars).split("\n")
+        summary = [v for v in summary if long_name in v]
+
+        # The length should be less than or equal to display_max_rows:
+        len_summary = len(summary)
+        data_vars_print_size = min(display_max_rows, len_summary)
+        assert len_summary == data_vars_print_size
diff --git a/xarray/tests/test_merge.py b/xarray/tests/test_merge.py
index afb65a35..34b138e1 100644
--- a/xarray/tests/test_merge.py
+++ b/xarray/tests/test_merge.py
@@ -4,7 +4,7 @@ import pytest
 import xarray as xr
 from xarray.core import dtypes, merge
 from xarray.core.merge import MergeError
-from xarray.testing import assert_identical
+from xarray.testing import assert_equal, assert_identical
 
 from . import raises_regex
 from .test_dataset import create_test_data
@@ -33,17 +33,17 @@ class TestMergeFunction:
         data = create_test_data()
         actual = xr.merge([data.var1, data.var2])
         expected = data[["var1", "var2"]]
-        assert actual.identical(expected)
+        assert_identical(actual, expected)
 
     def test_merge_datasets(self):
         data = create_test_data()
 
         actual = xr.merge([data[["var1"]], data[["var2"]]])
         expected = data[["var1", "var2"]]
-        assert actual.identical(expected)
+        assert_identical(actual, expected)
 
         actual = xr.merge([data, data])
-        assert actual.identical(data)
+        assert_identical(actual, data)
 
     def test_merge_dataarray_unnamed(self):
         data = xr.DataArray([1, 2], dims="x")
@@ -61,7 +61,7 @@ class TestMergeFunction:
         actual = xr.merge([data.var1, data.var2])
         expected = data[["var1", "var2"]]
         expected.attrs = expected_attrs
-        assert actual.identical(expected)
+        assert_identical(actual, expected)
 
     @pytest.mark.parametrize(
         "combine_attrs, var1_attrs, var2_attrs, expected_attrs, expect_exception",
@@ -107,7 +107,7 @@ class TestMergeFunction:
             actual = xr.merge([data.var1, data.var2], combine_attrs=combine_attrs)
             expected = data[["var1", "var2"]]
             expected.attrs = expected_attrs
-            assert actual.identical(expected)
+            assert_identical(actual, expected)
 
     def test_merge_attrs_override_copy(self):
         ds1 = xr.Dataset(attrs={"x": 0})
@@ -119,12 +119,12 @@ class TestMergeFunction:
     def test_merge_dicts_simple(self):
         actual = xr.merge([{"foo": 0}, {"bar": "one"}, {"baz": 3.5}])
         expected = xr.Dataset({"foo": 0, "bar": "one", "baz": 3.5})
-        assert actual.identical(expected)
+        assert_identical(actual, expected)
 
     def test_merge_dicts_dims(self):
         actual = xr.merge([{"y": ("x", [13])}, {"x": [12]}])
         expected = xr.Dataset({"x": [12], "y": ("x", [13])})
-        assert actual.identical(expected)
+        assert_identical(actual, expected)
 
     def test_merge_error(self):
         ds = xr.Dataset({"x": 0})
@@ -174,7 +174,7 @@ class TestMergeFunction:
 
         expected = data[["var1", "var2"]]
         actual = xr.merge([data1.var1, data2.var2], compat="no_conflicts")
-        assert expected.identical(actual)
+        assert_identical(expected, actual)
 
         data1["var1"][:, :5] = np.nan
         data2["var1"][:, 5:] = np.nan
@@ -183,22 +183,22 @@ class TestMergeFunction:
         del data2["var3"]
 
         actual = xr.merge([data1, data2], compat="no_conflicts")
-        assert data.equals(actual)
+        assert_equal(data, actual)
 
     def test_merge_no_conflicts_preserve_attrs(self):
         data = xr.Dataset({"x": ([], 0, {"foo": "bar"})})
         actual = xr.merge([data, data])
-        assert data.identical(actual)
+        assert_identical(data, actual)
 
     def test_merge_no_conflicts_broadcast(self):
         datasets = [xr.Dataset({"x": ("y", [0])}), xr.Dataset({"x": np.nan})]
         actual = xr.merge(datasets)
         expected = xr.Dataset({"x": ("y", [0])})
-        assert expected.identical(actual)
+        assert_identical(expected, actual)
 
         datasets = [xr.Dataset({"x": ("y", [np.nan])}), xr.Dataset({"x": 0})]
         actual = xr.merge(datasets)
-        assert expected.identical(actual)
+        assert_identical(expected, actual)
 
 
 class TestMergeMethod:
@@ -208,17 +208,17 @@ class TestMergeMethod:
         ds2 = data[["var3"]]
         expected = data[["var1", "var3"]]
         actual = ds1.merge(ds2)
-        assert expected.identical(actual)
+        assert_identical(expected, actual)
 
         actual = ds2.merge(ds1)
-        assert expected.identical(actual)
+        assert_identical(expected, actual)
 
         actual = data.merge(data)
-        assert data.identical(actual)
+        assert_identical(data, actual)
         actual = data.reset_coords(drop=True).merge(data)
-        assert data.identical(actual)
+        assert_identical(data, actual)
         actual = data.merge(data.reset_coords(drop=True))
-        assert data.identical(actual)
+        assert_identical(data, actual)
 
         with pytest.raises(ValueError):
             ds1.merge(ds2.rename({"var3": "var1"}))
@@ -231,19 +231,19 @@ class TestMergeMethod:
         ds1 = xr.Dataset({"x": 0})
         ds2 = xr.Dataset({"x": ("y", [0, 0])})
         actual = ds1.merge(ds2)
-        assert ds2.identical(actual)
+        assert_identical(ds2, actual)
 
         actual = ds2.merge(ds1)
-        assert ds2.identical(actual)
+        assert_identical(ds2, actual)
 
         actual = ds1.copy()
         actual.update(ds2)
-        assert ds2.identical(actual)
+        assert_identical(ds2, actual)
 
         ds1 = xr.Dataset({"x": np.nan})
         ds2 = xr.Dataset({"x": ("y", [np.nan, np.nan])})
         actual = ds1.merge(ds2)
-        assert ds2.identical(actual)
+        assert_identical(ds2, actual)
 
     def test_merge_compat(self):
         ds1 = xr.Dataset({"x": 0})
diff --git a/xarray/tests/test_sparse.py b/xarray/tests/test_sparse.py
index a746a676..49b6a586 100644
--- a/xarray/tests/test_sparse.py
+++ b/xarray/tests/test_sparse.py
@@ -651,7 +651,7 @@ class TestSparseDataArrayAndDataset:
         assert_equal(expected, stacked)
 
         roundtripped = stacked.unstack()
-        assert arr.identical(roundtripped)
+        assert_identical(arr, roundtripped)
 
     @pytest.mark.filterwarnings("ignore::PendingDeprecationWarning")
     def test_ufuncs(self):
diff --git a/xarray/tests/test_variable.py b/xarray/tests/test_variable.py
index ecd53aa9..91eeb044 100644
--- a/xarray/tests/test_variable.py
+++ b/xarray/tests/test_variable.py
@@ -1392,7 +1392,7 @@ class TestVariable(VariableSubclassobjects):
         ]:
             variable = Variable([], value)
             actual = variable.transpose()
-            assert actual.identical(variable)
+            assert_identical(actual, variable)
 
     def test_squeeze(self):
         v = Variable(["x", "y"], [[1]])
@@ -1445,7 +1445,7 @@ class TestVariable(VariableSubclassobjects):
         for i in range(3):
             exp_values[i] = ("a", 1)
         expected = Variable(["x"], exp_values)
-        assert actual.identical(expected)
+        assert_identical(actual, expected)
 
     def test_stack(self):
         v = Variable(["x", "y"], [[0, 1], [2, 3]], {"foo": "bar"})
@@ -2075,12 +2075,12 @@ class TestIndexVariable(VariableSubclassobjects):
         coords = [IndexVariable("t", periods[:5]), IndexVariable("t", periods[5:])]
         expected = IndexVariable("t", periods)
         actual = IndexVariable.concat(coords, dim="t")
-        assert actual.identical(expected)
+        assert_identical(actual, expected)
         assert isinstance(actual.to_index(), pd.PeriodIndex)
 
         positions = [list(range(5)), list(range(5, 10))]
         actual = IndexVariable.concat(coords, dim="t", positions=positions)
-        assert actual.identical(expected)
+        assert_identical(actual, expected)
         assert isinstance(actual.to_index(), pd.PeriodIndex)
 
     def test_concat_multiindex(self):
@@ -2088,7 +2088,7 @@ class TestIndexVariable(VariableSubclassobjects):
         coords = [IndexVariable("x", idx[:2]), IndexVariable("x", idx[2:])]
         expected = IndexVariable("x", idx)
         actual = IndexVariable.concat(coords, dim="x")
-        assert actual.identical(expected)
+        assert_identical(actual, expected)
         assert isinstance(actual.to_index(), pd.MultiIndex)
 
     def test_coordinate_alias(self):